diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6dfb5e1 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,229 @@ +cmake_policy(SET CMP0020 NEW) +cmake_policy(SET CMP0048 NEW) +cmake_policy(SET CMP0057 NEW) +if(NOT ${CMAKE_VERSION} VERSION_LESS 3.10) + cmake_policy(SET CMP0071 OLD) +endif() + +# Debug includes RatapiDebug, WebDebug +# Release includes RatapiRelease, WebRelease +# EngineDebug +# EngineRelease +# RatapiDebug +# RatapiRelease +# WebDebug +# WebRelease +set(default_build_type "Debug") +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "${default_build_type}") +endif() + +if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug" OR + "${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo") + set(OPENAFC_BUILD_TYPE "EngineDebug" "RatapiDebug" "WebDebug") +elseif ("${CMAKE_BUILD_TYPE}" STREQUAL "Release") + set(OPENAFC_BUILD_TYPE "EngineRelease" "RatapiRelease" "WebRelease") +elseif (${CMAKE_BUILD_TYPE} MATCHES "EngineRatapiDebug") + set(OPENAFC_BUILD_TYPE "EngineDebug" "RatapiDebug") +elseif (${CMAKE_BUILD_TYPE} MATCHES "EngineRatapiRelease") + set(OPENAFC_BUILD_TYPE "EngineRelease" "RatapiRelease") +elseif (${CMAKE_BUILD_TYPE} MATCHES "RatapiWebDebug") + set(OPENAFC_BUILD_TYPE "RatapiDebug" "WebDebug") +elseif (${CMAKE_BUILD_TYPE} MATCHES "Ulsprocessor") + set(OPENAFC_BUILD_TYPE "Ulsprocessor") +else() + set(OPENAFC_BUILD_TYPE "${CMAKE_BUILD_TYPE}") +endif() + +message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") +message(STATUS "OPENAFC_BUILD_TYPE: ${OPENAFC_BUILD_TYPE}") + +# External version naming +file(READ "${CMAKE_SOURCE_DIR}/version.txt" VERSIONFILE) +string(STRIP ${VERSIONFILE} VERSIONFILE) + +if ("EngineDebug" IN_LIST OPENAFC_BUILD_TYPE OR + "EngineRelease" IN_LIST OPENAFC_BUILD_TYPE OR + "Ulsprocessor" IN_LIST OPENAFC_BUILD_TYPE) + project(fbrat VERSION ${VERSIONFILE}) +else() + project(fbrat VERSION ${VERSIONFILE} LANGUAGES) +endif() + +cmake_minimum_required(VERSION 3.4) +set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake ${CMAKE_MODULE_PATH}) +option(BUILD_AFCENGINE "Build the AFC engine portion of the project" ON) + +# Shared library ABI versioning +set(SOVERSION "${PROJECT_VERSION}") + +if ("EngineDebug" IN_LIST OPENAFC_BUILD_TYPE OR + "EngineRelease" IN_LIST OPENAFC_BUILD_TYPE OR + "Ulsprocessor" IN_LIST OPENAFC_BUILD_TYPE) + # Compiler and linker config + set(CMAKE_CXX_STANDARD 11) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + #set(CMAKE_CXX_EXTENSIONS ON) # No GNU/MSVC extensions + if(UNIX) + set(CMAKE_C_FLAGS "${CMAKE_CXX_FLAGS} -lbsd") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -lbsd") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") + #set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror") + endif(UNIX) + if(WIN32) + # Fix use of min()/max() in MSVC + add_definitions("-D_USE_MATH_DEFINES -DNOMINMAX") + # Attempt use of cmake auto-export + set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) + + # /bigobj Increases Number of Sections in .Obj file (needed for projects with large number of inline functions) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") + # Ignore warning from CppMicroServices lib + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4180") + # Ignore generic C++ naming warnings and template-interface-export warning + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4503 /wd4251 /wd4275") + + # Search conan-package binaries also + list(APPEND CMAKE_PROGRAM_PATH ${CONAN_BIN_DIRS}) + + add_definitions("-DARMA_USE_CXX11") # Workaround MSVC lack of __cplusplus + add_definitions("-DCPL_DISABLE_DLL") # Workaround issue with "dllexport" in "cpl_port.h" + endif(WIN32) + + # debug build flags + if (DEBUG_AFC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DDEBUG_AFC=1") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=format-extra-args") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=format") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=shadow") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=switch") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=return-type") + # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg") + message(STATUS "Using DEBUG_AFC build mode") + endif() + + if ("Ulsprocessor" IN_LIST OPENAFC_BUILD_TYPE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -larmadillo") + endif() + + message("CMAKE_C_FLAGS ${CMAKE_C_FLAGS}") + message("CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}") +endif() + + + +# Coverage analysis +include(CheckCoverage) + +# Flag for special compilation flag for Fedora Build +if (OS_FEDORA) + add_definitions("-DOS_FEDORA") +endif(OS_FEDORA) + +# Standard installation paths +# - PKG_INSTALL_LIBDIR directory of windows ".lib" files +# - PKG_INSTALL_BINDIR directory of windows ".dll" files and unix ".so" files +# - PKG_INSTALL_DATADIR directory for shared application-specific data +# - PKG_INSTALL_SYSCONFDIR root directory for system-default config files +# - PKG_MODULE_LIBDIR directory for storing plugin module shared objects +if(UNIX) + # Name for config/data files under standard paths (incl. XDG paths) + set(PKG_APP_NAME "${PROJECT_NAME}") + include(GNUInstallDirs) + if(SHARE_INSTALL_PREFIX) + set(CMAKE_INSTALL_DATADIR ${SHARE_INSTALL_PREFIX}) + endif(SHARE_INSTALL_PREFIX) + if(SYSCONF_INSTALL_DIR) + set(CMAKE_INSTALL_SYSCONFDIR ${SYSCONF_INSTALL_DIR}) + endif(SYSCONF_INSTALL_DIR) + # Directly in system paths + set(PKG_INSTALL_LIBDIR ${CMAKE_INSTALL_LIBDIR}) + set(PKG_INSTALL_BINDIR ${CMAKE_INSTALL_BINDIR}) + set(PKG_INSTALL_SBINDIR ${CMAKE_INSTALL_SBINDIR}) + # Suffix under system paths + set(PKG_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR}/${PKG_APP_NAME}) + set(PKG_INSTALL_DATADIR ${CMAKE_INSTALL_DATADIR}/${PKG_APP_NAME}) + set(PKG_INSTALL_SYSCONFDIR ${CMAKE_INSTALL_SYSCONFDIR}) + set(XDG_INSTALL_SYSCONFDIR "${CMAKE_INSTALL_SYSCONFDIR}/xdg") +endif(UNIX) +if(WIN32) + # Name for config/data files under standard paths (inc. %PROGRAMFILES%) + set(PKG_APP_NAME "${PROJECT_NAME}") + # All files under common PREFIX path (%PROGRAMFILES%/) + set(PKG_INSTALL_INCLUDEDIR "include") + # Libaries to link + set(PKG_INSTALL_LIBDIR "lib") + # Runtime binaries + set(PKG_INSTALL_BINDIR "bin") + set(PKG_INSTALL_SBINDIR "bin") + # External debug symbols + set(PKG_INSTALL_DEBUGDIR "debug") + # To be consistent with QStandardPaths::AppDataLocation + set(CMAKE_INSTALL_DATADIR "bin/data") + set(PKG_INSTALL_DATADIR "${CMAKE_INSTALL_DATADIR}/${PKG_APP_NAME}") + # on windows config is within datadir + set(PKG_INSTALL_SYSCONFDIR ${CMAKE_INSTALL_DATADIR}) + set(XDG_INSTALL_SYSCONFDIR "${CMAKE_INSTALL_DATADIR}") +endif(WIN32) +# Extended paths +set(XDG_ICONS_INSTALL_DIR "${CMAKE_INSTALL_DATADIR}/icons") +set(XDG_MIME_INSTALL_DIR "${CMAKE_INSTALL_DATADIR}/mime") +set(XDG_APPS_INSTALL_DIR "${CMAKE_INSTALL_DATADIR}/applications") +set(CMAKE_MODULE_NAME ${PROJECT_NAME}) +set(PKG_INSTALL_CMAKE_CONFIG_DIR "${PKG_INSTALL_LIBDIR}/cmake/${CMAKE_MODULE_NAME}") + +if ("Ulsprocessor" IN_LIST OPENAFC_BUILD_TYPE) + find_package(Qt5Core 5.3 REQUIRED) +endif() + +# build only engine code +if ("EngineDebug" IN_LIST OPENAFC_BUILD_TYPE OR + "EngineRelease" IN_LIST OPENAFC_BUILD_TYPE) + # External libraries + find_package(Qt5Core 5.3 REQUIRED) + find_package(Qt5Concurrent REQUIRED) + find_package(Qt5Network REQUIRED) + find_package(Qt5Gui REQUIRED) + find_package(Qt5Sql REQUIRED) + find_package(Qt5Test REQUIRED) + find_package(Qt5Xml REQUIRED) + find_package(Qt5Widgets REQUIRED) + + find_package(Armadillo REQUIRED) + find_package(ZLIB REQUIRED) + find_package(minizip REQUIRED) + + find_package(GDAL REQUIRED) + if(NOT GDAL_FOUND) + message(FATAL_ERROR "Missing GDAL library") + endif() + if(GDAL_FOUND AND NOT TARGET GDAL::GDAL) + add_library(GDAL::GDAL UNKNOWN IMPORTED) + set_target_properties(GDAL::GDAL PROPERTIES + IMPORTED_LOCATION "${GDAL_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${GDAL_INCLUDE_DIR}" + ) + endif() + + set(Boost_USE_MULTITHREADED ON) + set(Boost_USE_STATIC_RUNTIME OFF) + find_package(Boost 1.54 REQUIRED COMPONENTS log program_options regex system thread) + add_definitions("-DBOOST_ALL_DYN_LINK") + +endif() + + +# External library search options +if(WIN32) + SET(CMAKE_FIND_LIBRARY_PREFIXES "") + SET(CMAKE_FIND_LIBRARY_SUFFIXES ".dll") + set(PATH_OPTS NO_DEFAULT_PATH NO_CMAKE_ENVIRONMENT_PATH NO_SYSTEM_ENVIRONMENT_PATH NO_CMAKE_SYSTEM_PATH) +endif(WIN32) + +# Build/install in source path +add_subdirectory(src) +add_subdirectory(pkg) + +if(APIDOC_INSTALL_PATH) + configure_file(Doxyfile.in Doxyfile @ONLY) +endif() diff --git a/CPPLINT.cfg b/CPPLINT.cfg new file mode 100644 index 0000000..b8e8ec1 --- /dev/null +++ b/CPPLINT.cfg @@ -0,0 +1,2 @@ +filter=-whitespace/braces +filter=-whitespace/parens diff --git a/Customization.md b/Customization.md new file mode 100644 index 0000000..f3670eb --- /dev/null +++ b/Customization.md @@ -0,0 +1,106 @@ +Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +refers solely to the Broadcom Inc. corporate affiliate that owns +the software below. +This work is licensed under the OpenAFC Project License, a copy of which is included with this software program. + +# About Page Customization +When a user first enter the webpage without loggin in, the about screen can be accessed on the navigation menu on the left side. The about screen instructs the user of next steps to gain an account. About page customization can be done via environment variables or via json file. +## Environment Variables +### Captcha +Captcha configuration is needed if Captcha is to be enabled in the About page to protect the +access request form. +### Captcha Config + +``` +USE_CAPTCHA=True +CAPTCHA_SECRET='your-captcha-secrets' +CAPTCHA_SITEKEY='your-captcha-sitekey' +CAPTCHA_VERIFY='url-to-verify-captcha' +``` + +### Optional Mail Server Configuration +If not, specified, a local server implementation is used, which does not use any encryption. +``` +MAIL_SERVER= 'smtp.gmail.com' +MAIL_PORT= 465 +MAIL_USE_TLS = False +MAIL_USE_SSL = True +MAIL_USERNAME= 'afc-management-email-address' +MAIL_PASSWORD = "password" +``` + +### Mail configuration +Mail configuration specifies where email are sent, and what email account is used to send them. This is used by the AFC server to send notifications to the admin when a new user signs up. +This is required for a functional About page to handle access requests submitted via the web form. + +``` +REGISTRATION_DEST_EMAIL = 'where-the-registration-email-is-sent' +REGISTRATION_DEST_PDL_EMAIL = 'group-where-the-registration-email-is-sent' +REGISTRATION_SRC_EMAIL = MAIL_USERNAME +REGISTRATION_APPROVE_LINK='approval link to include in email' +``` + +##Json file config +An preferred method other than using environment variables is via json config files. The json file +is to be put in a volume mounted on the container, and the path must be provided to the server +via environment variable (RATAPI_ARGR) e.g. using docker-compose environment variable or using secrets. + +e.g. +Docker compose file: +``` + rat_server: + environment: + - RATAPI_ARG=/path/ratapi_config.json +``` + +The content of the json file is as below: +``` +{ + "USE_CAPTCHA":"True", + "CAPTCHA_SECRET":"somevalue" + "CAPTCHA_SITEKEY":"somevalue" + "CAPTCHA_VERIFY":"https://www.google.com/recaptcha/api/siteverify", + + "MAIL_SERVER":"smtp.gmail.com", + "MAIL_PORT":"465", + "MAIL_USE_TLS":"False", + "MAIL_USE_SSL":"True", + "MAIL_USERNAME":"afc-management-email-address" + "MAIL_PASSWORD":"password", + + "REGISTRATION_DEST_EMAIL":"where-the-registration-email-is-sent" + "REGISTRATION_DEST_PDL_EMAIL":"group-where-the-registration-email-is-sent" + "REGISTRATION_SRC_EMAIL":"afc-management-email-address" + "REGISTRATION_APPROVE_LINK":"approval link to include in email" +} +``` +Note that the path must be accessible to the httpd which runs under fbrat +``` +chown -R 1003:1003 /localpath +``` + + +# OIDC Configuration +See OIDC_Login.md + +# Miscellaneous Configurations +## private file +Under root of the source code (at same level as src), you can create private directory. +The structure: +private/ + templates/ + images/ +Under templates: various templates that can be used to customize various web form, for eg. + about.html: This is the page the user can access first to sign up as a new user. This can be customized to give more detail sign up instruction + flask_user_layout.html: to customize the login page for the non-OIDC method. +Under images: the files here customize various images of the web page. + logo.png: the company logo on the Information (i) page + background.png: the background image on the Information (i) page + +## Company Name: + The config json file (RATAPI_ARG) accepts entry for "USER_APP_NAME" to customize the company name that appears in + various parts of the webpage. e.g. + { + ... snip ... + "USER_APP_NAME":"My company AFC" + } diff --git a/Doxyfile.in b/Doxyfile.in new file mode 100644 index 0000000..40cac0b --- /dev/null +++ b/Doxyfile.in @@ -0,0 +1,1777 @@ +# Doxyfile 1.8.3.1 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (" "). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# http://www.gnu.org/software/libiconv for the list of possible encodings. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or sequence of words) that should +# identify the project. Note that if you do not use Doxywizard you need +# to put quotes around the project name if it contains spaces. + +PROJECT_NAME = "OpenAFC" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. +# This could be handy for archiving the generated documentation or +# if some version control system is used. + +PROJECT_NUMBER = "@PROJECT_VERSION@-@SVN_LAST_REVISION@" + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer +# a quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "RLAN OpenAFC Tool" + +# With the PROJECT_LOGO tag one can specify an logo or icon that is +# included in the documentation. The maximum height of the logo should not +# exceed 55 pixels and the maximum width should not exceed 200 pixels. +# Doxygen will copy the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) +# base path where the generated documentation will be put. +# If a relative path is entered, it will be relative to the location +# where doxygen was started. If left blank the current directory will be used. + +OUTPUT_DIRECTORY = @APIDOC_INSTALL_PATH@ + +# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create +# 4096 sub-directories (in 2 levels) under the output directory of each output +# format and will distribute the generated files over these directories. +# Enabling this option can be useful when feeding doxygen a huge amount of +# source files, where putting all generated files in the same directory would +# otherwise cause performance problems for the file system. + +CREATE_SUBDIRS = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# The default language is English, other supported languages are: +# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, +# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German, +# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English +# messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, +# Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, +# Slovene, Spanish, Swedish, Ukrainian, and Vietnamese. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will +# include brief member descriptions after the members that are listed in +# the file and class documentation (similar to JavaDoc). +# Set to NO to disable this. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend +# the brief description of a member or function before the detailed description. +# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator +# that is used to form the text in various listings. Each string +# in this list, if found as the leading text of the brief description, will be +# stripped from the text and the result after processing the whole list, is +# used as the annotated text. Otherwise, the brief description is used as-is. +# If left blank, the following values are used ("$name" is automatically +# replaced with the name of the entity): "The $name class" "The $name widget" +# "The $name file" "is" "provides" "specifies" "contains" +# "represents" "a" "an" "the" + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# Doxygen will generate a detailed section even if there is only a brief +# description. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full +# path before files name in the file list and in the header files. If set +# to NO the shortest path that makes the file name unique will be used. + +FULL_PATH_NAMES = YES + +# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag +# can be used to strip a user-defined part of the path. Stripping is +# only done if one of the specified strings matches the left-hand part of +# the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the +# path to strip. Note that you specify absolute paths here, but also +# relative paths, which will be relative from the directory where doxygen is +# started. + +STRIP_FROM_PATH = "@CMAKE_SOURCE_DIR@/src" + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of +# the path mentioned in the documentation of a class, which tells +# the reader which header file to include in order to use a class. +# If left blank only the name of the header file containing the class +# definition is used. Otherwise one should specify the include paths that +# are normally passed to the compiler using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter +# (but less readable) file names. This can be useful if your file system +# doesn't support long names like on DOS, Mac, or CD-ROM. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen +# will interpret the first line (until the first dot) of a JavaDoc-style +# comment as the brief description. If set to NO, the JavaDoc +# comments will behave just like regular Qt-style comments +# (thus requiring an explicit @brief command for a brief description.) + +JAVADOC_AUTOBRIEF = YES + +# If the QT_AUTOBRIEF tag is set to YES then Doxygen will +# interpret the first line (until the first dot) of a Qt-style +# comment as the brief description. If set to NO, the comments +# will behave just like regular Qt-style comments (thus requiring +# an explicit \brief command for a brief description.) + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen +# treat a multi-line C++ special comment block (i.e. a block of //! or /// +# comments) as a brief description. This used to be the default behaviour. +# The new default is to treat a multi-line C++ comment block as a detailed +# description. Set this tag to YES if you prefer the old behaviour instead. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented +# member inherits the documentation from any documented member that it +# re-implements. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce +# a new page for each member. If set to NO, the documentation of a member will +# be part of the file/class/namespace that contains it. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. +# Doxygen uses this value to replace tabs by spaces in code fragments. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that acts +# as commands in the documentation. An alias has the form "name=value". +# For example adding "sideeffect=\par Side Effects:\n" will allow you to +# put the command \sideeffect (or @sideeffect) in the documentation, which +# will result in a user-defined paragraph with heading "Side Effects:". +# You can put \n's in the value part of an alias to insert newlines. + +ALIASES = + +# Starting block for citation list, parameter is group name +ALIASES += beginxdocs{1}="
\1:
" +# A single citation with items {name,version} +ALIASES += xdoc{2}="" +# End a single citation list +ALIASES += endxdocs="
\anchor \1 \1\2" +ALIASES += endxdoc="
" + +# Cross-reference a citation with square-bracket notation +ALIASES += xref{1}="\ref \1 \"[\1]\"" + + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding +# "class=itcl::class" will allow you to use the command class in the +# itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C +# sources only. Doxygen will then generate output that is more tailored for C. +# For instance, some of the names that are used will be different. The list +# of all members will be omitted, etc. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java +# sources only. Doxygen will then generate output that is more tailored for +# Java. For instance, namespaces will be presented as packages, qualified +# scopes will look different, etc. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources only. Doxygen will then generate output that is more tailored for +# Fortran. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for +# VHDL. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, +# and language is one of the parsers supported by doxygen: IDL, Java, +# Javascript, CSharp, C, C++, D, PHP, Objective-C, Python, Fortran, VHDL, C, +# C++. For instance to make doxygen treat .inc files as Fortran files (default +# is PHP), and .f files as C (default is Fortran), use: inc=Fortran f=C. Note +# that for custom extensions you also need to set FILE_PATTERNS otherwise the +# files are not read by doxygen. + +EXTENSION_MAPPING = + +# If MARKDOWN_SUPPORT is enabled (the default) then doxygen pre-processes all +# comments according to the Markdown format, which allows for more readable +# documentation. See http://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you +# can mix doxygen, HTML, and XML commands with Markdown formatting. +# Disable only in case of backward compatibilities issues. + +MARKDOWN_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should +# set this tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. +# func(std::string) {}). This also makes the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. + +BUILTIN_STL_SUPPORT = YES + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. +# Doxygen will parse them like normal C++ but will assume all classes use public +# instead of private inheritance when no explicit protection keyword is present. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES (the +# default) will make doxygen replace the get and set methods by a property in +# the documentation. This will only work if the methods are indeed getting or +# setting a simple type. If this is not the case, or you want to show the +# methods anyway, you should set this option to NO. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. + +DISTRIBUTE_GROUP_DOC = YES + +# Set the SUBGROUPING tag to YES (the default) to allow class member groups of +# the same type (for instance a group of public functions) to be put as a +# subgroup of that type (e.g. under the Public Functions section). Set it to +# NO to prevent subgrouping. Alternatively, this can be done per class using +# the \nosubgrouping command. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and +# unions are shown inside the group in which they are included (e.g. using +# @ingroup) instead of on a separate page (for HTML and Man pages) or +# section (for LaTeX and RTF). + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and +# unions with only public data fields will be shown inline in the documentation +# of the scope in which they are defined (i.e. file, namespace, or group +# documentation), provided this scope is documented. If set to NO (the default), +# structs, classes, and unions are shown on a separate page (for HTML and Man +# pages) or section (for LaTeX and RTF). + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum +# is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically +# be useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. + +TYPEDEF_HIDES_STRUCT = NO + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. +# Private class members and static file members will be hidden unless +# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class +# will be included in the documentation. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal +# scope will be included in the documentation. + +EXTRACT_PACKAGE = YES + +# If the EXTRACT_STATIC tag is set to YES all static members of a file +# will be included in the documentation. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) +# defined locally in source files will be included in the documentation. +# If set to NO only classes defined in header files are included. + +EXTRACT_LOCAL_CLASSES = NO + +# This flag is only useful for Objective-C code. When set to YES local +# methods, which are defined in the implementation section but not in +# the interface are included in the documentation. +# If set to NO (the default) only methods in the interface are included. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base +# name of the file that contains the anonymous namespace. By default +# anonymous namespaces are hidden. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members of documented classes, files or namespaces. +# If set to NO (the default) these members will be included in the +# various overviews, but no documentation section is generated. +# This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. +# If set to NO (the default) these classes will be included in the various +# overviews. This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all +# friend (class|struct|union) declarations. +# If set to NO (the default) these declarations will be included in the +# documentation. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. +# If set to NO (the default) these blocks will be appended to the +# function's detailed documentation block. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation +# that is typed after a \internal command is included. If the tag is set +# to NO (the default) then the documentation will be excluded. +# Set it to YES to include the internal documentation. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate +# file names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen +# will show members with their full class and namespace scopes in the +# documentation. If set to YES the scope will be hidden. + +HIDE_SCOPE_NAMES = NO + +# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen +# will put a list of the files that are included by a file in the documentation +# of that file. + +SHOW_INCLUDE_FILES = YES + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen +# will list include files with double quotes in the documentation +# rather than with sharp brackets. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] +# is inserted in the documentation for inline members. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen +# will sort the (detailed) documentation of file and class members +# alphabetically by member name. If set to NO the members will appear in +# declaration order. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the +# brief documentation of file, namespace and class members alphabetically +# by member name. If set to NO (the default) the members will appear in +# declaration order. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen +# will sort the (brief and detailed) documentation of class members so that +# constructors and destructors are listed first. If set to NO (the default) +# the constructors will appear in the respective orders defined by +# SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. +# This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO +# and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the +# hierarchy of group names into alphabetical order. If set to NO (the default) +# the group names will appear in their defined order. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be +# sorted by fully-qualified names, including namespaces. If set to +# NO (the default), the class list will be sorted only by class name, +# not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the +# alphabetical list. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to +# do proper type resolution of all parameters of a function it will reject a +# match between the prototype and the implementation of a member function even +# if there is only one candidate or it is obvious which candidate to choose +# by doing a simple string match. By disabling STRICT_PROTO_MATCHING doxygen +# will still accept a match between prototype and implementation in such cases. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or +# disable (NO) the todo list. This list is created by putting \todo +# commands in the documentation. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or +# disable (NO) the test list. This list is created by putting \test +# commands in the documentation. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or +# disable (NO) the bug list. This list is created by putting \bug +# commands in the documentation. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or +# disable (NO) the deprecated list. This list is created by putting +# \deprecated commands in the documentation. + +GENERATE_DEPRECATEDLIST = YES + +# The ENABLED_SECTIONS tag can be used to enable conditional +# documentation sections, marked by \if section-label ... \endif +# and \cond section-label ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines +# the initial value of a variable or macro consists of for it to appear in +# the documentation. If the initializer consists of more lines than specified +# here it will be hidden. Use a value of 0 to hide initializers completely. +# The appearance of the initializer of individual variables and macros in the +# documentation can be controlled using \showinitializer or \hideinitializer +# command in the documentation regardless of this setting. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated +# at the bottom of the documentation of classes and structs. If set to YES the +# list will mention the files that were used to generate the documentation. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. +# This will remove the Files entry from the Quick Index and from the +# Folder Tree View (if specified). The default is YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the +# Namespaces page. +# This will remove the Namespaces entry from the Quick Index +# and from the Folder Tree View (if specified). The default is YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command , where is the value of +# the FILE_VERSION_FILTER tag, and is the name of an input file +# provided by doxygen. Whatever the program writes to standard output +# is used as the file version. See the manual for examples. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. +# You can optionally specify a file name after the option, if omitted +# DoxygenLayout.xml will be used as the name of the layout file. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files +# containing the references data. This must be a list of .bib files. The +# .bib extension is automatically appended if omitted. Using this command +# requires the bibtex tool to be installed. See also +# http://en.wikipedia.org/wiki/BibTeX for more info. For LaTeX the style +# of the bibliography can be controlled using LATEX_BIB_STYLE. To use this +# feature you need bibtex and perl available in the search path. Do not use +# file names with spaces, bibtex cannot handle them. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated +# by doxygen. Possible values are YES and NO. If left blank NO is used. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated by doxygen. Possible values are YES and NO. If left blank +# NO is used. + +WARNINGS = YES + +# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings +# for undocumented members. If EXTRACT_ALL is set to YES then this flag will +# automatically be disabled. + +WARN_IF_UNDOCUMENTED = YES + +# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some +# parameters in a documented function, or documenting parameters that +# don't exist or using markup commands wrongly. + +WARN_IF_DOC_ERROR = YES + +# The WARN_NO_PARAMDOC option can be enabled to get warnings for +# functions that are documented, but have no documentation for their parameters +# or return value. If set to NO (the default) doxygen will only warn about +# wrong or incomplete parameter documentation, but not about the absence of +# documentation. + +WARN_NO_PARAMDOC = YES + +# The WARN_FORMAT tag determines the format of the warning messages that +# doxygen can produce. The string should contain the $file, $line, and $text +# tags, which will be replaced by the file and line number from which the +# warning originated and the warning text. Optionally the format may contain +# $version, which will be replaced by the version of the file (if it could +# be obtained via FILE_VERSION_FILTER) + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning +# and error messages should be written. If left blank the output is written +# to stderr. + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag can be used to specify the files and/or directories that contain +# documented source files. You may enter file names like "myfile.cpp" or +# directories like "/usr/src/myproject". Separate the files or directories +# with spaces. + +INPUT = "@CMAKE_SOURCE_DIR@/src" + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is +# also the default input encoding. Doxygen uses libiconv (or the iconv built +# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for +# the list of possible encodings. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank the following patterns are tested: +# *.c *.cc *.cxx *.cpp *.c++ *.d *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh +# *.hxx *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.dox *.py +# *.f90 *.f *.for *.vhd *.vhdl + +FILE_PATTERNS = + +# The RECURSIVE tag can be used to turn specify whether or not subdirectories +# should be searched for input files as well. Possible values are YES and NO. +# If left blank NO is used. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. Note that the wildcards are matched +# against the file with absolute path, so to exclude all test directories +# for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or +# directories that contain example code fragments that are included (see +# the \include command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank all files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude +# commands irrespective of the value of the RECURSIVE tag. +# Possible values are YES and NO. If left blank NO is used. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or +# directories that contain image that are included in the documentation (see +# the \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command , where +# is the value of the INPUT_FILTER tag, and is the name of an +# input file. Doxygen will then use the output that the filter program writes +# to standard output. +# If FILTER_PATTERNS is specified, this tag will be +# ignored. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. +# Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. +# The filters are a list of the form: +# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further +# info on how filters are used. If FILTER_PATTERNS is empty or if +# non of the patterns match the file name, INPUT_FILTER is applied. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will be used to filter the input files when producing source +# files to browse (i.e. when SOURCE_BROWSER is set to YES). + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) +# and it is also possible to disable source filtering for a specific pattern +# using *.ext= (so without naming a filter). This option only has effect when +# FILTER_SOURCE_FILES is enabled. + +FILTER_SOURCE_PATTERNS = + +#--------------------------------------------------------------------------- +# configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will +# be generated. Documented entities will be cross-referenced with these sources. +# Note: To get rid of all source code in the generated output, make sure also +# VERBATIM_HEADERS is set to NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body +# of functions and classes directly in the documentation. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct +# doxygen to hide any special comment blocks from generated source code +# fragments. Normal C, C++ and Fortran comments will always remain visible. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES +# then for each documented function all documented +# functions referencing it will be listed. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES +# then for each documented function all documented entities +# called/used by that function will be listed. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES (the default) +# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from +# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will +# link to the source code. +# Otherwise they will link to the documentation. + +REFERENCES_LINK_SOURCE = YES + +# If the USE_HTAGS tag is set to YES then the references to source code +# will point to the HTML generated by the htags(1) tool instead of doxygen +# built-in source browser. The htags tool is part of GNU's global source +# tagging system (see http://www.gnu.org/software/global/global.html). You +# will need version 4.8.6 or higher. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen +# will generate a verbatim copy of the header file for each class for +# which an include is specified. Set to NO to disable this. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index +# of all compounds will be generated. Enable this if the project +# contains a lot of classes, structs, unions or interfaces. + +ALPHABETICAL_INDEX = YES + +# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then +# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns +# in which this list will be split (can be a number in the range [1..20]) + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all +# classes will be put under the same header in the alphabetical index. +# The IGNORE_PREFIX tag can be used to specify one or more prefixes that +# should be ignored while generating the index headers. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES (the default) Doxygen will +# generate HTML output. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `html' will be used as the default path. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for +# each generated HTML page (for example: .htm,.php,.asp). If it is left blank +# doxygen will generate files with .html extension. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a personal HTML header for +# each generated HTML page. If it is left blank doxygen will generate a +# standard header. Note that when using a custom header you are responsible +# for the proper inclusion of any scripts and style sheets that doxygen +# needs, which is dependent on the configuration options used. +# It is advised to generate a default header using "doxygen -w html +# header.html footer.html stylesheet.css YourConfigFile" and then modify +# that header. Note that the header is subject to change so you typically +# have to redo this when upgrading to a newer version of doxygen or when +# changing the value of configuration settings such as GENERATE_TREEVIEW! + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a personal HTML footer for +# each generated HTML page. If it is left blank doxygen will generate a +# standard footer. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading +# style sheet that is used by each HTML page. It can be used to +# fine-tune the look of the HTML output. If left blank doxygen will +# generate a default style sheet. Note that it is recommended to use +# HTML_EXTRA_STYLESHEET instead of this one, as it is more robust and this +# tag will in the future become obsolete. + +HTML_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath$ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that +# the files will be copied as-is; there are no commands or markers available. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. +# Doxygen will adjust the colors in the style sheet and background images +# according to this color. Hue is specified as an angle on a colorwheel, +# see http://en.wikipedia.org/wiki/Hue for more information. +# For instance the value 0 represents red, 60 is yellow, 120 is green, +# 180 is cyan, 240 is blue, 300 purple, and 360 is red again. +# The allowed range is 0 to 359. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of +# the colors in the HTML output. For a value of 0 the output will use +# grayscales only. A value of 255 will produce the most vivid colors. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to +# the luminance component of the colors in the HTML output. Values below +# 100 gradually make the output lighter, whereas values above 100 make +# the output darker. The value divided by 100 is the actual gamma applied, +# so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2, +# and 100 does not change the gamma. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting +# this to NO can help when comparing the output of multiple runs. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of +# entries shown in the various tree structured indices initially; the user +# can expand and collapse entries dynamically later on. Doxygen will expand +# the tree to such a level that at most the specified number of entries are +# visible (unless a fully collapsed tree already exceeds this amount). +# So setting the number of entries 1 will produce a full collapsed tree by +# default. 0 is a special value representing an infinite number of entries +# and will result in a full expanded tree by default. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files +# will be generated that can be used as input for Apple's Xcode 3 +# integrated development environment, introduced with OSX 10.5 (Leopard). +# To create a documentation set, doxygen will generate a Makefile in the +# HTML output directory. Running make will produce the docset in that +# directory and running "make install" will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find +# it at startup. +# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. + +GENERATE_DOCSET = NO + +# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the +# feed. A documentation feed provides an umbrella under which multiple +# documentation sets from a single provider (such as a company or product suite) +# can be grouped. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that +# should uniquely identify the documentation set bundle. This should be a +# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen +# will append .docset to the name. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely +# identify the documentation publisher. This should be a reverse domain-name +# style string, e.g. com.mycompany.MyDocSet.documentation. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES, additional index files +# will be generated that can be used as input for tools like the +# Microsoft HTML help workshop to generate a compiled HTML help file (.chm) +# of the generated HTML documentation. + +GENERATE_HTMLHELP = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can +# be used to specify the file name of the resulting .chm file. You +# can add a path in front of the file if the result should not be +# written to the html output directory. + +CHM_FILE = + +# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can +# be used to specify the location (absolute path including file name) of +# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run +# the HTML help compiler on the generated index.hhp. + +HHC_LOCATION = + +# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag +# controls if a separate .chi index file is generated (YES) or that +# it should be included in the master .chm file (NO). + +GENERATE_CHI = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING +# is used to encode HtmlHelp index (hhk), content (hhc) and project file +# content. + +CHM_INDEX_ENCODING = + +# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag +# controls whether a binary table of contents is generated (YES) or a +# normal table of contents (NO) in the .chm file. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members +# to the contents of the HTML help documentation and to the tree view. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated +# that can be used as input for Qt's qhelpgenerator to generate a +# Qt Compressed Help (.qch) of the generated HTML documentation. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can +# be used to specify the file name of the resulting .qch file. +# The path specified is relative to the HTML output folder. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#namespace + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#virtual-folders + +QHP_VIRTUAL_FOLDER = @APIDOC_INSTALL_PATH@ + +# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to +# add. For more information please see +# http://doc.trolltech.com/qthelpproject.html#custom-filters + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see +# +# Qt Help Project / Custom Filters. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's +# filter section matches. +# +# Qt Help Project / Filter Attributes. + +QHP_SECT_FILTER_ATTRS = + +# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can +# be used to specify the location of Qt's qhelpgenerator. +# If non-empty doxygen will try to run qhelpgenerator on the generated +# .qhp file. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files +# will be generated, which together with the HTML files, form an Eclipse help +# plugin. To install this plugin and make it available under the help contents +# menu in Eclipse, the contents of the directory containing the HTML and XML +# files needs to be copied into the plugins directory of eclipse. The name of +# the directory within the plugins directory should be the same as +# the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before +# the help appears. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have +# this name. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# The DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) +# at top of each HTML page. The value NO (the default) enables the index and +# the value YES disables it. Since the tabs have the same information as the +# navigation tree you can set this option to NO if you already set +# GENERATE_TREEVIEW to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. +# If the tag value is set to YES, a side panel will be generated +# containing a tree-like index structure (just like the one that +# is generated for HTML Help). For this to work a browser that supports +# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). +# Windows users are probably better off using the HTML help feature. +# Since the tree basically has the same information as the tab index you +# could consider to set DISABLE_INDEX to NO when enabling this option. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values +# (range [0,1..20]) that doxygen will group on one line in the generated HTML +# documentation. Note that a value of 0 will completely suppress the enum +# values from appearing in the overview section. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be +# used to set the initial width (in pixels) of the frame in which the tree +# is shown. + +TREEVIEW_WIDTH = 250 + +# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open +# links to external symbols imported via tag files in a separate window. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of Latex formulas included +# as images in the HTML documentation. The default is 10. Note that +# when you change the font size after a successful doxygen run you need +# to manually remove any form_*.png images from the HTML output directory +# to force them to be regenerated. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are +# not supported properly for IE 6.0, but are supported on all modern browsers. +# Note that when changing this option you need to delete any form_*.png files +# in the HTML output before the changes have effect. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax +# (see http://www.mathjax.org) which uses client side Javascript for the +# rendering instead of using prerendered bitmaps. Use this if you do not +# have LaTeX installed or if you want to formulas look prettier in the HTML +# output. When enabled you may also need to install MathJax separately and +# configure the path to it using the MATHJAX_RELPATH option. + +USE_MATHJAX = NO + +# When MathJax is enabled you need to specify the location relative to the +# HTML output directory using the MATHJAX_RELPATH option. The destination +# directory should contain the MathJax.js script. For instance, if the mathjax +# directory is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to +# the MathJax Content Delivery Network so you can quickly see the result without +# installing MathJax. +# However, it is strongly recommended to install a local +# copy of MathJax from http://www.mathjax.org before deployment. + +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest + +# The MATHJAX_EXTENSIONS tag can be used to specify one or MathJax extension +# names that should be enabled during MathJax rendering. + +MATHJAX_EXTENSIONS = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box +# for the HTML output. The underlying search engine uses javascript +# and DHTML and should work on any modern browser. Note that when using +# HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets +# (GENERATE_DOCSET) there is already a search function so this one should +# typically be disabled. For large projects the javascript based search engine +# can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution. + +SEARCHENGINE = YES + +# When the SERVER_BASED_SEARCH tag is enabled the search engine will be +# implemented using a web server instead of a web client using Javascript. +# There are two flavours of web server based search depending on the +# EXTERNAL_SEARCH setting. When disabled, doxygen will generate a PHP script for +# searching and an index file used by the script. When EXTERNAL_SEARCH is +# enabled the indexing and searching needs to be provided by external tools. +# See the manual for details. + +SERVER_BASED_SEARCH = NO + +#--------------------------------------------------------------------------- +# configuration options related to the LaTeX output +#--------------------------------------------------------------------------- + +# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will +# generate Latex output. + +GENERATE_LATEX = NO + +# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `latex' will be used as the default path. + +LATEX_OUTPUT = latex + +# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be +# invoked. If left blank `latex' will be used as the default command name. +# Note that when enabling USE_PDFLATEX this option is only used for +# generating bitmaps for formulas in the HTML output, but not in the +# Makefile that is written to the output directory. + +LATEX_CMD_NAME = latex + +# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to +# generate index for LaTeX. If left blank `makeindex' will be used as the +# default command name. + +MAKEINDEX_CMD_NAME = makeindex + +# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact +# LaTeX documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_LATEX = NO + +# The PAPER_TYPE tag can be used to set the paper type that is used +# by the printer. Possible values are: a4, letter, legal and +# executive. If left blank a4wide will be used. + +PAPER_TYPE = a4 + +# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX +# packages that should be included in the LaTeX output. + +EXTRA_PACKAGES = + +# The LATEX_HEADER tag can be used to specify a personal LaTeX header for +# the generated latex document. The header should contain everything until +# the first chapter. If it is left blank doxygen will generate a +# standard header. Notice: only use this tag if you know what you are doing! + +LATEX_HEADER = + +# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for +# the generated latex document. The footer should contain everything after +# the last chapter. If it is left blank doxygen will generate a +# standard footer. Notice: only use this tag if you know what you are doing! + +LATEX_FOOTER = + +# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated +# is prepared for conversion to pdf (using ps2pdf). The pdf file will +# contain links (just like the HTML output) instead of page references +# This makes the output suitable for online browsing using a pdf viewer. + +PDF_HYPERLINKS = YES + +# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of +# plain latex in the generated Makefile. Set this option to YES to get a +# higher quality PDF documentation. + +USE_PDFLATEX = YES + +# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. +# command to the generated LaTeX files. This will instruct LaTeX to keep +# running if errors occur, instead of asking the user for help. +# This option is also used when generating formulas in HTML. + +LATEX_BATCHMODE = NO + +# If LATEX_HIDE_INDICES is set to YES then doxygen will not +# include the index chapters (such as File Index, Compound Index, etc.) +# in the output. + +LATEX_HIDE_INDICES = NO + +# If LATEX_SOURCE_CODE is set to YES then doxygen will include +# source code with syntax highlighting in the LaTeX output. +# Note that which sources are shown also depends on other settings +# such as SOURCE_BROWSER. + +LATEX_SOURCE_CODE = NO + +# The LATEX_BIB_STYLE tag can be used to specify the style to use for the +# bibliography, e.g. plainnat, or ieeetr. The default style is "plain". See +# http://en.wikipedia.org/wiki/BibTeX for more info. + +LATEX_BIB_STYLE = plain + +#--------------------------------------------------------------------------- +# configuration options related to the RTF output +#--------------------------------------------------------------------------- + +# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output +# The RTF output is optimized for Word 97 and may not look very pretty with +# other RTF readers or editors. + +GENERATE_RTF = NO + +# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `rtf' will be used as the default path. + +RTF_OUTPUT = rtf + +# If the COMPACT_RTF tag is set to YES Doxygen generates more compact +# RTF documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_RTF = NO + +# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated +# will contain hyperlink fields. The RTF file will +# contain links (just like the HTML output) instead of page references. +# This makes the output suitable for online browsing using WORD or other +# programs which support those fields. +# Note: wordpad (write) and others do not support links. + +RTF_HYPERLINKS = NO + +# Load style sheet definitions from file. Syntax is similar to doxygen's +# config file, i.e. a series of assignments. You only have to provide +# replacements, missing definitions are set to their default value. + +RTF_STYLESHEET_FILE = + +# Set optional variables used in the generation of an rtf document. +# Syntax is similar to doxygen's config file. + +RTF_EXTENSIONS_FILE = + +#--------------------------------------------------------------------------- +# configuration options related to the man page output +#--------------------------------------------------------------------------- + +# If the GENERATE_MAN tag is set to YES (the default) Doxygen will +# generate man pages + +GENERATE_MAN = NO + +# The MAN_OUTPUT tag is used to specify where the man pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `man' will be used as the default path. + +MAN_OUTPUT = man + +# The MAN_EXTENSION tag determines the extension that is added to +# the generated man pages (default is the subroutine's section .3) + +MAN_EXTENSION = .3 + +# If the MAN_LINKS tag is set to YES and Doxygen generates man output, +# then it will generate one additional man file for each entity +# documented in the real man page(s). These additional files +# only source the real man page, but without them the man command +# would be unable to find the correct page. The default is NO. + +MAN_LINKS = NO + +#--------------------------------------------------------------------------- +# configuration options related to the XML output +#--------------------------------------------------------------------------- + +# If the GENERATE_XML tag is set to YES Doxygen will +# generate an XML file that captures the structure of +# the code including all documentation. + +GENERATE_XML = NO + +# The XML_OUTPUT tag is used to specify where the XML pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `xml' will be used as the default path. + +XML_OUTPUT = xml + +# If the XML_PROGRAMLISTING tag is set to YES Doxygen will +# dump the program listings (including syntax highlighting +# and cross-referencing information) to the XML output. Note that +# enabling this will significantly increase the size of the XML output. + +XML_PROGRAMLISTING = YES + +#--------------------------------------------------------------------------- +# configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will +# generate an AutoGen Definitions (see autogen.sf.net) file +# that captures the structure of the code including all +# documentation. Note that this feature is still experimental +# and incomplete at the moment. + +GENERATE_AUTOGEN_DEF = NO + +#--------------------------------------------------------------------------- +# configuration options related to the Perl module output +#--------------------------------------------------------------------------- + +# If the GENERATE_PERLMOD tag is set to YES Doxygen will +# generate a Perl module file that captures the structure of +# the code including all documentation. Note that this +# feature is still experimental and incomplete at the +# moment. + +GENERATE_PERLMOD = NO + +# If the PERLMOD_LATEX tag is set to YES Doxygen will generate +# the necessary Makefile rules, Perl scripts and LaTeX code to be able +# to generate PDF and DVI output from the Perl module output. + +PERLMOD_LATEX = NO + +# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be +# nicely formatted so it can be parsed by a human reader. +# This is useful +# if you want to understand what is going on. +# On the other hand, if this +# tag is set to NO the size of the Perl module output will be much smaller +# and Perl will parse it just the same. + +PERLMOD_PRETTY = YES + +# The names of the make variables in the generated doxyrules.make file +# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. +# This is useful so different doxyrules.make files included by the same +# Makefile don't overwrite each other's variables. + +PERLMOD_MAKEVAR_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- + +# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will +# evaluate all C-preprocessor directives found in the sources and include +# files. + +ENABLE_PREPROCESSING = YES + +# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro +# names in the source code. If set to NO (the default) only conditional +# compilation will be performed. Macro expansion can be done in a controlled +# way by setting EXPAND_ONLY_PREDEF to YES. + +MACRO_EXPANSION = YES + +# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES +# then the macro expansion is limited to the macros specified with the +# PREDEFINED and EXPAND_AS_DEFINED tags. + +EXPAND_ONLY_PREDEF = NO + +# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files +# pointed to by INCLUDE_PATH will be searched when a #include is found. + +SEARCH_INCLUDES = YES + +# The INCLUDE_PATH tag can be used to specify one or more directories that +# contain include files that are not input files but should be processed by +# the preprocessor. + +INCLUDE_PATH = + +# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard +# patterns (like *.h and *.hpp) to filter out the header-files in the +# directories. If left blank, the patterns specified with FILE_PATTERNS will +# be used. + +INCLUDE_FILE_PATTERNS = + +# The PREDEFINED tag can be used to specify one or more macro names that +# are defined before the preprocessor is started (similar to the -D option of +# gcc). The argument of the tag is a list of macros of the form: name +# or name=definition (no spaces). If the definition and the = are +# omitted =1 is assumed. To prevent a macro definition from being +# undefined via #undef or recursively expanded use the := operator +# instead of the = operator. + +PREDEFINED = \ + __attribute__(x)= \ + + +# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then +# this tag can be used to specify a list of macro names that should be expanded. +# The macro definition that is found in the sources will be used. +# Use the PREDEFINED tag if you want to use a different macro definition that +# overrules the definition found in the source code. + +EXPAND_AS_DEFINED = \ + DMNS_BEGIN DMNS_END \ + BFNS_BEGIN BFNS_END \ + JSNS_BEGIN JSNS_END \ + LBNS_BEGIN LBNS_END + +# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then +# doxygen's preprocessor will remove all references to function-like macros +# that are alone on a line, have an all uppercase name, and do not end with a +# semicolon, because these will confuse the parser if not removed. + +SKIP_FUNCTION_MACROS = YES + +#--------------------------------------------------------------------------- +# Configuration::additions related to external references +#--------------------------------------------------------------------------- + +# The TAGFILES option can be used to specify one or more tagfiles. For each +# tag file the location of the external documentation should be added. The +# format of a tag file without this location is as follows: +# +# TAGFILES = file1 file2 ... +# Adding location for the tag files is done as follows: +# +# TAGFILES = file1=loc1 "file2 = loc2" ... +# where "loc1" and "loc2" can be relative or absolute paths +# or URLs. Note that each tag file must have a unique name (where the name does +# NOT include the path). If a tag file is not located in the directory in which +# doxygen is run, you must also specify the path to the tagfile here. + +TAGFILES = + +# When a file name is specified after GENERATE_TAGFILE, doxygen will create +# a tag file that is based on the input files it reads. + +GENERATE_TAGFILE = @APIDOC_INSTALL_PATH@/fbrat.dtag + +# If the ALLEXTERNALS tag is set to YES all external classes will be listed +# in the class index. If set to NO only the inherited external classes +# will be listed. + +ALLEXTERNALS = NO + +# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed +# in the modules index. If set to NO, only the current project's groups will +# be listed. + +EXTERNAL_GROUPS = YES + +# The PERL_PATH should be the absolute path and name of the perl script +# interpreter (i.e. the result of `which perl'). + +PERL_PATH = /usr/bin/perl + +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- + +# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will +# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base +# or super classes. Setting the tag to NO turns the diagrams off. Note that +# this option also works with HAVE_DOT disabled, but it is recommended to +# install and use dot, since it yields more powerful graphs. + +CLASS_DIAGRAMS = NO + +# You can define message sequence charts within doxygen comments using the \msc +# command. Doxygen will then run the mscgen tool (see +# http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the +# documentation. The MSCGEN_PATH tag allows you to specify the directory where +# the mscgen tool resides. If left empty the tool is assumed to be found in the +# default search path. + +MSCGEN_PATH = + +# If set to YES, the inheritance and collaboration graphs will hide +# inheritance and usage relations if the target is undocumented +# or is not a class. + +HIDE_UNDOC_RELATIONS = NO + +# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# available from the path. This tool is part of Graphviz, a graph visualization +# toolkit from AT&T and Lucent Bell Labs. The other options in this section +# have no effect if this option is set to NO (the default) + +HAVE_DOT = YES + +# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is +# allowed to run in parallel. When set to 0 (the default) doxygen will +# base this on the number of processors available in the system. You can set it +# explicitly to a value larger than 0 to get control over the balance +# between CPU load and processing speed. + +DOT_NUM_THREADS = 0 + +# By default doxygen will use the Helvetica font for all dot files that +# doxygen generates. When you want a differently looking font you can specify +# the font name using DOT_FONTNAME. You need to make sure dot is able to find +# the font, which can be done by putting it in a standard location or by setting +# the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the +# directory containing the font. + +DOT_FONTNAME = Helvetica + +# The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. +# The default size is 10pt. + +DOT_FONTSIZE = 10 + +# By default doxygen will tell dot to use the Helvetica font. +# If you specify a different font using DOT_FONTNAME you can use DOT_FONTPATH to +# set the path where dot can find it. + +DOT_FONTPATH = + +# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect inheritance relations. Setting this tag to YES will force the +# CLASS_DIAGRAMS tag to NO. + +CLASS_GRAPH = YES + +# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect implementation dependencies (inheritance, containment, and +# class references variables) of the class with other documented classes. + +COLLABORATION_GRAPH = YES + +# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for groups, showing the direct groups dependencies + +GROUP_GRAPHS = YES + +# If the UML_LOOK tag is set to YES doxygen will generate inheritance and +# collaboration diagrams in a style similar to the OMG's Unified Modeling +# Language. + +UML_LOOK = NO + +# If the UML_LOOK tag is enabled, the fields and methods are shown inside +# the class node. If there are many fields or methods and many nodes the +# graph may become too big to be useful. The UML_LIMIT_NUM_FIELDS +# threshold limits the number of items for each type to make the size more +# managable. Set this to 0 for no limit. Note that the threshold may be +# exceeded by 50% before the limit is enforced. + +UML_LIMIT_NUM_FIELDS = 10 + +# If set to YES, the inheritance and collaboration graphs will show the +# relations between templates and their instances. + +TEMPLATE_RELATIONS = NO + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT +# tags are set to YES then doxygen will generate a graph for each documented +# file showing the direct and indirect include dependencies of the file with +# other documented files. + +INCLUDE_GRAPH = YES + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and +# HAVE_DOT tags are set to YES then doxygen will generate a graph for each +# documented header file showing the documented files that directly or +# indirectly include this file. + +INCLUDED_BY_GRAPH = YES + +# If the CALL_GRAPH and HAVE_DOT options are set to YES then +# doxygen will generate a call dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable call graphs +# for selected functions only using the \callgraph command. + +CALL_GRAPH = NO + +# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then +# doxygen will generate a caller dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable caller +# graphs for selected functions only using the \callergraph command. + +CALLER_GRAPH = NO + +# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen +# will generate a graphical hierarchy of all classes instead of a textual one. + +GRAPHICAL_HIERARCHY = YES + +# If the DIRECTORY_GRAPH and HAVE_DOT tags are set to YES +# then doxygen will show the dependencies a directory has on other directories +# in a graphical way. The dependency relations are determined by the #include +# relations between the files in the directories. + +DIRECTORY_GRAPH = YES + +# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images +# generated by dot. Possible values are svg, png, jpg, or gif. +# If left blank png will be used. If you choose svg you need to set +# HTML_FILE_EXTENSION to xhtml in order to make the SVG files +# visible in IE 9+ (other browsers do not have this requirement). + +DOT_IMAGE_FORMAT = png + +# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to +# enable generation of interactive SVG images that allow zooming and panning. +# Note that this requires a modern browser other than Internet Explorer. +# Tested and working are Firefox, Chrome, Safari, and Opera. For IE 9+ you +# need to set HTML_FILE_EXTENSION to xhtml in order to make the SVG files +# visible. Older versions of IE do not have SVG support. + +INTERACTIVE_SVG = NO + +# The tag DOT_PATH can be used to specify the path where the dot tool can be +# found. If left blank, it is assumed the dot tool can be found in the path. + +DOT_PATH = + +# The DOTFILE_DIRS tag can be used to specify one or more directories that +# contain dot files that are included in the documentation (see the +# \dotfile command). + +DOTFILE_DIRS = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the +# \mscfile command). + +MSCFILE_DIRS = + +# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of +# nodes that will be shown in the graph. If the number of nodes in a graph +# becomes larger than this value, doxygen will truncate the graph, which is +# visualized by representing a node as a red box. Note that doxygen if the +# number of direct children of the root node in a graph is already larger than +# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note +# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. + +DOT_GRAPH_MAX_NODES = 50 + +# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the +# graphs generated by dot. A depth value of 3 means that only nodes reachable +# from the root by following a path via at most 3 edges will be shown. Nodes +# that lay further from the root node will be omitted. Note that setting this +# option to 1 or 2 may greatly reduce the computation time needed for large +# code bases. Also note that the size of a graph can be further restricted by +# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. + +MAX_DOT_GRAPH_DEPTH = 0 + +# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent +# background. This is disabled by default, because dot on Windows does not +# seem to support this out of the box. Warning: Depending on the platform used, +# enabling this option may lead to badly anti-aliased labels on the edges of +# a graph (i.e. they become hard to read). + +DOT_TRANSPARENT = NO + +# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output +# files in one run (i.e. multiple -o and -T options on the command line). This +# makes dot run faster, but since only newer versions of dot (>1.8.10) +# support this, this feature is disabled by default. + +DOT_MULTI_TARGETS = NO + +# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will +# generate a legend page explaining the meaning of the various boxes and +# arrows in the dot generated graphs. + +GENERATE_LEGEND = YES + +# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will +# remove the intermediate dot files that are used to generate +# the various graphs. + +DOT_CLEANUP = YES diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..f2a67ab --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,13 @@ +Project License Copyright 2019-2024, Meta; 2021-2024, Broadcom and Cisco; 2021-2024, TIP and its Contributors; all rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +4. To the extent any other open source software is included or referenced herein, such open source software is licensed under their applicable license terms and conditions and/or copyright notices found in those respective files. + +THIS SOFTWARE IS PROVIDED BY TIP AND ITS CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/NewUserRegistration.md b/NewUserRegistration.md new file mode 100644 index 0000000..59c9586 --- /dev/null +++ b/NewUserRegistration.md @@ -0,0 +1,29 @@ +Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +refers solely to the Broadcom Inc. corporate affiliate that owns +the software below. +This work is licensed under the OpenAFC Project License, a copy of which is included with this software program. + +# New User Creation +New users can be created via the CLI or can be registered via the Web GUI + +## CLI to create users +``` +rat-manage-api user create --role Admin --role AP --role Analysis myusername "Enter Your Password Here" +``` +## CLI to assign roles. +Roles for existing users can be modified using CLI +Note that for users added via "user create" command, the email and username are the same +``` +rat-manage-api user update --role Admin --role AP --role Analysis --email "user@mycompany.com" +``` + +# Web User Registration +New user can request an account on the web page. For non-OIDC method, use the register button and follow the instructions sent via email. +For OIDC signin method, use the About link to fill out the request form. If a request for access is granted, an email reply informs the user on next steps to get on board. Custom configuration is required on the server to handle new user requests. See Customization.md for details. +Regardless of method newly granted users will have default Trial roles upon first time the person logs in. The Admin or Super user can use the web GUI to change a user roles + +# New User access +New Users who are granted access automatically have Trial roles and have access to the Virtual AP tab to submit requests. +Test user can chose from **TEST_US** **TEST_CA** or **TEST_BR** in the drop down in the Virtual AP tab. The corresponding config for these test regsions should first be set in AFC Config tab by the admin. New Users who are granted access automatically have Trial roles and have access to the Virtual AP tab to submit requests. + + diff --git a/OIDC_Login.md b/OIDC_Login.md new file mode 100644 index 0000000..3529d60 --- /dev/null +++ b/OIDC_Login.md @@ -0,0 +1,70 @@ +Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +refers solely to the Broadcom Inc. corporate affiliate that owns +the software below. +This work is licensed under the OpenAFC Project License, a copy of which is included with this software program. + +# **Introduction** +You can configure AFC server to use one of two login mechanisms. The OIDC login provides Single Sign On (SSO) where the handling of identity verification is done at the separate identity provider server. The non OIDC login implements local authentication using the AFC server's local database. + +# **Non OIDC Login** +The legacy login mechanism performs authentication locally on the AFC server. It is configured by default. Steps to configure are described in [README.md](/README.md) + +# **OIDC Login** +OIDC relies on an identity provider outside of the AFC server to verify the users identity. Your organization could already have its own identity server for its employees. When a user logs in the AFC application, AFC server forwards a request to the identity provider to authenticate. For your organization's employees, the authentication is completed by your identity provider. For federated users, the identity provider will further forward the user to his/her respective identity server for verification. + +Background on OIDC can be found here: https://openid.net/connect/ + +## OIDC Configuration +### Use Json file +The preferred method is to use an oidc config file in json format, e.g. file oidc.json : +``` +{ + "OIDC_LOGIN":"True", + "OIDC_CLIENT_ID":"1234", + "OIDC_CLIENT_SECRET":"my_secret_string", + "OIDC_DISCOVERY_URL":"https://accounts.mycompany.com" +} +``` + +And in docker-compose.yaml: +``` +rat_server: + volumes: + - /hostpath:/localpath + + environment: + - OIDC_ARG=/localpath/oidc.json +``` +In the above, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET are the information which your server needs to present to the identity server to verify a user. OIDC_DISCOVERY_URL is the url which returns various urls needed for verification. One example of such discovery url is https://accounts.google.com/.well-known/openid-configuration + +The path to the config file can be put in a mounted file, passed to the container via environment variable OIDC_ARG. This can be done via docker-compose file, or secrets + +Note that the path must be accessible to the httpd which runs as fbrat username and fbrat group +``` +chown -R 1003:1003 /localpath +``` +### Use Environment Variables +The alternative method is to use environment variables to pass each parameter, e.g. + +For example, docker-compose.yaml: +``` +OIDC_LOGIN = True +OIDC_CLIENT_ID = '1234' +OIDC_CLIENT_SECRET = 'my_secret_string' +OIDC_DISCOVERY_URL = 'https://accounts.mycompany.com' +``` + + +More information on creating your own google cloud account can be found here: +https://cloud.google.com/apigee/docs/hybrid/v1.3/precog-gcpaccount + + +## **Migrate From non OIDC to OIDC login method** +With OIDC method, the user acounts are stored in the identity server which maintains accounts of your employees. Accounts created in non OIDC database are not recognized by OIDC identity server. Therefore, after converting to OIDC, you will not be able to login via the WEB using test user accounts created via CLI, although those can still be used by test scripts, and the roles of those accounts continue will be maintained. + +To facilitate switching real (non test) accounts, when a user logs in for the first time via OIDC, and the email address from the OIDC identity server matches an existing user in the database, that existing account is converted while retaining all roles. Thus, when logging in via WEB GUI, the user has the same access as before the switch. + +## **Switching from OIDC to non OIDC login method** +Accounts that are maintained exclusively by OIDC identity provider are not maintained locally. So, after the switch to non OIDC, they cannot be logged in via the WEB GUI unless the admin modify the password. Accounts created via CLI can be logged in using the same password used in the CLI to create them. + +In non OIDC, any account's password can be modified by the admin. This is true even for accounts that were maintained by OIDC. However, note that the newly modified password is not recognised by OIDC, and if switched back to OIDC mode again, the password used by the OIDC identity server must be used to login. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f87a3d --- /dev/null +++ b/README.md @@ -0,0 +1,960 @@ +This work is licensed under the OpenAFC Project License, a copy of which is included in the LICENSE.txt file with this software program. + +
+
+ +## Table of Contents +- [**Introduction**](#introduction) +- [**Contributing**](#contributing) + - [How to contribute](#how-to-contribute) + - [Pull request best practices](#pull-request-best-practices) + - [Step 1: File an issue](#step-1-file-an-issue) + - [Step 2: Clone OpenAFC GitHub repository](#step-2-clone-openafc-github-repository) + - [Step 3: Create a temporary branch](#step-3-create-a-temporary-branch) + - [Step 4: Commit your changes](#step-4-commit-your-changes) + - [Step 5: Rebase](#step-5-rebase) + - [Step 6: Run the tests](#step-6-run-the-tests) + - [Step 7: Push your branch to GitHub](#step-7-push-your-branch-to-github) + - [Step 8: Send the pull request](#step-8-send-the-pull-request) + - [Change Description](#change-description) +- [**How to Build**](#how-to-build) +- [AFC Engine build in docker setup](#afc-engine-build-in-docker-setup) + - [Installing docker engine](#installing-docker-engine) + - [Building the Docker image](#building-the-docker-image) + - [Prerequisites:](#prerequisites) + - [Building Docker image from Dockerfile (can be omitted once we have Docker registry)](#building-docker-image-from-dockerfile-can-be-omitted-once-we-have-docker-registry) + - [Pulling the Docker image from Docker registry](#pulling-the-docker-image-from-docker-registry) + - [Building OpenAFC engine](#building-openafc-engine) +- [**OpenAFC Engine Server usage in Docker Environment**](#openafc-engine-server-usage-in-docker-environment) +- [AFC Engine build in docker](#afc-engine-build-in-docker) + - [Building Docker Container OpenAFC engine server](#building-docker-container-openafc-engine-server) + - [Using scripts from the code base](#using-scripts-from-the-code-base) + - [To 'manually' build containers one by one:](#to-manually-build-containers-one-by-one) + - [celery worker prereq containers](#celery-worker-prereq-containers) + - [Prereqs](#prereqs) + - [docker-compose](#docker-compose) + - [**Environment variables**](#environment-variables) + - [RabbitMQ settings](#rabbitmq-settings) + - [PostgreSQL structure](#postgresql-structure) + - [Upgrade PostgreSQL](#upgrade-postgresql) + - [Initial Super Administrator account](#initial-super-administrator-account) + - [Note for an existing user database](#note-for-an-existing-user-database) + - [Managing user account](#managing-user-account) + - [User roles](#user-roles) + - [MTLS](#mtls) + - [ULS database update automation](#uls-database-update-automation) + +# **Introduction** + +This document describes the procedure for submitting the source code changes to the openAFC github project. Procedure described in this document requires access to the openAFC project and knowledge of the GIT usage. Please contact TBD@TBD.com in case you need access to the openAFC project. + +Github.com can be referred for [details of alternate procedures for creating the pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests), developers can use any of these methods but need to include change description as part of pull requests description. + +OpenAFC conforms to all the requirements from FCC per [6GHz Report & Order](https://docs.fcc.gov/public/attachments/DOC-363490A1.pdf) and FCC 47 CFR Part 15.407 for unlicensed standard power devices in the 6 GHz band. + +In addition, OpenAFC fully conforms to WinnForum’s Functional Requirements for the U.S. 6 GHz Band under the Control of an AFC System in WINNF-TS-1014-V1.4.0 ([https://6ghz.wirelessinnovation.org/baseline-standards](https://6ghz.wirelessinnovation.org/baseline-standards)). This includes some of the implementation details – for example correction of FS parameters in the ULS database, FS antenna pattern, FS noise power and feederloss to use, calculation of near-field adjustment factor, calculation of interference to FS links with passive sites and diversity receivers, path loss models and their parameters, etc. +Finally, OpenAFC fully conforms to the implementation details specified in [WFA SUT Test Plan v1.5](https://www.wi-fi.org/file/afc-specification-and-test-plans). + +OpenAFC software deployment consists of multiple containers, and it can be deployed on a standalone system for test and development purposes via the provided docker-compose based solution. Instructions on how to build the containers and a sample docker-compose.yaml can be found in the [OpenAFC Engine Server usage in Docker Environment](#afc-engine-build-in-docker). + +OpenAFC software can also be deployed for production using the Kubernetes framework. Please refer to the readme-kubernetes.md for the instructions. + +The sample docker-compose.yaml assumes that the required databases (e.g. terrain, landcover, winnforum databases, etc.) have been obtained and placed in an accessible folder according to the information in [database_readme.md](https://github.com/Telecominfraproject/open-afc/blob/main/database_readme.md +) on Github. + +Many of the components have additional README files inside folders that describe the additional configuration for each component. Default values are provided either inside the component or in the sample files that will work to stand up the system. + +Note that this sample does not provide working SSL certificates for authentication to the server. + +

+ +# **Contributing** +All contributions are welcome to this project. + +## How to contribute + +* **File an issue** - if you found a bug, want to request an enhancement, or want to implement something (bug fix or feature). +* **Send a pull request** - if you want to contribute code. Please be sure to file an issue first. + +## Pull request best practices + +We want to accept your pull requests. Please follow these steps: + +### Step 1: File an issue + +Before writing any code, please file an Issue ticket using this Github's repository's 'Issues' tab, stating the problem you want to solve or the feature you want to implement along with a high-level description of the resolution. This allows us to give you feedback before you spend any time writing code. There may be a known limitation that can't be addressed, or a bug that has already been fixed in a different way. The issue ticket allows us to communicate and figure out if it's worth your time to write a bunch of code for the project. + +### Step 2: Clone OpenAFC GitHub repository + +OpenAFC source repository can be cloned using the below command. +``` +git clone git@github.com:Telecominfraproject/open-afc.git +``` +This will create your own copy of our repository. +[about remote repositories](https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories) + +### Step 3: Create a temporary branch + +Create a temporary branch for making your changes. +Keep a separate branch for each issue/feature you want to address . +``` +git checkout -b -branch_name +``` + +Highly desirable to use branch name from Issue ticket title, or use meaningful branch name reflecting the actual changes +``` +eg. git checkout -b 146-update-readme-md-to-reflect-issueticket-and-branch-creation-procedure +``` +### Step 4: Make your changes +Review the [Readme in the tools/editing directory](tools/editing/README.md) to review the code style tools that are required. Pull requests not meeting the code style requirements will fail to build in the pull request. + +### Step 5: Commit your changes +As you develop code, commit your changes into your local feature branch. +Please make sure to include the issue number you're addressing in your commit message. +This helps us out by allowing us to track which issue/feature your commit relates to. +Below command will commit your changes to the local branch. +Note to use Issue ticket number at the beginning of commit message. +``` +git commit -a -m " desctiption of the change ..." +``` +### Step 6: Rebase + +Before sending a pull request, rebase against upstream, such as: + +``` +git fetch origin +git rebase origin +``` +This will add your changes on top of what's already in upstream, minimizing merge issues. + +### Step 7: Run the tests + +Run sufficient targetted tests on the change made to validate that the change works as expected. Please document and submit the test requests/results in the Issue ticket. + +This includes running the regression test framework available under the 'tests > regression' directory to verify your changes have not broken other portions of the system. +Make sure that all regression tests are passing before submitting a pull request. + +### Step 8: Push your branch to GitHub + +Push code to your remote feature branch. +Below command will push your local branch along with the changes to OpenAFC GitHub. +``` +git push -u origin -branch_name +``` + > NOTE: The push can include several commits (not only one), but these commits should be related to the same logical change/issue fix/new feature originally described in the [Step 1](#step-1-file-an-issue). + +### Step 9: Send the pull request + +Send the pull request from your feature branch to us. + +#### Change Description + + +When submitting a pull request, please use the following template to submit the change description, risks and validations done after making the changes +(not a book, but an info required to understand the change/scenario/risks/test coverage) + +- Issue ticket number (from [Step 1](#step-1-file-an-issue)). A brief description of issue(s) being fixed and likelihood/frequency/severity of the issue, or description of new feature if it is a new feature. +- Reproduction procedure: Details of how the issue could be reproduced / procedure to reproduce the issue. +Description of Change: A detailed description of what the change is and assumptions / decisions made +- Risks: Low, Medium or High and reasoning for the same. +- Fix validation procedure: + - Description of validations done after the fix. + - Required regression tests: Describe what additional tests should be done to ensure no regressions in other functionalities. + - Sanity test results as described in the [Step 7](#step-7-run-the-tests) + +> NOTE: Keep in mind that we like to see one issue addressed per pull request, as this helps keep our git history clean and we can more easily track down issues. + + +

+ +# **How to Build** +# AFC Engine build in docker and compose setup + +## Installing docker engine +Docker engine instructions specific to your OS are available on the [Docker official website](https://docs.docker.com/engine/install/) + +## Building the Docker images + +### Prerequisites: + +Currently, all the prerequisites to build the containers for the system (except docker installation) are situated in this repository. All you need is to clone OpenAFC locally to your working directory and start all following commands from there. + +In order to run the system, you will need to construct a data volume and make it available to the containers. See [database_readme.md](database_readme.md) for details on what data is required. + +### Building Docker image from Dockerfiles + +There is a script that builds all container used by the AFC service. +This script is used by automatic test infrastructure. Please check [tests/regression](/tests/regression/) dir. + +This script uses two environment variables PRIV_REPO and PUB_REPO to determine what repository to push images to. These values default to those used by the regression test infrastructure, but you should define them to refer to your repository. Note that the script + +### Using scripts from the code base + +To rebuild and tag all containers in your local docker repository, use this script: +``` +cd open-afc +tests/regression/build_imgs.sh `pwd` my_tag 0 +``` +after the build, check all new containers: +``` +docker images | grep my_tag +``` +these containers are used by [tests/regression/run_srvr.sh](/tests/regression/run_srvr.sh) + +### To 'manually' build containers one by one: +``` +cd open-afc + +docker build . -t rat_server -f rat_server/Dockerfile + +docker build . -t uls_service -f uls/Dockerfile-uls_service + +docker build . -t msghnd -f msghnd/Dockerfile + +docker build . -t ratdb -f ratdb/Dockerfile + +docler build . -t objst -f objstorage/Dockerfile + +docker build . -t rmq -f rabbitmq/Dockerfile + +docker build . -t dispatcher -f dispatcher/Dockerfile + +docker build . -t cert_db -f cert_db/Dockerfile + +docker build . -t rcache -f rcache/Dockerfile + +cd als && docker build . -t als_siphon -f Dockerfile.siphon; cd ../ +cd als && docker build . -t als_kafka -f Dockerfile.kafka; cd ../ +cd bulk_postgres && docker build . -t bulk_postgres -f Dockerfile; cd ../ +``` + +### celery worker prereq containers +``` +docker build . -t worker-preinst -f worker/Dockerfile.preinstall + +docker build . -t worker-build -f worker/Dockerfile.build +``` +to build the worker using local preq containers: +``` +docker build . -t worker -f worker/Dockerfile --build-arg PRINST_NAME=worker-preinst --build-arg PRINST_TAG=local --build-arg BLD_NAME=worker-build --build-arg BLD_TAG=local +``` +### Prometheus Monitoring images +If you wish to use [Prometheus](https://prometheus.io/) to montitor your system, you can build these images +``` +cd prometheus && docker build . -t Dockerfile-prometheus -t prometheus-image ; cd ../ +cd /prometheus && docker build . Dockerfile-cadvisor -t cadvisor-image ; cd ../ +cd /prometheus && docker build . Dockerfile-nginxexporter -t nginxexporter-image ; cd ../ +cd /prometheus && docker build . Dockerfile-grafana -t grafana-image ; cd ../ +``` + +Once built, docker images are usable as usual docker image. + +## Building OpenAFC engine + +If you wish to build or run the engine component outside of the entire OpenAFC system, you can build the docker images that are needed for only that component. + +**NB:** "-v" option in docker maps the folder of the real machine into the insides of the docker container. + +"-v /tmp/work/open-afc:/wd/afc" means that contents of "/tmp/work/open-afc" folder will be available inside of container in /wd/afc/ + + +goto the project dir +``` +cd open-afc +``` +If you have not already, build the worker build image +``` +docker build . -t worker-build -f worker/Dockerfile.build +``` + +run shell of alpine docker-for-build shell +``` +docker run --rm -it --user `id -u`:`id -g` --group-add `id -G | sed "s/ / --group-add /g"` -v `pwd`:/wd/afc worker-build:latest ash +``` + +inside the container's shell, execute: +``` +mkdir -p -m 777 /wd/afc/build && BUILDREV=offlinebuild && cd /wd/afc/build && cmake -DCMAKE_INSTALL_PREFIX=/wd/afc/__install -DCMAKE_PREFIX_PATH=/usr -DBUILD_WITH_COVERAGE=off -DCMAKE_BUILD_TYPE=EngineRelease -DSVN_LAST_REVISION=$BUILDREV -G Ninja /wd/afc && ninja -j$(nproc) install +``` +Now the afc-engine is ready: +``` +[@wcc-afc-01 work/dimar/open-afc] > ls -l build/src/afc-engine/afc-engine +-rwxr-xr-x. 1 dr942120 dr942120 4073528 Mar 8 04:03 build/src/afc-engine/afc-engine +``` +run it from the default worker container: +``` +docker run --rm -it --user `id -u`:`id -g` --group-add `id -G | sed "s/ / --group-add /g"` -v `pwd`:/wd/afc -v /opt/afc/worker:latest sh +``` +inside the worker container execute the afc-engine app +``` +./afc/build/src/afc-engine/afc-engine +``` + +## Prereqs +OpenAFC containers needs several mappings to work properly. Assuming that you are using /var/databases on your host to store the databases, you can select either option 1 here (which is assumed in the docker compose shown below) or set mappings individually as shown in 2-6. + +1) All databases in one folder - map to /mnt/nfs/rat_transfer + ``` + /var/databases:/mnt/nfs/rat_transfer + ``` + Those databases are: + - 3dep + - daily_uls_parse + - databases + - globe + - itudata + - nlcd + - population + - proc_gdal + - proc_lidar_2019 + - RAS_Database + - srtm3arcsecondv003 + - ULS_Database + - nfa + - pr + + +2) LiDAR Databases to /mnt/nfs/rat_transfer/proc_lidar_2019 + ``` + /var/databases/proc_lidar_2019:/mnt/nfs/rat_transfer/proc_lidar_2019 + ``` +3) RAS database to /mnt/nfs/rat_transfer/RAS_Database + ``` + /var/databases/RAS_Database:/mnt/nfs/rat_transfer/RAS_Database + ``` +4) Actual ULS Databases to /mnt/nfs/rat_transfer/ULS_Database + ``` + /var/databases/ULS_Database:/mnt/nfs/rat_transfer/ULS_Database + ``` +5) Folder with daily ULS Parse data /mnt/nfs/rat_transfer/daily_uls_parse + ``` + /var/databases/daily_uls_parse:/mnt/nfs/rat_transfer/daily_uls_parse + ``` +6) Folder with AFC Config data /mnt/nfs/afc_config (now can be moved to Object Storage by default) + ``` + /var/afc_config:/mnt/nfs/afc_config + ``` +**NB: All or almost all files and folders should be owned by user and group 1003 (currently - fbrat)** + +This can be applied via following command (mind the real location of these folders on your host system): + +``` +chown -R 1003:1003 /var/databases /var/afc_config +``` + +## docker-compose + +You would probably like to use docker-compose for setting up everything together - in this case feel free to use following docker-compose.yaml file as reference. +also check [docker-compose.yaml](/tests/regression/docker-compose.yaml) and [.env](/tests/regression/.env) files from tests/regression directory, which are used by OpenAFC CI. + +Note that the image tags here are the ones from the [manual build](#to-manually-build-containers-one-by-one) not the ones used by the [script build](#using-scripts-from-the-code-base). + +``` +version: '3.2' +services: + ratdb: + image: ratdb:${TAG:-latest} + restart: always + dns_search: [.] + + rmq: + image: rmq:${TAG:-latest} + restart: always + dns_search: [.] + + dispatcher: + image: dispatcher:${TAG:-latest} + restart: always + ports: + - "${EXT_PORT}:80" + - "${EXT_PORT_S}:443" + volumes: + - ${VOL_H_NGNX:-/tmp}:${VOL_C_NGNX:-/dummyngnx} + environment: + - AFC_SERVER_NAME=${AFC_SERVER_NAME:-_} + - AFC_ENFORCE_HTTPS=${AFC_ENFORCE_HTTPS:-TRUE} + # set to true if required to enforce mTLS check + - AFC_ENFORCE_MTLS=false + - AFC_MSGHND_NAME=msghnd + - AFC_MSGHND_PORT=8000 + - AFC_WEBUI_NAME=rat_server + - AFC_WEBUI_PORT=80 + # Filestorage params: + - AFC_OBJST_HOST=objst + - AFC_OBJST_PORT=5000 + - AFC_OBJST_SCHEME=HTTP + depends_on: + - msghnd + - rat_server + dns_search: [.] + + rat_server: + image: rat_server:${TAG:-latest} + volumes: + - ${VOL_H_DB}:${VOL_C_DB} + - ./pipe:/pipe + depends_on: + - ratdb + - rmq + - objst + - als_kafka + - als_siphon + - bulk_postgres + - rcache + secrets: + - NOTIFIER_MAIL.json + - OIDC.json + - REGISTRATION.json + - REGISTRATION_CAPTCHA.json + dns_search: [.] + environment: + # RabbitMQ server name: + - BROKER_TYPE=external + - BROKER_FQDN=rmq + # Filestorage params: + - AFC_OBJST_HOST=objst + - AFC_OBJST_PORT=5000 + - AFC_OBJST_SCHEME=HTTP + # ALS params + - ALS_KAFKA_SERVER_ID=rat_server + - ALS_KAFKA_CLIENT_BOOTSTRAP_SERVERS=${ALS_KAFKA_SERVER_}:${ALS_KAFKA_CLIENT_PORT_} + - ALS_KAFKA_MAX_REQUEST_SIZE=${ALS_KAFKA_MAX_REQUEST_SIZE_} + # Rcache parameters + - RCACHE_ENABLED=${RCACHE_ENABLED} + - RCACHE_POSTGRES_DSN=postgresql://postgres:postgres@bulk_postgres/rcache + - RCACHE_SERVICE_URL=http://rcache:${RCACHE_CLIENT_PORT} + - RCACHE_RMQ_DSN=amqp://rcache:rcache@rmq:5672/rcache + + msghnd: + image: msghnd:${TAG:-latest} + environment: + # RabbitMQ server name: + - BROKER_TYPE=external + - BROKER_FQDN=rmq + # Filestorage params: + - AFC_OBJST_HOST=objst + - AFC_OBJST_PORT=5000 + - AFC_OBJST_SCHEME=HTTP + # ALS params + - ALS_KAFKA_SERVER_ID=msghnd + - ALS_KAFKA_CLIENT_BOOTSTRAP_SERVERS=${ALS_KAFKA_SERVER_}:${ALS_KAFKA_CLIENT_PORT_} + - ALS_KAFKA_MAX_REQUEST_SIZE=${ALS_KAFKA_MAX_REQUEST_SIZE_} + # Rcache parameters + - RCACHE_ENABLED=${RCACHE_ENABLED} + - RCACHE_POSTGRES_DSN=postgresql://postgres:postgres@bulk_postgres/rcache + - RCACHE_SERVICE_URL=http://rcache:${RCACHE_CLIENT_PORT} + - RCACHE_RMQ_DSN=amqp://rcache:rcache@rmq:5672/rcache + dns_search: [.] + depends_on: + - ratdb + - rmq + - objst + - als_kafka + - als_siphon + - bulk_postgres + - rcache + + objst: + image: objst:${TAG:-latest} + environment: + - AFC_OBJST_PORT=5000 + - AFC_OBJST_HIST_PORT=4999 + - AFC_OBJST_LOCAL_DIR=/storage + dns_search: [.] + + worker: + image: worker:${TAG:-latest} + volumes: + - ${VOL_H_DB}:${VOL_C_DB} + - ./pipe:/pipe + environment: + # Filestorage params: + - AFC_OBJST_HOST=objst + - AFC_OBJST_PORT=5000 + - AFC_OBJST_SCHEME=HTTP + # worker params + - AFC_WORKER_CELERY_WORKERS=rat_1 rat_2 + # RabbitMQ server name: + - BROKER_TYPE=external + - BROKER_FQDN=rmq + # afc-engine preload lib params + - AFC_AEP_ENABLE=1 + - AFC_AEP_DEBUG=1 + - AFC_AEP_REAL_MOUNTPOINT=${VOL_C_DB}/3dep/1_arcsec + # Rcache parameters + - RCACHE_ENABLED=${RCACHE_ENABLED} + - RCACHE_SERVICE_URL=http://rcache:${RCACHE_CLIENT_PORT} + - RCACHE_RMQ_DSN=amqp://rcache:rcache@rmq:5672/rcache + # ALS params + - ALS_KAFKA_SERVER_ID=worker + - ALS_KAFKA_CLIENT_BOOTSTRAP_SERVERS=${ALS_KAFKA_SERVER_}:${ALS_KAFKA_CLIENT_PORT_} + - ALS_KAFKA_MAX_REQUEST_SIZE=${ALS_KAFKA_MAX_REQUEST_SIZE_} + depends_on: + - ratdb + - rmq + - objst + - rcache + - als_kafka + dns_search: [.] + + als_kafka: + image: als-kafka:${TAG:-latest} + restart: always + environment: + - KAFKA_ADVERTISED_HOST=${ALS_KAFKA_SERVER_} + - KAFKA_CLIENT_PORT=${ALS_KAFKA_CLIENT_PORT_} + - KAFKA_MAX_REQUEST_SIZE=${ALS_KAFKA_MAX_REQUEST_SIZE_} + dns_search: [.] + + als_siphon: + image: als-siphon:${TAG:-latest} + restart: always + environment: + - KAFKA_SERVERS=${ALS_KAFKA_SERVER_}:${ALS_KAFKA_CLIENT_PORT_} + - POSTGRES_HOST=bulk_postgres + - INIT_IF_EXISTS=skip + - KAFKA_MAX_REQUEST_SIZE=${ALS_KAFKA_MAX_REQUEST_SIZE_} + depends_on: + - als_kafka + - bulk_postgres + dns_search: [.] + + bulk_postgres: + image: public.ecr.aws/w9v6y1o0/openafc/bulk-postgres-image:${TAG:-latest} + dns_search: [.] + + uls_downloader: + image: public.ecr.aws/w9v6y1o0/openafc/uls-downloader:${TAG:-latest} + restart: always + environment: + - ULS_SERVICE_STATE_DB_DSN=postgresql://postgres:postgres@bulk_postgres/fs_state + - ULS_AFC_URL=http://msghnd:8000/fbrat/ap-afc/availableSpectrumInquiryInternal?nocache=True + - ULS_DELAY_HR=1 + - ULS_PROMETHEUS_PORT=8000 + # Rcache parameters + - RCACHE_ENABLED=${RCACHE_ENABLED} + - RCACHE_SERVICE_URL=http://rcache:${RCACHE_CLIENT_PORT} + volumes: + - ${VOL_H_DB}/ULS_Database:/rat_transfer/ULS_Database + - ${VOL_H_DB}/RAS_Database:/rat_transfer/RAS_Database + secrets: + - NOTIFIER_MAIL.json + dns_search: [.] + + cert_db: + image: cert_db:${TAG:-latest} + depends_on: + - ratdb + links: + - ratdb + - als_kafka + environment: + - ALS_KAFKA_SERVER_ID=cert_db + - ALS_KAFKA_CLIENT_BOOTSTRAP_SERVERS=${ALS_KAFKA_SERVER_}:${ALS_KAFKA_CLIENT_PORT_} + - ALS_KAFKA_MAX_REQUEST_SIZE=${ALS_KAFKA_MAX_REQUEST_SIZE_} + + rcache: + image: rcache:${TAG:-latest} + restart: always + environment: + - RCACHE_ENABLED=${RCACHE_ENABLED} + - RCACHE_CLIENT_PORT=${RCACHE_CLIENT_PORT} + - RCACHE_POSTGRES_DSN=postgresql://postgres:postgres@bulk_postgres/rcache + - RCACHE_AFC_REQ_URL=http://msghnd:8000/fbrat/ap-afc/availableSpectrumInquiry?nocache=True + - RCACHE_RULESETS_URL=http://rat_server/fbrat/ratapi/v1/GetRulesetIDs + - RCACHE_CONFIG_RETRIEVAL_URL=http://rat_server/fbrat/ratapi/v1/GetAfcConfigByRulesetID + depends_on: + - bulk_postgres + dns_search: [.] + + grafana: + image: grafana-image:${TAG:-latest} + restart: always + depends_on: + - prometheus + - bulk_postgres + dns_search: [.] + + prometheus: + image: prometheus-image:${TAG:-latest} + restart: always + depends_on: + - cadvisor + - nginxexporter + dns_search: [.] + + cadvisor: + image: cadvisor-image:${TAG:-latest} + restart: always + volumes: + - /:/rootfs:ro + - /var/run:/var/run:rw + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + - /dev/disk/:/dev/disk:ro + dns_search: [.] + + nginxexporter: + image: nginxexporter-image:${TAG:-latest} + restart: always + depends_on: + - dispatcher + dns_search: [.] + +secrets: + NOTIFIER_MAIL.json: + file: ${VOL_H_SECRETS}/NOTIFIER_MAIL.json + OIDC.json: + file: ${VOL_H_SECRETS}/OIDC.json + REGISTRATION.json: + file: ${VOL_H_SECRETS}/REGISTRATION.json + REGISTRATION_CAPTCHA.json: + file: ${VOL_H_SECRETS}/REGISTRATION_CAPTCHA.json + + +``` +`.env` file used with the docker-compose.yaml. please read comments in the file and update it accordingly +``` +# --------------------------------------------------- # +# docker-compose.yaml variables # +# convention: Host volume VOL_H_XXX will be mapped # +# as container's volume VOL_C_YYY # +# VOL_H_XXX:VOL_C_YYY # +# --------------------------------------------------- # + +# -= MUST BE defined =- +# Hostname for AFC server +AFC_SERVER_NAME="_" +# Wether to forward all http requests to https +AFC_ENFORCE_HTTPS=TRUE + +# Host static DB root dir +VOL_H_DB=/var/databases/rat_transfer + +# Container's static DB root dir (dont change it !) +VOL_C_DB=/mnt/nfs/rat_transfer + +#RAT user to be used in containers +UID=1003 +GID=1003 + +# AFC service external PORTs configuration +# syntax: +# [IP]: +# like 172.31.11.188:80-180 +# where: +# IP is 172.31.11.188 +# port range is 80-180 + +# Here we configuring range of external ports to be used by the service +# docker-compose randomly uses one port from the range + +# Note 1: +# The IP arrdess can be skipped if there is only one external +# IP address (i.e. 80-180 w/o IP address is acceptable as well) + +# Note 2: +# range of ports can be skipped . and just one port is acceptable as well + +# all these valuase are acaptable: +# PORT=172.31.11.188:80-180 +# PORT=172.31.11.188:80 +# PORT=80-180 +# PORT=80 + + +# http ports range +EXT_PORT=80 + +# https host ports range +EXT_PORT_S=443 + + +# -= ALS CONFIGURATION STUFF =- + +# Port on which ALS Kafka server listens for clients +ALS_KAFKA_CLIENT_PORT_=9092 + +# ALS Kafka server host name +ALS_KAFKA_SERVER_=als_kafka + +# Maximum ALS message size (default 1MB is too tight for GUI AFC Response) +ALS_KAFKA_MAX_REQUEST_SIZE_=10485760 + + +# -= FS(ULS) DOWNLOADER CONFIGURATION STUFF =- + +# Symlink pointing to current ULS database +ULS_CURRENT_DB_SYMLINK=FS_LATEST.sqlite3 + + +# -= RCACHE SERVICE CONFIGURATION STUFF =- + +# True (1, t, on, y, yes) to enable use of Rcache. False (0, f, off, n, no) to +# use legacy file-based cache. Default is True +RCACHE_ENABLED=True + +# Port Rcache service listens os +RCACHE_CLIENT_PORT=8000 + + +# -= SECRETS STUFF =- + +# Host directory containing secret files +VOL_H_SECRETS=../../tools/secrets/empty_secrets +#VOL_H_SECRETS=/opt/afc/secrets + +# Directory inside container where to secrets are mounted (always /run/secrets +# in Compose, may vary in Kubernetes) +VOL_C_SECRETS=/run/secrets + + + +# -= OPTIONAL =- +# to work without tls/mtls,remove these variables from here +# if you have tls/mtls configuration, keep configuration +# files in these host volumes +VOL_H_SSL=./ssl +VOL_C_SSL=/usr/share/ca-certificates/certs +VOL_H_NGNX=./ssl/nginx +VOL_C_NGNX=/certificates/servers + + +``` + + +Just create this file on the same level with Dockerfile and you are almost ready. Verify that the VOL_H_DB setting in the .env file is pointing at your host directory with the databases. + +Just run in this folder following command and it is done: +``` +docker-compose up -d +``` + +Keep in mind that on the first run it will build and pull all the needed containers and it can take some time (based on your machine power). You may want to do a build (see [Building Docker image from Dockerfiles](#building-docker-image-from-dockerfiles)) + +After the initial start of the server we recommend to stop it and then start again using these commands: +``` +docker-compose down +docker-compose up -d +``` + +If you later need to rebuild the server with the changes - simply run this command: +``` +docker-compose build +``` +and then restart it. +To force rebuild it completely use _--no-cache_ option: + +``` +docker-compose build --no-cache +``` + +**NB: the postgres container requires the folder /mnt/nfs/pgsql/data to be owned by it's internal user and group _postgres_, which both have id 999.** + +You can achieve it this way (mind the real location of these folders on your host system): +``` +chown 999:999 /var/databases/pgdata +``` +## Initial configuration and first user + +On the first start of the PostgreSQL server there are some initial steps to do. First to create the database. Its default name now is **fbrat**. If you are using compose script described above, everything will be done automatically to prepare the database for intialization. + +After that, once OpenAFC server is started, you need to create DB structure for the user database. This can be done using a _rat-manage-api_ utility. + +``` +rat-manage-api db-create +``` + +If you do it with the server which is run thru the docker-compose script described above, you can do it using this command: +``` +docker-compose exec rat_server rat-manage-api db-create +``` +### Initial Super Administrator account + +Once done with database and starting the server, you need to create default administrative user to handle your server from WebUI. It is done from the server console using the _rat-manage-api_ utility. + +If you are running from the compose file described above, you first need to get the OpenAFC server console. +``` +docker-compose exec rat_server bash +``` +it will return something like this: +``` +[root@149372a2ac05 wd]# +``` +this means you are in. + +By default, the login uses non OIDC login method which manages user accounts locally. You can use the following command to create an administrator for your OpenAFC server. + +``` +rat-manage-api user create --role Super --role Admin --role AP --role Analysis admin "Enter Your Password Here" +``` + +Once done, you can authorize with this user and password in WebUI. +To exit the console press Ctrl+D or type the 'exit' command. + +If you would like to use OIDC login method, please read [OIDC_Login.md](/OIDC_Login.md) + + +## **Environment variables** +|Name|Default val|Container|Notes +| :- | :- | :- | :- | +| **RabbitMQ settings**|||| +|BROKER_TYPE|`internal`|rat-server,msghnd,worker | whether `internal` or `external` AFC RMQ service used| +|BROKER_PROT|`amqp` |rat-server,msghnd,worker | what protocol used for AFC RMQ service| +|BROKER_USER|`celery`|rat-server,msghnd,worker | user used for AFC RMQ service| +|BROKER_PWD |`celery`|rat-server,msghnd,worker | password used for AFC RMQ service| +|BROKER_FQDN|`localhost`|rat-server,msghnd,worker | IP/domain name of AFC RMQ service| +|BROKER_PORT|`5672`|rat-server,msghnd,worker | port of AFC RMQ service| +|RMQ_LOG_CONSOLE_LEVEL|warning|rmq|RabbitMQ console log level (debug, info, warning, error, critical, none)| +| **AFC Object Storage** |||please read [objst README.md](/objstorage/README.md)| +|AFC_OBJST_HOST|`0.0.0.0`|objst,rat-server,msghnd,worker|file storage service host domain/IP| +|AFC_OBJST_PORT|`5000`|objst,rat-server,msghnd,worker|file storage service port| +|AFC_OBJST_SCHEME|'HTTP'|rat-server,msghnd,worker|file storage service scheme. `HTTP` or `HTTPS`| +|AFC_OBJST_MEDIA|`LocalFS`|objst|The media used for storing files by the service. The possible values are `LocalFS` - store files on docker's FS. `GoogleCloudBucket` - store files on Google Store| +|AFC_OBJST_LOCAL_DIR|`/storage`|objst|file system path to stored files in file storage container. Used only when `AFC_OBJST_MEDIA` is `LocalFS`| +|AFC_OBJST_LOG_LVL|`ERROR`|objst|logging level of the file storage. The relevant values are `DEBUG` and `ERROR`| +|AFC_OBJST_HIST_PORT|`4999`|objst,rat-server,msghnd,worker|history service port| +|AFC_OBJST_WORKERS|`10`|objst|number of gunicorn workers running objst server| +|AFC_OBJST_HIST_WORKERS|`2`|objst|number of gunicorn workers runnining history server| +| **MSGHND settings**|||| +|AFC_MSGHND_BIND|`0.0.0.0`|msghnd| the socket to bind. a string of the form: | +|AFC_MSGHND_PORT|`8000`|msghnd| the port to use in bind. a string of the form: | +|AFC_MSGHND_PID|`/run/gunicorn/openafc_app.pid`|msghnd| a filename to use for the PID file| +|AFC_MSGHND_WORKERS|`20`|msghnd| the number of worker processes for handling requests| +|AFC_MSGHND_TIMEOUT|`180`|msghnd| workers silent for more than this many seconds are killed and restarted| +|AFC_MSGHND_ACCESS_LOG||msghnd| the Access log file to write to. Default to don't. Use `/proc/self/fd/2` for console| +|AFC_MSGHND_ERROR_LOG|`/proc/self/fd/2`|msghnd| the Error log file to write to| +|AFC_MSGHND_LOG_LEVEL|`info`|msghnd| The granularity of Error log outputs (values are 'debug', 'info', 'warning', 'error', 'critical'| +| **worker settings**|||please read [afc-engine-preload README.md](/src/afc-engine-preload/README.md)| +|AFC_AEP_ENABLE|Not defined|worker|Enable the preload library if defined| +|AFC_AEP_FILELIST|`/aep/list/aep.list`|worker|Path to file tree info file| +|AFC_AEP_DEBUG|`0`|worker|Log level. 0 - disable, 1 - log time of read operations| +|AFC_AEP_LOGFILE|`/aep/log/aep.log`|worker|Where to write the log| +|AFC_AEP_CACHE|`/aep/cache`|worker|Where to store the cache| +|AFC_AEP_CACHE_MAX_FILE_SIZE|`50000000`|worker|Cache files with size less than the value| +|AFC_AEP_CACHE_MAX_SIZE|`1000000000`|worker|Max cache size| +|AFC_AEP_REAL_MOUNTPOINT|`/mnt/nfs/rat_transfer`|worker|Redirect read access to there| +|AFC_AEP_ENGINE_MOUNTPOINT|value of AFC_AEP_REAL_MOUNTPOINT|worker|Redirect read access from here| +|AFC_WORKER_CELERY_WORKERS|`rat_1`|worker|Celery worker name(s) to use| +|AFC_WORKER_CELERY_OPTS||worker|Additional celery worker options| +|AFC_WORKER_CELERY_LOG|`INFO`|worker|Celery log level. `ERROR` or `INFO` or `DEBUG`| +|AFC_ENGINE_LOG_LVL|'info'|worker|afc-engine log level| +|AFC_MSGHND_NAME|msghnd|dispatcher|Message handler service hostname| +|AFC_MSGHND_PORT|8000|dispatcher|Message handler service HTTP port| +|AFC_WEBUI_NAME|rat_server|dispatcher|WebUI service hostname| +|AFC_WEBUI_PORT|80|dispatcher|WebUI service HTTP Port| +|AFC_ENFORCE_HTTPS|TRUE|dispatcher|Wether to enforce forwarding of HTTP requests to HTTPS. TRUE - for enable, everything else - to disable| +|AFC_SERVER_NAME|"_"|dispatcher|Hostname of the AFC Server, for example - "openafc.tip.build". "_" - will accept any hostname (but this is not secure)| +| **RCACHE settings** |||| +|RCACHE_ENABLED|TRUE|rcache, rat_server, msghnd, worker, uls_downloader|TRUE if Rcache enabled, FALSE to use legacy objstroage response cache| +|RCACHE_POSTGRES_DSN|Must be set|rcache, rat_server, msghnd|Connection string to Rcache Postgres database| +|RCACHE_SERVICE_URL|Must be set|rat_server, msghnd, worker, uls_downloader|Rcache service REST API base URL| +|RCACHE_RMQ_DSN|Must be set|rat_server, msghnd, worker|AMQP URL to RabbitMQ vhost that workers use to communicate computation result| +|RCACHE_UPDATE_ON_SEND|TRUE|TRUE if worker sends result to Rcache server, FALSE if msghnd/rat_server| +|RCACHE_CLIENT_PORT|8000|rcache|Rcache REST API port| +|RCACHE_AFC_REQ_URL||REST API Rcache precomputer uses to send invalidated AFC requests for precomputation. No precomputation if not set| +|RCACHE_RULESETS_URL||REST API Rcache spatial invalidator uses to retrieve AFC Configs' rulesets. Default invalidation distance usd if not set| +|RCACHE_CONFIG_RETRIEVAL_URL||REST API Rcache spatial invalidator uses to retrieve AFC Config by ruleset. Default invalidation distance usd if not set| + + +## RabbitMQ settings + +There is a way to conifugre AFC server to use a RabbitMQ broker from different docker image. +Following the list of environment variables you may configure a server to use 'external' Rabbit MQ instance. +``` +BROKER_TYPE = external +BROKER_PROT = amqp +BROKER_USER = celery +BROKER_PWD = celery +BROKER_FQDN = +BROKER_PORT = 5672 +BROKER_MNG_PORT = 15672 +``` +Following the example to use RabbitMQ service in docker-compose. +``` + rmq: + image: public.ecr.aws/w9v6y1o0/openafc/rmq-image:latest + restart: always +``` + +## Managing the PostgreSQL database for users + +### Upgrading PostgresSQL +When PostgreSQL is upgraded the pgdata should be converted to be compatible with the new PostgreSQL version. It can be done by tools/db_tools/update_db.sh script. +``` +tools/db_tools/update_db.sh [pgdata_dir] [postgres_password] [old_postgres_version] [new_postgres_version] +``` +This script makes a backup of [pgdata_dir] to [pgdata_dir].back and puts the converted db in [pgdata_dir]. +This command should be run under root permissions, i.e. 'sudo tools/db_tools/update_db.sh ...' + +Example: convert db which was created by PostgreSQL version 9.6 to be used by PostgreSQL version 14.7: +``` +sudo tools/db_tools/update_db.sh ./pgdata qwerty 9.6 14.7 +``` + +### Note for an existing user database + +Database format has changed over time. If your user database uses older format, you might find errors indicating missing database fields upon bootup and login. The error message has instructions on how to migrate the database. These steps apply whether you're using OIDC or non OIDC login method. You have sereral options: + +**1. Reinitialize the database without users:** + +``` +rat-manage-api db-drop +rat-manage-api db-create +``` + +This will wipe out existing users, e.g. user acounts need to be manually recreated again. + +**2. Migrate the database with users:** + +``` +rat-manage-api db-upgrade +``` +## Managing user accounts +Users can be created and removed. User roles can be added and removed. +Remove user with user remove command, e.g.: +``` +rat-manage-api user remove user@mycompany.com + +``` +Update user roles with user update command, e.g.: +``` +rat-manage-api user update --role Admin --role AP --role Analysis --email "user@mycompany.com" +``` +Create user with user create command. If org argument is not given, the organization can be derived from the username if it's given in the form of an email address e.g.: +``` +rat-manage-api user create --role Admin --role AP --role Analysis --org mycompany.com "username" "mypassword' + +``` +## User roles +Roles are: Super, admin, AP, Admin, Analysis, Trial +"Super" is the highest level role, which allows access rights to all organizations, as opposed to "Admin", which is limited to one organization. When upgrade from older system without "Super", you will need to decide which users to be assigned role of "Super" and update their roles via the user update command. + +## MTLS +Vanilla installation comes with placeholder file for client certificate bundle. + +Besides the GUI, mtls certificates can be managed via CLI. +To list certificates: +``` +rat-manage-api mtls list +``` + +To add certificates: +``` +rat-manage-api mtls create --src --org --note +``` + +To remove certificates: +``` +rat-manage-api mtls remove --id +``` +To dump a certificate to a file: +``` +rat-manage-api mtls dump --id --dst +``` + +Happy usage! + +## ULS database update automation + +ULS Database needs (preferrably daily) updates to be up-to-date with regulators requirements. See [README in uls](uls/README.md "ULS Service ReadMe") for instructions and configuration. The docker compose given above will create a container that runs the daily ULS update service. diff --git a/ReleaseNote.md b/ReleaseNote.md new file mode 100644 index 0000000..d671ca3 --- /dev/null +++ b/ReleaseNote.md @@ -0,0 +1,2 @@ +# Release Note +## **Version and Date** diff --git a/TestDemoUser.Readme.md b/TestDemoUser.Readme.md new file mode 100644 index 0000000..22f2c51 --- /dev/null +++ b/TestDemoUser.Readme.md @@ -0,0 +1,3 @@ +# DEMO and Test user configuration +Super user can chose **DEMO_US** as a ruleset in the for Demo purposes or **TEST_US** for testing afc config +When the AvailableSpectrumInquiry is sent with these rulesets, the appropriate config will be applied. diff --git a/TrialUser.Readme.md b/TrialUser.Readme.md new file mode 100644 index 0000000..3e78743 --- /dev/null +++ b/TrialUser.Readme.md @@ -0,0 +1,9 @@ +# Trial user configuration +AFC has the capability to provide for trial users that have limited ability to perform spectrum availability requests in preset configuration. + +### Create a user with the trial role only +The new user can register for account online via the UI. Upon approval, the user is granted by default the Trial role, and can run start sending inquiries. + +### Running the Spectrum query as the Trial user +The trial user can simply provide **TestCertificationId** as the certification ID and **TestSerialNumber** as the serial number in the Spectrum query. + diff --git a/als/ALS.sql b/als/ALS.sql new file mode 100644 index 0000000..8e0fa6b --- /dev/null +++ b/als/ALS.sql @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2022 Broadcom. All rights reserved. + * The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate + * that owns the software below. + * This work is licensed under the OpenAFC Project License, a copy of which is + * included with this software program. + * + * This file creates ALS (AFC Request/Response/Config Logging System) database on PostgreSQL+PostGIS server + * This file is generated, direct editing is not recommended. + * Intended maintenance sequence is as follows: + * 1. Load (copypaste) als_db_schema/ALS.dbml into dbdiagram.io + * 2. Modify as needed + * 3. Save (copypaste) modified sources back to als_db_schema/ALS.dbml + * 4. Also export schema in PostgreSQL format as als_db_schema/ALS_raw.sql + * 5. Rectify exported schema with als_rectifier.awk (awk -f als_db_schema/als_rectifier.awk < als_db_schema/ALS_raw.sql > ALS.sql) + */ + +CREATE EXTENSION postgis; + +CREATE TABLE "afc_message" ( + "message_id" bigserial, + "month_idx" smallint, + "afc_server" serial, + "rx_time" timestamptz, + "tx_time" timestamptz, + "rx_envelope_digest" uuid, + "tx_envelope_digest" uuid, + PRIMARY KEY ("message_id", "month_idx") +); + +CREATE TABLE "rx_envelope" ( + "rx_envelope_digest" uuid, + "month_idx" smallint, + "envelope_json" json, + PRIMARY KEY ("rx_envelope_digest", "month_idx") +); + +CREATE TABLE "tx_envelope" ( + "tx_envelope_digest" uuid, + "month_idx" smallint, + "envelope_json" json, + PRIMARY KEY ("tx_envelope_digest", "month_idx") +); + +CREATE TABLE "request_response_in_message" ( + "message_id" bigint, + "request_id" text, + "month_idx" smallint, + "request_response_digest" uuid, + "expire_time" timestamptz, + PRIMARY KEY ("message_id", "request_id", "month_idx") +); + +CREATE TABLE "request_response" ( + "request_response_digest" uuid, + "month_idx" smallint, + "afc_config_text_digest" uuid, + "customer_id" integer, + "uls_data_version_id" integer, + "geo_data_version_id" integer, + "request_json_digest" uuid, + "response_json_digest" uuid, + "device_descriptor_digest" uuid, + "location_digest" uuid, + "response_code" int, + "response_description" text, + "response_data" text, + PRIMARY KEY ("request_response_digest", "month_idx") +); + +CREATE TABLE "device_descriptor" ( + "device_descriptor_digest" uuid, + "month_idx" smallint, + "serial_number" text, + "certifications_digest" uuid, + PRIMARY KEY ("device_descriptor_digest", "month_idx") +); + +CREATE TABLE "certification" ( + "certifications_digest" uuid, + "certification_index" smallint, + "month_idx" smallint, + "ruleset_id" text, + "certification_id" text, + PRIMARY KEY ("certifications_digest", "certification_index", "month_idx") +); + +CREATE TABLE "compressed_json" ( + "compressed_json_digest" uuid, + "month_idx" smallint, + "compressed_json_data" bytea, + PRIMARY KEY ("compressed_json_digest", "month_idx") +); + +CREATE TABLE "customer" ( + "customer_id" serial, + "month_idx" smallint, + "customer_name" text, + PRIMARY KEY ("customer_id", "month_idx") +); + +CREATE TABLE "location" ( + "location_digest" uuid, + "month_idx" smallint, + "location_wgs84" geography(POINT,4326), + "location_uncertainty_m" real, + "location_type" text, + "deployment_type" int, + "height_m" real, + "height_uncertainty_m" real, + "height_type" text, + PRIMARY KEY ("location_digest", "month_idx") +); + +CREATE TABLE "afc_config" ( + "afc_config_text_digest" uuid, + "month_idx" smallint, + "afc_config_text" text, + "afc_config_json" json, + PRIMARY KEY ("afc_config_text_digest", "month_idx") +); + +CREATE TABLE "geo_data_version" ( + "geo_data_version_id" serial, + "month_idx" smallint, + "geo_data_version" text, + PRIMARY KEY ("geo_data_version_id", "month_idx") +); + +CREATE TABLE "uls_data_version" ( + "uls_data_version_id" serial, + "month_idx" smallint, + "uls_data_version" text, + PRIMARY KEY ("uls_data_version_id", "month_idx") +); + +CREATE TABLE "max_psd" ( + "request_response_digest" uuid, + "month_idx" smallint, + "low_frequency_mhz" smallint, + "high_frequency_mhz" smallint, + "max_psd_dbm_mhz" real, + PRIMARY KEY ("request_response_digest", "month_idx", "low_frequency_mhz", "high_frequency_mhz") +); + +CREATE TABLE "max_eirp" ( + "request_response_digest" uuid, + "month_idx" smallint, + "op_class" smallint, + "channel" smallint, + "max_eirp_dbm" real, + PRIMARY KEY ("request_response_digest", "month_idx", "op_class", "channel") +); + +CREATE TABLE "afc_server" ( + "afc_server_id" serial, + "month_idx" smallint, + "afc_server_name" text, + PRIMARY KEY ("afc_server_id", "month_idx") +); + +CREATE TABLE "decode_error" ( + "id" bigserial PRIMARY KEY, + "time" timestamptz, + "msg" text, + "code_line" integer, + "data" text, + "month_idx" smallint +); + +CREATE INDEX ON "afc_message" ("rx_time"); + +CREATE INDEX ON "afc_message" ("tx_time"); + +CREATE INDEX ON "rx_envelope" USING HASH ("rx_envelope_digest"); + +CREATE INDEX ON "tx_envelope" USING HASH ("tx_envelope_digest"); + +CREATE INDEX ON "request_response_in_message" ("request_id"); + +CREATE INDEX ON "request_response_in_message" ("request_response_digest"); + +CREATE INDEX ON "request_response_in_message" ("expire_time"); + +CREATE INDEX ON "request_response" USING HASH ("request_response_digest"); + +CREATE INDEX ON "request_response" ("afc_config_text_digest"); + +CREATE INDEX ON "request_response" ("customer_id"); + +CREATE INDEX ON "request_response" ("device_descriptor_digest"); + +CREATE INDEX ON "request_response" ("location_digest"); + +CREATE INDEX ON "request_response" ("response_code"); + +CREATE INDEX ON "request_response" ("response_description"); + +CREATE INDEX ON "request_response" ("response_data"); + +CREATE INDEX ON "device_descriptor" USING HASH ("device_descriptor_digest"); + +CREATE INDEX ON "device_descriptor" ("serial_number"); + +CREATE INDEX ON "device_descriptor" ("certifications_digest"); + +CREATE INDEX ON "certification" USING HASH ("certifications_digest"); + +CREATE INDEX ON "certification" ("ruleset_id"); + +CREATE INDEX ON "certification" ("certification_id"); + +CREATE INDEX ON "compressed_json" USING HASH ("compressed_json_digest"); + +CREATE INDEX ON "customer" ("customer_name"); + +CREATE INDEX ON "location" USING HASH ("location_digest"); + +CREATE INDEX ON "location" ("location_wgs84"); + +CREATE INDEX ON "location" ("location_type"); + +CREATE INDEX ON "location" ("height_m"); + +CREATE INDEX ON "location" ("height_type"); + +CREATE INDEX ON "afc_config" USING HASH ("afc_config_text_digest"); + +CREATE UNIQUE INDEX ON "geo_data_version" ("geo_data_version", "month_idx"); + +CREATE INDEX ON "geo_data_version" ("geo_data_version"); + +CREATE UNIQUE INDEX ON "uls_data_version" ("uls_data_version", "month_idx"); + +CREATE INDEX ON "uls_data_version" ("uls_data_version"); + +CREATE INDEX ON "max_psd" USING HASH ("request_response_digest"); + +CREATE INDEX ON "max_psd" ("low_frequency_mhz"); + +CREATE INDEX ON "max_psd" ("high_frequency_mhz"); + +CREATE INDEX ON "max_psd" ("max_psd_dbm_mhz"); + +CREATE INDEX ON "max_eirp" USING HASH ("request_response_digest"); + +CREATE INDEX ON "max_eirp" ("op_class"); + +CREATE INDEX ON "max_eirp" ("channel"); + +CREATE INDEX ON "max_eirp" ("max_eirp_dbm"); + +CREATE UNIQUE INDEX ON "afc_server" ("afc_server_name", "month_idx"); + +CREATE INDEX ON "afc_server" ("afc_server_name"); + +COMMENT ON TABLE "afc_message" IS 'AFC Request/Response message pair (contain individual requests/responses)'; + +COMMENT ON COLUMN "afc_message"."rx_envelope_digest" IS 'Envelope of AFC Request message'; + +COMMENT ON COLUMN "afc_message"."tx_envelope_digest" IS 'Envelope of AFC Response message'; + +COMMENT ON TABLE "rx_envelope" IS 'Envelope (constant part) of AFC Request Message'; + +COMMENT ON COLUMN "rx_envelope"."rx_envelope_digest" IS 'MD5 of envelope_json field in UTF8 encoding'; + +COMMENT ON COLUMN "rx_envelope"."envelope_json" IS 'AFC Request JSON with empty availableSpectrumInquiryRequests field'; + +COMMENT ON TABLE "tx_envelope" IS 'Envelope (constant part) of AFC Response Message'; + +COMMENT ON COLUMN "tx_envelope"."tx_envelope_digest" IS 'MD5 of envelope_json field in UTF8 encoding'; + +COMMENT ON COLUMN "tx_envelope"."envelope_json" IS 'AFC Response JSON with empty availableSpectrumInquiryRequests field'; + +COMMENT ON TABLE "request_response_in_message" IS 'Associative table for relatonship between AFC Request/Response messages and individual requests/responses. Also encapsulates variable part of requests/responses'; + +COMMENT ON COLUMN "request_response_in_message"."message_id" IS 'AFC request/response message pair this request/response belongs'; + +COMMENT ON COLUMN "request_response_in_message"."request_id" IS 'ID of request/response within message'; + +COMMENT ON COLUMN "request_response_in_message"."request_response_digest" IS 'Reference to otentially constant part of request/response'; + +COMMENT ON COLUMN "request_response_in_message"."expire_time" IS 'Response expiration time'; + +COMMENT ON TABLE "request_response" IS 'Potentiially constant part of request/response'; + +COMMENT ON COLUMN "request_response"."request_response_digest" IS 'MD5 computed over request/response with requestId and availabilityExpireTime fields set to empty'; + +COMMENT ON COLUMN "request_response"."afc_config_text_digest" IS 'MD5 over used AFC Config text represnetation'; + +COMMENT ON COLUMN "request_response"."customer_id" IS 'AP vendor'; + +COMMENT ON COLUMN "request_response"."uls_data_version_id" IS 'Version of used ULS data'; + +COMMENT ON COLUMN "request_response"."geo_data_version_id" IS 'Version of used geospatial data'; + +COMMENT ON COLUMN "request_response"."request_json_digest" IS 'MD5 of request JSON with empty requestId'; + +COMMENT ON COLUMN "request_response"."response_json_digest" IS 'MD5 of resaponse JSON with empty requesatId and availabilityExpireTime'; + +COMMENT ON COLUMN "request_response"."device_descriptor_digest" IS 'MD5 of device descriptor (AP) related part of request JSON'; + +COMMENT ON COLUMN "request_response"."location_digest" IS 'MD5 of location-related part of request JSON'; + +COMMENT ON COLUMN "request_response"."response_description" IS 'Optional response code short description. Null for success'; + +COMMENT ON COLUMN "request_response"."response_data" IS 'Optional supplemental failure information. Optional comma-separated list of missing/invalid/unexpected parameters, etc.'; + +COMMENT ON TABLE "device_descriptor" IS 'Information about device (e.g. AP)'; + +COMMENT ON COLUMN "device_descriptor"."device_descriptor_digest" IS 'MD5 over parts of requesat JSON pertinent to AP'; + +COMMENT ON COLUMN "device_descriptor"."serial_number" IS 'AP serial number'; + +COMMENT ON COLUMN "device_descriptor"."certifications_digest" IS 'Device certifications'; + +COMMENT ON TABLE "certification" IS 'Element of certifications list'; + +COMMENT ON COLUMN "certification"."certifications_digest" IS 'MD5 of certification list json'; + +COMMENT ON COLUMN "certification"."certification_index" IS 'Index in certification list'; + +COMMENT ON COLUMN "certification"."ruleset_id" IS 'Name of rules for which AP certified (equivalent of region)'; + +COMMENT ON COLUMN "certification"."certification_id" IS 'ID of certification (equivalent of manufacturer)'; + +COMMENT ON TABLE "compressed_json" IS 'Compressed body of request or response'; + +COMMENT ON COLUMN "compressed_json"."compressed_json_digest" IS 'MD5 hash of compressed data'; + +COMMENT ON COLUMN "compressed_json"."compressed_json_data" IS 'Compressed data'; + +COMMENT ON TABLE "customer" IS 'Customer aka vendor aka user'; + +COMMENT ON COLUMN "customer"."customer_name" IS 'Its name'; + +COMMENT ON TABLE "location" IS 'AP location'; + +COMMENT ON COLUMN "location"."location_digest" IS 'MD5 computed over location part of request JSON'; + +COMMENT ON COLUMN "location"."location_wgs84" IS 'AP area center (WGS84 coordinates)'; + +COMMENT ON COLUMN "location"."location_uncertainty_m" IS 'Radius of AP uncertainty area in meters'; + +COMMENT ON COLUMN "location"."location_type" IS 'Ellipse/LinearPolygon/RadialPolygon'; + +COMMENT ON COLUMN "location"."deployment_type" IS '0/1/2 for unknown/indoor/outdoor'; + +COMMENT ON COLUMN "location"."height_m" IS 'AP elevation in meters'; + +COMMENT ON COLUMN "location"."height_uncertainty_m" IS 'Elevation uncertainty in meters'; + +COMMENT ON COLUMN "location"."height_type" IS 'Elevation type'; + +COMMENT ON TABLE "afc_config" IS 'AFC Config'; + +COMMENT ON COLUMN "afc_config"."afc_config_text_digest" IS 'MD5 computed over text representation'; + +COMMENT ON COLUMN "afc_config"."afc_config_text" IS 'Text representation of AFC Config'; + +COMMENT ON COLUMN "afc_config"."afc_config_json" IS 'JSON representation of AFC Config'; + +COMMENT ON TABLE "geo_data_version" IS 'Version of geospatial data'; + +COMMENT ON TABLE "uls_data_version" IS 'Version of ULS data"'; + +COMMENT ON TABLE "max_psd" IS 'PSD result'; + +COMMENT ON COLUMN "max_psd"."request_response_digest" IS 'Request this result belongs to'; + +COMMENT ON TABLE "max_eirp" IS 'EIRP result'; + +COMMENT ON COLUMN "max_eirp"."request_response_digest" IS 'Request this result belongs to'; + +ALTER TABLE "afc_message" ADD CONSTRAINT "afc_message_afc_server_ref" FOREIGN KEY ("afc_server", "month_idx") REFERENCES "afc_server" ("afc_server_id", "month_idx"); + +ALTER TABLE "afc_message" ADD CONSTRAINT "afc_message_rx_envelope_digest_ref" FOREIGN KEY ("rx_envelope_digest", "month_idx") REFERENCES "rx_envelope" ("rx_envelope_digest", "month_idx"); + +ALTER TABLE "afc_message" ADD CONSTRAINT "afc_message_tx_envelope_digest_ref" FOREIGN KEY ("tx_envelope_digest", "month_idx") REFERENCES "tx_envelope" ("tx_envelope_digest", "month_idx"); + +ALTER TABLE "request_response_in_message" ADD CONSTRAINT "request_response_in_message_message_id_ref" FOREIGN KEY ("message_id", "month_idx") REFERENCES "afc_message" ("message_id", "month_idx"); + +ALTER TABLE "request_response_in_message" ADD CONSTRAINT "request_response_in_message_request_response_digest_ref" FOREIGN KEY ("request_response_digest", "month_idx") REFERENCES "request_response" ("request_response_digest", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_afc_config_text_digest_ref" FOREIGN KEY ("afc_config_text_digest", "month_idx") REFERENCES "afc_config" ("afc_config_text_digest", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_customer_id_ref" FOREIGN KEY ("customer_id", "month_idx") REFERENCES "customer" ("customer_id", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_uls_data_version_id_ref" FOREIGN KEY ("uls_data_version_id", "month_idx") REFERENCES "uls_data_version" ("uls_data_version_id", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_geo_data_version_id_ref" FOREIGN KEY ("geo_data_version_id", "month_idx") REFERENCES "geo_data_version" ("geo_data_version_id", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_request_json_digest_ref" FOREIGN KEY ("request_json_digest", "month_idx") REFERENCES "compressed_json" ("compressed_json_digest", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_response_json_digest_ref" FOREIGN KEY ("response_json_digest", "month_idx") REFERENCES "compressed_json" ("compressed_json_digest", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_device_descriptor_digest_ref" FOREIGN KEY ("device_descriptor_digest", "month_idx") REFERENCES "device_descriptor" ("device_descriptor_digest", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_location_digest_ref" FOREIGN KEY ("location_digest", "month_idx") REFERENCES "location" ("location_digest", "month_idx"); + + +ALTER TABLE "max_psd" ADD CONSTRAINT "max_psd_request_response_digest_ref" FOREIGN KEY ("request_response_digest", "month_idx") REFERENCES "request_response" ("request_response_digest", "month_idx"); + +ALTER TABLE "max_eirp" ADD CONSTRAINT "max_eirp_request_response_digest_ref" FOREIGN KEY ("request_response_digest", "month_idx") REFERENCES "request_response" ("request_response_digest", "month_idx"); +; \ No newline at end of file diff --git a/als/Dockerfile.kafka b/als/Dockerfile.kafka new file mode 100644 index 0000000..f3ecc2b --- /dev/null +++ b/als/Dockerfile.kafka @@ -0,0 +1,43 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# Dockerfile for Kafka server, used by ALS (AFC Request/Response/Config) logging +# This dockerfile maps outside config variables to those, used by Bitnami/Kafka + +FROM bitnami/kafka:3.3.1 + +# Outside configuration variables +ENV KAFKA_ADVERTISED_HOST=localhost +ENV KAFKA_CLIENT_PORT=9092 +ENV KAFKA_BROKER_PORT=9093 +ENV KAFKA_CLIENT_SECURITY_PROTOCOL=PLAINTEXT + +# Bitnami Kafka configuration parameters +ENV KAFKA_ENABLE_KRAFT=yes +ENV KAFKA_CFG_PROCESS_ROLES=broker,controller +ENV KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER +ENV KAFKA_CFG_BROKER_ID=1 +ENV ALLOW_PLAINTEXT_LISTENER=yes +ENV KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true +ENV KAFKA_CFG_NUM_PARTITIONS=1 +ENV KAFKA_MAX_REQUEST_SIZE=1048576 + +# Setting Kafka log level to ERROR limits initialization blurt +ENV KAFKA_LOG_LEVEL=ERROR +RUN sed -i "s/log4j\.logger\.kafka=.*/log4j.logger.kafka=${KAFKA_LOG_LEVEL}/" /opt/bitnami/kafka/config/log4j.properties +RUN sed -i "s/log4j\.logger\.org\.apache\.kafka=.*/log4j.logger.org.apache.kafka=${KAFKA_LOG_LEVEL}/" /opt/bitnami/kafka/config/log4j.properties + +# Kafka environment variables, computed from DockerCompose-supplied variables +# can't be defined in ENV - hence they are moved to ENTRYPOINT +ENTRYPOINT env \ + KAFKA_CFG_MESSAGE_MAX_BYTES=${KAFKA_MAX_REQUEST_SIZE} \ + KAFKA_CFG_MAX_REQUEST_SIZE=${KAFKA_MAX_REQUEST_SIZE} \ + KAFKA_CFG_LISTENERS=PLAINTEXT://:${KAFKA_CLIENT_PORT},CONTROLLER://:${KAFKA_BROKER_PORT} \ + KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:${KAFKA_CLIENT_SECURITY_PROTOCOL},PLAINTEXT:${KAFKA_CLIENT_SECURITY_PROTOCOL} \ + KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://${KAFKA_ADVERTISED_HOST}:${KAFKA_CLIENT_PORT} \ + KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@localhost:${KAFKA_BROKER_PORT} \ + /opt/bitnami/scripts/kafka/entrypoint.sh /opt/bitnami/scripts/kafka/run.sh diff --git a/als/Dockerfile.siphon b/als/Dockerfile.siphon new file mode 100644 index 0000000..ad1f08e --- /dev/null +++ b/als/Dockerfile.siphon @@ -0,0 +1,90 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# Dockerfile for Siphon - als_siphon.py script that takes log records from +# Kafka and puts them to PostgreSQL+PostGIS database (optionally creating +# necessary database before operation) + +FROM alpine:3.18 + +RUN mkdir -p -m 777 /usr/app +WORKDIR /usr/app + +RUN apk add --update --no-cache python3=~3.11 py3-sqlalchemy=~1.4 py3-pip \ + py3-psycopg2 py3-pydantic=~1.10 py3-alembic py3-lz4 + +RUN apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \ + py3-confluent-kafka + +COPY requirements.txt /usr/app/ +RUN pip3 install --no-cache-dir --root-user-action=ignore -r requirements.txt + +ENV PYTHONPATH=/usr/app +ENV PATH=$PATH:/usr/app + +# Comma-separated list of Kafka (bootstrap) servers, each having 'host[:port]' +# form. Port, if not specified, is 9092 +ENV KAFKA_SERVERS=localhost +# Client ID to use in Kafka logs. If ends with '@' - suffixed by unique random +# string +ENV KAFKA_CLIENT_ID=siphon_@ +# 'SSL' or 'PLAINTEXT'. Default is 'PLAINTEXT' +ENV KAFKA_SECURITY_PROTOCOL= +# SSL keyfile +ENV KAFKA_SSL_KEYFILE= +# SSL CA (Certificate Authority) file +ENV KAFKA_SSL_CAFILE= +# Maximum message size (default is 1MB) +ENV KAFKA_MAX_REQUEST_SIZE= +# PostgreSQL server hostname +ENV POSTGRES_HOST=localhost +# PostgreSQL server port +ENV POSTGRES_PORT=5432 +# Parameters (name, user, password, options) of initial database - database to +# connect to to create other databases +ENV POSTGRES_INIT_DB=postgres +ENV POSTGRES_INIT_USER=postgres +ENV POSTGRES_INIT_PASSWORD=postgres +ENV POSTGRES_INIT_OPTIONS= +# Parameters (name, user, password, options) of database for +# Request/Response/Config logs +ENV POSTGRES_ALS_DB=ALS +ENV POSTGRES_ALS_USER=postgres +ENV POSTGRES_ALS_PASSWORD=postgres +ENV POSTGRES_ALS_OPTIONS= +# Parameters (name, user, password, options) of database for JSON logs +ENV POSTGRES_LOG_DB=AFC_LOGS +ENV POSTGRES_LOG_USER=postgres +ENV POSTGRES_LOG_PASSWORD=postgres +ENV POSTGRES_LOG_OPTIONS= +# What to do if database being created already exists: 'skip', 'drop'. Default +# is to fail +ENV INIT_IF_EXISTS=skip +# Port to serve Prometheus metrics on (none/empty is to not serve) +ENV SIPHON_PROMETHEUS_PORT=8080 + +COPY als_siphon.py als_query.py /usr/app/ +RUN chmod a+x /usr/app/*.py +COPY ALS.sql /usr/app + +ENTRYPOINT /usr/app/als_siphon.py init_siphon \ + --kafka_servers=$KAFKA_SERVERS \ + --kafka_client_id=$KAFKA_CLIENT_ID \ + --kafka_security_protocol=$KAFKA_SECURITY_PROTOCOL \ + --kafka_ssl_keyfile=$KAFKA_SSL_KEYFILE \ + --kafka_ssl_cafile=$KAFKA_SSL_CAFILE \ + --kafka_max_partition_fetch_bytes=$KAFKA_MAX_REQUEST_SIZE \ + --init_postgres=postgresql://${POSTGRES_INIT_USER}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_INIT_DB}${POSTGRES_INIT_OPTIONS} \ + --init_postgres_password=$POSTGRES_INIT_PASSWORD \ + --if_exists=$INIT_IF_EXISTS \ + --als_postgres=postgresql://${POSTGRES_ALS_USER}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_ALS_DB}${POSTGRES_ALS_OPTIONS} \ + --als_postgres_password=$POSTGRES_ALS_PASSWORD \ + --log_postgres=postgresql://${POSTGRES_LOG_USER}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_LOG_DB}${POSTGRES_LOG_OPTIONS} \ + --log_postgres_password=$POSTGRES_LOG_PASSWORD \ + --prometheus_port=$SIPHON_PROMETHEUS_PORT \ + --if_exists=$INIT_IF_EXISTS \ + --als_sql /usr/app/ALS.sql diff --git a/als/README.md b/als/README.md new file mode 100644 index 0000000..8398422 --- /dev/null +++ b/als/README.md @@ -0,0 +1,214 @@ +Copyright (C) 2022 Broadcom. All rights reserved.\ +The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate that +owns the software below. This work is licensed under the OpenAFC Project +License, a copy of which is included with this software program. + +# Tools For Working With Log Databases + +## Table of Contents +- [Databases ](#databases) + - [*ALS* Database ](#als_database) + - [*AFC\_LOGS* Database ](#afc_logs_database) + - [Initial Database ](#initial_database) + - [Template Databases ](#template_databases) +- [`als_siphon.py` - Moving Logs From Kafka To Postgres ](#als_siphon_py) +- [`als_query.py` - Querying Logs From Postgres Database ](#als_query_py) + - [Installation](#als_query_install) + - [Addressing PostgreSQL](#als_query_server) + - [`log` Command ](#als_query_log) + - [`log` Command Examples ](#als_query_log_examples) + + +## Databases + +ALS (AFC Log Storage) functionality revolves around two PostgreSQL databases, used for log storage: **ALS** and **AFC_LOGS**. + +### *ALS* Database + +Stores log of AFC Request/Response/Config data. Has rather convoluted multitable structure. + +SQL code for creation of this database contained in *ALS.sql* file. This file should be considered generated and not be manually edited. + +*als_db_schema* folder contains source material for *ALS.sql* generation: + +- *ALS.dbml*. Source file for [dbdiagram.io](https://dbdiagram.io) DB diagramming site. Copy/paste content of this file there, make modifications, then copy/paste back to this file. + Also upon completion *ALS_raw.sql* and *ALS.png* should be exported (as *Export to PostgreSQL* and *Export to PNG* respectively) - see below. + +- *ALS_raw.sql*. Database creation SQL script that should exported from [dbdiagram.io](https://dbdiagram.io) after changes made to *ALS.dbml*. + This file is almost like final *ALS.sql*, but requires certain tweaks: + * Declaring used PostgreSQL extensions (PostGIS in this case) + * Removal of many-to-many artifacts. For many-to-many relationships [dbdiagram.io](https://dbdiagram.io) creates artificial tables that are, adding insult to injury, violate PostgreSQL syntax. They are not used and should be removed. + * Segmentation. Database is planned with segmentation in mind (by *month_idx* field). But segmentation itself not performed. This will need to be done eventually. + +- *als_rectifier.awk* AWK script for converting *ALS_raw.sql* to *ALS.sql*. + +- *ALS.png* Picturesque database schema. Should be exported as PNG after changes, made to *ALS.dbml*. + +### *AFC_LOGS* Database + +For each JSON log type (*topic* on Kafka parlance) this database has separate table with following columns: + +- *time* Log record timetag with timezone + +- *source* String that uniquely identifies entity that created log record + +- *log* JSON log record. + +### Initial Database + +To create database `als_siphon.py` script should connect to already database. This already existing database named *initial database*, by default it is built-in database named *postgres*. + +### Template Databases + +Template databases, used for creation of *ALS* and *AFC_LOGS* databases. Something other than default might be used (but not yet, as of time of this writing). + + +## `als_siphon.py` - Moving Logs From Kafka To Postgres + +The main purpose of `als_siphon.py` is to fetch log records from Kafka and move them to previously described PostgreSQL databases. Also it can initialize those databases. + +`$ als_siphon.py COMMAND PARAMETERS` + +Commands are: + +- `init` Create *ALS* and/or *AFC_LOGS* database. If already exists, databases may be recreated or left intact. + +- `siphon` Do the moving from Kafka to PostgreSQL. + +- `init_siphon` First create databases then do the siphoning. Used for Docker operation. + +Parameters are many - see help messages. + + +## `als_query.py` - Querying Logs From Postgres Database + +This script queries logs, stored in *ALS* and *AFC_LOGS* databases. + +As of time of this writing this script only supports `log` command that reads JSON logs from *AFC_LOGS* + + +### Installation + +`als_query.py` requires Python 3 with reasonably recent *sqlalchemy*, *psycopg2*, *geoalchemy2* modules installed (latter is optional - not required for e.g. `log` command). + +Proper installation of these modules requires too much luck to be described here (as even `venv/virtualenv` does not help always - only sometimes). If you'll succeed - fine, otherwise there is one more method: running from the container where `als_siphon.py` installed. In latter case invocation looks like this: + +`$ docker exec SIPHON_CONTAINER als_query.py CMD ...` + +Here `SIPHON_CONTAINER` is either value from first column of `docker ps` or from last column of `docker-compose ps`. + +### Addressing PostgreSQL Server + +Another important aspect is how to access PostgreSQL database server where logs were placed. + +#### Explicit specification + +Using `--server` (aka `-s`) and `--password` parameters of `als_query.py` command line). Here are most probable cases: + +1. `als_query.py` runs inside `als_siphon.py` container, PostgreSQL runs inside the container, named `bulk_postgres` in *docker-compose.yaml* (that's how it is named as of time of this writing): + `$ docker exec SIPHON_CONTAINER als_query.py CMD \ ` + `--server [USER@]als_postrgres[:PORT][?OPTIONS] [--password PASSWORD] ...` + Here `USER` or `PORT` might be omitted if they are `postgres` and `5432` respectively. `--password PASSWORD` and `OPTIONS` are optional. + Actually, in this case `--server` and `--password` may be omitted - see below on the use of environment variables. + +2. User/host/port of PostgreSQL server is known: + `$ [docker exec SIPHON_CONTAINER] als_query CMD \ ` + `--server [USER@]HOST[:PORT][?OPTIONS] [--password PASSWORD] ...` + +3. `als_query.py` runs outside container, PostgreSQL runs inside container: + `$ als_query.py CMD \ ` + `--server [USER@]^POSTGRES_CONTAINER[:PORT][?OPTIONS] \ ` + `[--password PASSWORD] ...` + Note the `***^***` before `POSTGRES_CONTAINER`. Here, again `POSTGRES_CONTAINER` is either value from first column of `docker ps` or from last column of `docker-compose ps` for container running PostgreSQL + +I expect #1 to be the common case for development environment, #2 - for deployment environment, #3 - for illustrations (for sake of brevity) or for some lucky conditions. + +#### Environment variables + +If `--server` parameter not specified `als_query.py` attempts to use environment variables: + +- `POSTGRES_LOG_USER`, `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_LOG_PASSWORD` for accessing *AFC_LOGS* database +- `POSTGRES_ALS_USER`, `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_ALS_PASSWORD` for accessing *ALS* database + +These environment variables are passed to container with `als_siphon.py`, so they are quite natural choice when running `als_query` from there (case #1 above). + +Hence for case #1 `als_query.py` command line would actually look like this: +`$ docker exec SIPHON_CONTAINER als_query.py CMD ...` +Where `...` does not contain `--server` + + +### `log` Command + +`log` command retrieves JSON logs from *AFC_LOGS* database. Each JSON logs is belongs to certain *topic* (handy term, originated from Kafka). Topic is a string (***lowercase highly recommended, 'ALS' name must not be used***) that supposedly corresponds to format (content) of JSON data. + +Topic specifies a name of table inside *AFC_LOGS* database. + +Since content of JSON may be any and PostgreSQL already provides the special 'SELECT' syntax for accessing JSON data (see e.g. [here](https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-json/) and [here](https://www.javatpoint.com/postgresql-json), google for further assistance), `log` command is, in fact, thin wrapper around `SELECT` command, plus a couple of additional options. + +Each table in *AFC_LOGS* has the following columns (this is important when composing `SELECT` statements): + +|Column|Content| +|------|-------| +|time|Time when log record was made in (includes date, time, timezone)| +|source|Entity (e.g. WEB server) that made record| +|log|JSON log data| + +Command format: +`$ [docker exec SIPHON_CONTAINER] als_query.py log OPTIONS [SELECT_BODY]` + +|Parameter|Meaning| +|---------|-------| +|--server/-s **[USER@][^]HOST_OR_CONTAINER[:PORT][?OPTIONS]**|PostgreSQL server connection parameters. See discussion in [Installing and running](#als_query_deploy) chapter. This parameter is mandatory| +|--password **PASSWORD**|PostgreSQL connection password (if required)| +|--topics|List existing topics (database tables)| +|--sources **[TOPIC]**|List sources - all or from specific topic| +|--format/-f **{bare\|json\|csv}**|Output format for SELECT-based queries: **bare** - unadorned single column output, **csv** - output as CSV table (default), **json** - output as JSON list or row dictionaries| +|**SELECT_BODY**|SQL SELECT statement body (without leading `SELECT` and trailing `;`. May be unquoted, but most likely requires quotes because of special symbols like `*`, `>`, etc.| + +#### `log` Command Examples + +Suppose that: + +- There are various topics (tables), among which there is topic *few* (let me remind again, that lowercase topic names are recommended), filled with JSONs with structure similar to this: +``` +{ + "a": 42, + "b": [1, 2, 3], + "c": {"d": 57} +} +``` + +- `als_query.py` runs in `regression_als_siphon_1` container (YMMV - see output of `docker-compose ps`). In this case there is no need to pass `--server` parameter, as it will be taken from environment variables. + +Now, here are some possible actions: + +- List all topics: + `$ docker exec regression_als_siphon_1 als_query.py log --topics` + Note that there is no `--server` parameter here, as `als_query.py` would values, passed over environment variables. + +- Print content of *foo* topic (table) in its entirety, using CSV format: + `$ docker exec regression_als_siphon_1 als_query.py log "* from foo"` + This invokes `SELECT * from foo;` on *AFC_LOGS* database of PostgreSQL server. + +- Print key names of JSONs of topic *foo*: + `$ docker exec regression_als_siphon_1 als_query.py log \ ` + `json_object_keys(log) from foo` + Note that quotes may be omitted here, as there are no special symbols in select statement. + +- From topic *foo* print values of *c.d* for all records, using bare (unadorned) format: + `$ docker exec regression_als_siphon_1 als_query.py log \ ` + `-f bare "log->'c'->'d' from foo"` + Note the quotes around field names + +- From topic *foo* print only values of *b[0]* for all records where *a* field equals *179*: + `$ docker exec regression_als_siphon_1 als_query.py log \ ` + `"log->'b'->0 from foo where log->'a' = 179"` + Note the way list indexing is performed (`->0`). + +- Print maximum value of column *a* in topic *foo*: + `$ docker exec regression_als_siphon_1 als_query.py log "MAX(log->'a') from foo"` + +- Print log records in given time range: + `$ docker exec regression_als_siphon_1 als_query.py log \ ` + `"* from foo where time > '2023-02-08 23:25:54.484174+00:00'" \ ` + `"and time < '2023-02-08 23:28:54.484174+00:00'"` diff --git a/als/als_db_schema/ALS.dbml b/als/als_db_schema/ALS.dbml new file mode 100644 index 0000000..541899c --- /dev/null +++ b/als/als_db_schema/ALS.dbml @@ -0,0 +1,290 @@ +// Copyright (C) 2022 Broadcom. All rights reserved. +// The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +// that owns the software below. +// This work is licensed under the OpenAFC Project License, a copy of which is +// included with this software program. + +// Schema of AFC Request/response/config log database +// This file is a DBML source code for visualizing database scheme in dbdiagram.io + +// Table, containing a record for each request messgae/response message/config(s) set +table afc_message [headercolor: #000000, note: 'AFC Request/Response message pair (contain individual requests/responses)'] { + message_id bigserial + month_idx smallint + afc_server serial + rx_time timestamptz + tx_time timestamptz + rx_envelope_digest uuid [note: 'Envelope of AFC Request message'] + tx_envelope_digest uuid [note: 'Envelope of AFC Response message'] + + indexes { + (message_id, month_idx) [pk] + rx_time + tx_time + } +} + +Ref afc_message_afc_server_ref: afc_message.(afc_server, month_idx) > afc_server.(afc_server_id, month_idx) +Ref afc_message_rx_envelope_digest_ref: afc_message.(rx_envelope_digest, month_idx) > rx_envelope.(rx_envelope_digest, month_idx) +Ref afc_message_tx_envelope_digest_ref: afc_message.(tx_envelope_digest, month_idx) > tx_envelope.(tx_envelope_digest, month_idx) + +// Outer part of request message +table rx_envelope [headercolor: #4B82B0, note: 'Envelope (constant part) of AFC Request Message'] { + rx_envelope_digest uuid [note: 'MD5 of envelope_json field in UTF8 encoding'] + month_idx smallint + envelope_json json [note: 'AFC Request JSON with empty availableSpectrumInquiryRequests field'] + + indexes { + (rx_envelope_digest, month_idx) [pk] + rx_envelope_digest [type: hash] + } +} + +// Outer part of response message +table tx_envelope [headercolor: #4B82B0, note: 'Envelope (constant part) of AFC Response Message'] { + tx_envelope_digest uuid [note: 'MD5 of envelope_json field in UTF8 encoding'] + month_idx smallint + envelope_json json [note: 'AFC Response JSON with empty availableSpectrumInquiryRequests field'] + + indexes { + (tx_envelope_digest, month_idx) [pk] + tx_envelope_digest [type: hash] + } +} + +// Join table between message table (afc_message) and request/response/config table (request_response) +// Implement smany-to-many relationship, contains variable part of request/response +table request_response_in_message [headercolor: #4B82B0, note: 'Associative table for relatonship between AFC Request/Response messages and individual requests/responses. Also encapsulates variable part of requests/responses'] { + message_id bigint [note: 'AFC request/response message pair this request/response belongs'] + request_id text [note: 'ID of request/response within message'] + month_idx smallint + request_response_digest uuid [note: 'Reference to otentially constant part of request/response'] + expire_time timestamptz [note: 'Response expiration time'] + + indexes { + (message_id, request_id, month_idx) [pk] + request_id + request_response_digest + expire_time + } +} + +Ref request_response_in_message_message_id_ref: request_response_in_message.(message_id, month_idx) > afc_message.(message_id, month_idx) +Ref request_response_in_message_request_response_digest_ref: request_response_in_message.(request_response_digest, month_idx) > request_response.(request_response_digest, month_idx) + +// Request/response/config - constant part +table request_response [headercolor: #2D6512, note: 'Potentiially constant part of request/response'] { + request_response_digest uuid [note: 'MD5 computed over request/response with requestId and availabilityExpireTime fields set to empty'] + month_idx smallint + afc_config_text_digest uuid [note: 'MD5 over used AFC Config text represnetation'] + customer_id integer [note: 'AP vendor'] + uls_data_version_id integer [note: 'Version of used ULS data'] + geo_data_version_id integer [note: 'Version of used geospatial data'] + request_json_digest uuid [note: 'MD5 of request JSON with empty requestId'] + response_json_digest uuid [note: 'MD5 of resaponse JSON with empty requesatId and availabilityExpireTime'] + device_descriptor_digest uuid [note: 'MD5 of device descriptor (AP) related part of request JSON'] + location_digest uuid [note: 'MD5 of location-related part of request JSON'] + response_code int + response_description text [note: 'Optional response code short description. Null for success'] + response_data text [note: 'Optional supplemental failure information. Optional comma-separated list of missing/invalid/unexpected parameters, etc.'] + + indexes { + (request_response_digest, month_idx) [pk] + request_response_digest [type: hash] + afc_config_text_digest + customer_id + device_descriptor_digest + location_digest + response_code + response_description + response_data + } +} + +Ref request_response_afc_config_text_digest_ref: request_response.(afc_config_text_digest, month_idx) > afc_config.(afc_config_text_digest, month_idx) +Ref request_response_customer_id_ref: request_response.(customer_id, month_idx) > customer.(customer_id, month_idx) +Ref request_response_uls_data_version_id_ref: request_response.(uls_data_version_id, month_idx) > uls_data_version.(uls_data_version_id, month_idx) +Ref request_response_geo_data_version_id_ref: request_response.(geo_data_version_id, month_idx) > geo_data_version.(geo_data_version_id, month_idx) +Ref request_response_request_json_digest_ref: request_response.(request_json_digest, month_idx) > compressed_json.(compressed_json_digest, month_idx) +Ref request_response_response_json_digest_ref: request_response.(response_json_digest, month_idx) > compressed_json.(compressed_json_digest, month_idx) +Ref request_response_device_descriptor_digest_ref: request_response.(device_descriptor_digest, month_idx) > device_descriptor.(device_descriptor_digest, month_idx) +Ref request_response_location_digest_ref: request_response.(location_digest, month_idx) > location.(location_digest, month_idx) + +// AP device descriptor +table device_descriptor [headercolor: #2D6512, note: 'Information about device (e.g. AP)'] { + device_descriptor_digest uuid [note: 'MD5 over parts of requesat JSON pertinent to AP'] + month_idx smallint + serial_number text [note: 'AP serial number'] + certifications_digest uuid [note: 'Device certifications'] + + indexes { + (device_descriptor_digest, month_idx) [pk] + device_descriptor_digest [type: hash] + serial_number + certifications_digest + } +} + +Ref device_descriptor_certifications_digest_ref: device_descriptor.(certifications_digest, month_idx) <> certification.(certifications_digest, month_idx) + +// Single certification +table certification [headercolor: #79AD51, note: 'Element of certifications list'] { + certifications_digest uuid [note: 'MD5 of certification list json'] + certification_index smallint [note: 'Index in certification list'] + month_idx smallint + ruleset_id text [note: 'Name of rules for which AP certified (equivalent of region)'] + certification_id text [note: 'ID of certification (equivalent of manufacturer)'] + + indexes { + (certifications_digest, certification_index, month_idx) [pk] + certifications_digest [type: hash] + ruleset_id + certification_id + } +} + +// Compressed text of constant part of request or response +table compressed_json [headercolor: #2D6512, note: 'Compressed body of request or response'] { + compressed_json_digest uuid [note: 'MD5 hash of compressed data'] + month_idx smallint + compressed_json_data bytea [note: 'Compressed data'] + + indexes { + (compressed_json_digest, month_idx) [pk] + compressed_json_digest [type: hash] + } +} + +// Customer information +table customer [headercolor: #79AD51, note: 'Customer aka vendor aka user'] { + customer_id serial + month_idx smallint + customer_name text [note: 'Its name'] + + indexes { + (customer_id, month_idx) [pk] + customer_name + } +} + +// AP location information +table location [headercolor: #2D6512, note: 'AP location'] { + location_digest uuid [note: 'MD5 computed over location part of request JSON'] + month_idx smallint + location_wgs84 geography(POINT,4326) [note: 'AP area center (WGS84 coordinates)'] + location_uncertainty_m real [note: 'Radius of AP uncertainty area in meters'] + location_type text [note: 'Ellipse/LinearPolygon/RadialPolygon'] + deployment_type int [note: '0/1/2 for unknown/indoor/outdoor'] + height_m real [note: 'AP elevation in meters'] + height_uncertainty_m real [note: 'Elevation uncertainty in meters'] + height_type text [note: 'Elevation type'] + + indexes { + (location_digest, month_idx) [pk] + location_digest [type: hash] + location_wgs84 + location_type + height_m + height_type + } +} + +// AFC Config +table afc_config [headercolor: #79AD51, note: 'AFC Config'] { + afc_config_text_digest uuid [note: 'MD5 computed over text representation'] + month_idx smallint + afc_config_text text [note: 'Text representation of AFC Config'] + afc_config_json json [note: 'JSON representation of AFC Config'] + + indexes { + (afc_config_text_digest, month_idx) [pk] + afc_config_text_digest [type: hash] + } +} + +// Geodetic data version +table geo_data_version [headercolor: #79AD51, note: 'Version of geospatial data'] { + geo_data_version_id serial + month_idx smallint + geo_data_version text + + indexes { + (geo_data_version_id, month_idx) [pk] + (geo_data_version, month_idx) [unique] + geo_data_version + } +} + +// ULS data version +table uls_data_version [headercolor: #79AD51, note: "Version of ULS data'"] { + uls_data_version_id serial + month_idx smallint + uls_data_version text + + indexes { + (uls_data_version_id, month_idx) [pk] + (uls_data_version, month_idx) [unique] + uls_data_version + } +} + +// PSD result +table max_psd [headercolor: #990D0D, note: 'PSD result'] { + request_response_digest uuid [note: 'Request this result belongs to'] + month_idx smallint + low_frequency_mhz smallint + high_frequency_mhz smallint + max_psd_dbm_mhz real + + indexes { + (request_response_digest, month_idx, low_frequency_mhz, high_frequency_mhz) [pk] + request_response_digest [type: hash] + low_frequency_mhz + high_frequency_mhz + max_psd_dbm_mhz + } +} + +Ref max_psd_request_response_digest_ref: max_psd.(request_response_digest, month_idx) > request_response.(request_response_digest, month_idx) + +// EIRP result +table max_eirp [headercolor: #990D0D, note: 'EIRP result'] { + request_response_digest uuid [note: 'Request this result belongs to'] + month_idx smallint + op_class smallint + channel smallint + max_eirp_dbm real + + indexes { + (request_response_digest, month_idx, op_class, channel) [pk] + request_response_digest [type: hash] + op_class + channel + max_eirp_dbm + } +} + +Ref max_eirp_request_response_digest_ref: max_eirp.(request_response_digest, month_idx) > request_response.(request_response_digest, month_idx) + +// AFC Server +table afc_server [headercolor: #4B82B0] { + afc_server_id serial + month_idx smallint + afc_server_name text + + indexes { + (afc_server_id, month_idx) [pk] + (afc_server_name, month_idx) [unique] + afc_server_name + } +} + +// Message decoding problems +table decode_error { + id bigserial [pk] + time timestamptz + msg text + code_line integer + data text + month_idx smallint +} diff --git a/als/als_db_schema/ALS.png b/als/als_db_schema/ALS.png new file mode 100644 index 0000000..f9986f7 Binary files /dev/null and b/als/als_db_schema/ALS.png differ diff --git a/als/als_db_schema/ALS_raw.sql b/als/als_db_schema/ALS_raw.sql new file mode 100644 index 0000000..b31d165 --- /dev/null +++ b/als/als_db_schema/ALS_raw.sql @@ -0,0 +1,397 @@ +CREATE TABLE "afc_message" ( + "message_id" bigserial, + "month_idx" smallint, + "afc_server" serial, + "rx_time" timestamptz, + "tx_time" timestamptz, + "rx_envelope_digest" uuid, + "tx_envelope_digest" uuid, + PRIMARY KEY ("message_id", "month_idx") +); + +CREATE TABLE "rx_envelope" ( + "rx_envelope_digest" uuid, + "month_idx" smallint, + "envelope_json" json, + PRIMARY KEY ("rx_envelope_digest", "month_idx") +); + +CREATE TABLE "tx_envelope" ( + "tx_envelope_digest" uuid, + "month_idx" smallint, + "envelope_json" json, + PRIMARY KEY ("tx_envelope_digest", "month_idx") +); + +CREATE TABLE "request_response_in_message" ( + "message_id" bigint, + "request_id" text, + "month_idx" smallint, + "request_response_digest" uuid, + "expire_time" timestamptz, + PRIMARY KEY ("message_id", "request_id", "month_idx") +); + +CREATE TABLE "request_response" ( + "request_response_digest" uuid, + "month_idx" smallint, + "afc_config_text_digest" uuid, + "customer_id" integer, + "uls_data_version_id" integer, + "geo_data_version_id" integer, + "request_json_digest" uuid, + "response_json_digest" uuid, + "device_descriptor_digest" uuid, + "location_digest" uuid, + "response_code" int, + "response_description" text, + "response_data" text, + PRIMARY KEY ("request_response_digest", "month_idx") +); + +CREATE TABLE "device_descriptor" ( + "device_descriptor_digest" uuid, + "month_idx" smallint, + "serial_number" text, + "certifications_digest" uuid, + PRIMARY KEY ("device_descriptor_digest", "month_idx") +); + +CREATE TABLE "certification" ( + "certifications_digest" uuid, + "certification_index" smallint, + "month_idx" smallint, + "ruleset_id" text, + "certification_id" text, + PRIMARY KEY ("certifications_digest", "certification_index", "month_idx") +); + +CREATE TABLE "compressed_json" ( + "compressed_json_digest" uuid, + "month_idx" smallint, + "compressed_json_data" bytea, + PRIMARY KEY ("compressed_json_digest", "month_idx") +); + +CREATE TABLE "customer" ( + "customer_id" serial, + "month_idx" smallint, + "customer_name" text, + PRIMARY KEY ("customer_id", "month_idx") +); + +CREATE TABLE "location" ( + "location_digest" uuid, + "month_idx" smallint, + "location_wgs84" geography(POINT,4326), + "location_uncertainty_m" real, + "location_type" text, + "deployment_type" int, + "height_m" real, + "height_uncertainty_m" real, + "height_type" text, + PRIMARY KEY ("location_digest", "month_idx") +); + +CREATE TABLE "afc_config" ( + "afc_config_text_digest" uuid, + "month_idx" smallint, + "afc_config_text" text, + "afc_config_json" json, + PRIMARY KEY ("afc_config_text_digest", "month_idx") +); + +CREATE TABLE "geo_data_version" ( + "geo_data_version_id" serial, + "month_idx" smallint, + "geo_data_version" text, + PRIMARY KEY ("geo_data_version_id", "month_idx") +); + +CREATE TABLE "uls_data_version" ( + "uls_data_version_id" serial, + "month_idx" smallint, + "uls_data_version" text, + PRIMARY KEY ("uls_data_version_id", "month_idx") +); + +CREATE TABLE "max_psd" ( + "request_response_digest" uuid, + "month_idx" smallint, + "low_frequency_mhz" smallint, + "high_frequency_mhz" smallint, + "max_psd_dbm_mhz" real, + PRIMARY KEY ("request_response_digest", "month_idx", "low_frequency_mhz", "high_frequency_mhz") +); + +CREATE TABLE "max_eirp" ( + "request_response_digest" uuid, + "month_idx" smallint, + "op_class" smallint, + "channel" smallint, + "max_eirp_dbm" real, + PRIMARY KEY ("request_response_digest", "month_idx", "op_class", "channel") +); + +CREATE TABLE "afc_server" ( + "afc_server_id" serial, + "month_idx" smallint, + "afc_server_name" text, + PRIMARY KEY ("afc_server_id", "month_idx") +); + +CREATE TABLE "decode_error" ( + "id" bigserial PRIMARY KEY, + "time" timestamptz, + "msg" text, + "code_line" integer, + "data" text, + "month_idx" smallint +); + +CREATE INDEX ON "afc_message" ("rx_time"); + +CREATE INDEX ON "afc_message" ("tx_time"); + +CREATE INDEX ON "rx_envelope" USING HASH ("rx_envelope_digest"); + +CREATE INDEX ON "tx_envelope" USING HASH ("tx_envelope_digest"); + +CREATE INDEX ON "request_response_in_message" ("request_id"); + +CREATE INDEX ON "request_response_in_message" ("request_response_digest"); + +CREATE INDEX ON "request_response_in_message" ("expire_time"); + +CREATE INDEX ON "request_response" USING HASH ("request_response_digest"); + +CREATE INDEX ON "request_response" ("afc_config_text_digest"); + +CREATE INDEX ON "request_response" ("customer_id"); + +CREATE INDEX ON "request_response" ("device_descriptor_digest"); + +CREATE INDEX ON "request_response" ("location_digest"); + +CREATE INDEX ON "request_response" ("response_code"); + +CREATE INDEX ON "request_response" ("response_description"); + +CREATE INDEX ON "request_response" ("response_data"); + +CREATE INDEX ON "device_descriptor" USING HASH ("device_descriptor_digest"); + +CREATE INDEX ON "device_descriptor" ("serial_number"); + +CREATE INDEX ON "device_descriptor" ("certifications_digest"); + +CREATE INDEX ON "certification" USING HASH ("certifications_digest"); + +CREATE INDEX ON "certification" ("ruleset_id"); + +CREATE INDEX ON "certification" ("certification_id"); + +CREATE INDEX ON "compressed_json" USING HASH ("compressed_json_digest"); + +CREATE INDEX ON "customer" ("customer_name"); + +CREATE INDEX ON "location" USING HASH ("location_digest"); + +CREATE INDEX ON "location" ("location_wgs84"); + +CREATE INDEX ON "location" ("location_type"); + +CREATE INDEX ON "location" ("height_m"); + +CREATE INDEX ON "location" ("height_type"); + +CREATE INDEX ON "afc_config" USING HASH ("afc_config_text_digest"); + +CREATE UNIQUE INDEX ON "geo_data_version" ("geo_data_version", "month_idx"); + +CREATE INDEX ON "geo_data_version" ("geo_data_version"); + +CREATE UNIQUE INDEX ON "uls_data_version" ("uls_data_version", "month_idx"); + +CREATE INDEX ON "uls_data_version" ("uls_data_version"); + +CREATE INDEX ON "max_psd" USING HASH ("request_response_digest"); + +CREATE INDEX ON "max_psd" ("low_frequency_mhz"); + +CREATE INDEX ON "max_psd" ("high_frequency_mhz"); + +CREATE INDEX ON "max_psd" ("max_psd_dbm_mhz"); + +CREATE INDEX ON "max_eirp" USING HASH ("request_response_digest"); + +CREATE INDEX ON "max_eirp" ("op_class"); + +CREATE INDEX ON "max_eirp" ("channel"); + +CREATE INDEX ON "max_eirp" ("max_eirp_dbm"); + +CREATE UNIQUE INDEX ON "afc_server" ("afc_server_name", "month_idx"); + +CREATE INDEX ON "afc_server" ("afc_server_name"); + +COMMENT ON TABLE "afc_message" IS 'AFC Request/Response message pair (contain individual requests/responses)'; + +COMMENT ON COLUMN "afc_message"."rx_envelope_digest" IS 'Envelope of AFC Request message'; + +COMMENT ON COLUMN "afc_message"."tx_envelope_digest" IS 'Envelope of AFC Response message'; + +COMMENT ON TABLE "rx_envelope" IS 'Envelope (constant part) of AFC Request Message'; + +COMMENT ON COLUMN "rx_envelope"."rx_envelope_digest" IS 'MD5 of envelope_json field in UTF8 encoding'; + +COMMENT ON COLUMN "rx_envelope"."envelope_json" IS 'AFC Request JSON with empty availableSpectrumInquiryRequests field'; + +COMMENT ON TABLE "tx_envelope" IS 'Envelope (constant part) of AFC Response Message'; + +COMMENT ON COLUMN "tx_envelope"."tx_envelope_digest" IS 'MD5 of envelope_json field in UTF8 encoding'; + +COMMENT ON COLUMN "tx_envelope"."envelope_json" IS 'AFC Response JSON with empty availableSpectrumInquiryRequests field'; + +COMMENT ON TABLE "request_response_in_message" IS 'Associative table for relatonship between AFC Request/Response messages and individual requests/responses. Also encapsulates variable part of requests/responses'; + +COMMENT ON COLUMN "request_response_in_message"."message_id" IS 'AFC request/response message pair this request/response belongs'; + +COMMENT ON COLUMN "request_response_in_message"."request_id" IS 'ID of request/response within message'; + +COMMENT ON COLUMN "request_response_in_message"."request_response_digest" IS 'Reference to otentially constant part of request/response'; + +COMMENT ON COLUMN "request_response_in_message"."expire_time" IS 'Response expiration time'; + +COMMENT ON TABLE "request_response" IS 'Potentiially constant part of request/response'; + +COMMENT ON COLUMN "request_response"."request_response_digest" IS 'MD5 computed over request/response with requestId and availabilityExpireTime fields set to empty'; + +COMMENT ON COLUMN "request_response"."afc_config_text_digest" IS 'MD5 over used AFC Config text represnetation'; + +COMMENT ON COLUMN "request_response"."customer_id" IS 'AP vendor'; + +COMMENT ON COLUMN "request_response"."uls_data_version_id" IS 'Version of used ULS data'; + +COMMENT ON COLUMN "request_response"."geo_data_version_id" IS 'Version of used geospatial data'; + +COMMENT ON COLUMN "request_response"."request_json_digest" IS 'MD5 of request JSON with empty requestId'; + +COMMENT ON COLUMN "request_response"."response_json_digest" IS 'MD5 of resaponse JSON with empty requesatId and availabilityExpireTime'; + +COMMENT ON COLUMN "request_response"."device_descriptor_digest" IS 'MD5 of device descriptor (AP) related part of request JSON'; + +COMMENT ON COLUMN "request_response"."location_digest" IS 'MD5 of location-related part of request JSON'; + +COMMENT ON COLUMN "request_response"."response_description" IS 'Optional response code short description. Null for success'; + +COMMENT ON COLUMN "request_response"."response_data" IS 'Optional supplemental failure information. Optional comma-separated list of missing/invalid/unexpected parameters, etc.'; + +COMMENT ON TABLE "device_descriptor" IS 'Information about device (e.g. AP)'; + +COMMENT ON COLUMN "device_descriptor"."device_descriptor_digest" IS 'MD5 over parts of requesat JSON pertinent to AP'; + +COMMENT ON COLUMN "device_descriptor"."serial_number" IS 'AP serial number'; + +COMMENT ON COLUMN "device_descriptor"."certifications_digest" IS 'Device certifications'; + +COMMENT ON TABLE "certification" IS 'Element of certifications list'; + +COMMENT ON COLUMN "certification"."certifications_digest" IS 'MD5 of certification list json'; + +COMMENT ON COLUMN "certification"."certification_index" IS 'Index in certification list'; + +COMMENT ON COLUMN "certification"."ruleset_id" IS 'Name of rules for which AP certified (equivalent of region)'; + +COMMENT ON COLUMN "certification"."certification_id" IS 'ID of certification (equivalent of manufacturer)'; + +COMMENT ON TABLE "compressed_json" IS 'Compressed body of request or response'; + +COMMENT ON COLUMN "compressed_json"."compressed_json_digest" IS 'MD5 hash of compressed data'; + +COMMENT ON COLUMN "compressed_json"."compressed_json_data" IS 'Compressed data'; + +COMMENT ON TABLE "customer" IS 'Customer aka vendor aka user'; + +COMMENT ON COLUMN "customer"."customer_name" IS 'Its name'; + +COMMENT ON TABLE "location" IS 'AP location'; + +COMMENT ON COLUMN "location"."location_digest" IS 'MD5 computed over location part of request JSON'; + +COMMENT ON COLUMN "location"."location_wgs84" IS 'AP area center (WGS84 coordinates)'; + +COMMENT ON COLUMN "location"."location_uncertainty_m" IS 'Radius of AP uncertainty area in meters'; + +COMMENT ON COLUMN "location"."location_type" IS 'Ellipse/LinearPolygon/RadialPolygon'; + +COMMENT ON COLUMN "location"."deployment_type" IS '0/1/2 for unknown/indoor/outdoor'; + +COMMENT ON COLUMN "location"."height_m" IS 'AP elevation in meters'; + +COMMENT ON COLUMN "location"."height_uncertainty_m" IS 'Elevation uncertainty in meters'; + +COMMENT ON COLUMN "location"."height_type" IS 'Elevation type'; + +COMMENT ON TABLE "afc_config" IS 'AFC Config'; + +COMMENT ON COLUMN "afc_config"."afc_config_text_digest" IS 'MD5 computed over text representation'; + +COMMENT ON COLUMN "afc_config"."afc_config_text" IS 'Text representation of AFC Config'; + +COMMENT ON COLUMN "afc_config"."afc_config_json" IS 'JSON representation of AFC Config'; + +COMMENT ON TABLE "geo_data_version" IS 'Version of geospatial data'; + +COMMENT ON TABLE "uls_data_version" IS 'Version of ULS data"'; + +COMMENT ON TABLE "max_psd" IS 'PSD result'; + +COMMENT ON COLUMN "max_psd"."request_response_digest" IS 'Request this result belongs to'; + +COMMENT ON TABLE "max_eirp" IS 'EIRP result'; + +COMMENT ON COLUMN "max_eirp"."request_response_digest" IS 'Request this result belongs to'; + +ALTER TABLE "afc_message" ADD CONSTRAINT "afc_message_afc_server_ref" FOREIGN KEY ("afc_server", "month_idx") REFERENCES "afc_server" ("afc_server_id", "month_idx"); + +ALTER TABLE "afc_message" ADD CONSTRAINT "afc_message_rx_envelope_digest_ref" FOREIGN KEY ("rx_envelope_digest", "month_idx") REFERENCES "rx_envelope" ("rx_envelope_digest", "month_idx"); + +ALTER TABLE "afc_message" ADD CONSTRAINT "afc_message_tx_envelope_digest_ref" FOREIGN KEY ("tx_envelope_digest", "month_idx") REFERENCES "tx_envelope" ("tx_envelope_digest", "month_idx"); + +ALTER TABLE "request_response_in_message" ADD CONSTRAINT "request_response_in_message_message_id_ref" FOREIGN KEY ("message_id", "month_idx") REFERENCES "afc_message" ("message_id", "month_idx"); + +ALTER TABLE "request_response_in_message" ADD CONSTRAINT "request_response_in_message_request_response_digest_ref" FOREIGN KEY ("request_response_digest", "month_idx") REFERENCES "request_response" ("request_response_digest", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_afc_config_text_digest_ref" FOREIGN KEY ("afc_config_text_digest", "month_idx") REFERENCES "afc_config" ("afc_config_text_digest", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_customer_id_ref" FOREIGN KEY ("customer_id", "month_idx") REFERENCES "customer" ("customer_id", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_uls_data_version_id_ref" FOREIGN KEY ("uls_data_version_id", "month_idx") REFERENCES "uls_data_version" ("uls_data_version_id", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_geo_data_version_id_ref" FOREIGN KEY ("geo_data_version_id", "month_idx") REFERENCES "geo_data_version" ("geo_data_version_id", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_request_json_digest_ref" FOREIGN KEY ("request_json_digest", "month_idx") REFERENCES "compressed_json" ("compressed_json_digest", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_response_json_digest_ref" FOREIGN KEY ("response_json_digest", "month_idx") REFERENCES "compressed_json" ("compressed_json_digest", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_device_descriptor_digest_ref" FOREIGN KEY ("device_descriptor_digest", "month_idx") REFERENCES "device_descriptor" ("device_descriptor_digest", "month_idx"); + +ALTER TABLE "request_response" ADD CONSTRAINT "request_response_location_digest_ref" FOREIGN KEY ("location_digest", "month_idx") REFERENCES "location" ("location_digest", "month_idx"); + +CREATE TABLE "device_descriptor_certification" ( + "device_descriptor_certifications_digest" uuid, + "device_descriptor_month_idx" smallint, + "certification_certifications_digest" uuid, + "certification_month_idx" smallint, + PRIMARY KEY ("device_descriptor_certifications_digest", "device_descriptor_month_idx", "certification_certifications_digest", "certification_month_idx") +); + +ALTER TABLE "device_descriptor_certification" ADD FOREIGN KEY ("device_descriptor_certifications_digest", "device_descriptor_month_idx") REFERENCES "device_descriptor" ("certifications_digest", "month_idx"); + +ALTER TABLE "device_descriptor_certification" ADD FOREIGN KEY ("certification_certifications_digest", "certification_month_idx") REFERENCES "certification" ("certifications_digest", "month_idx"); + + +ALTER TABLE "max_psd" ADD CONSTRAINT "max_psd_request_response_digest_ref" FOREIGN KEY ("request_response_digest", "month_idx") REFERENCES "request_response" ("request_response_digest", "month_idx"); + +ALTER TABLE "max_eirp" ADD CONSTRAINT "max_eirp_request_response_digest_ref" FOREIGN KEY ("request_response_digest", "month_idx") REFERENCES "request_response" ("request_response_digest", "month_idx"); diff --git a/als/als_db_schema/als_rectifier.awk b/als/als_db_schema/als_rectifier.awk new file mode 100644 index 0000000..c98c33f --- /dev/null +++ b/als/als_db_schema/als_rectifier.awk @@ -0,0 +1,35 @@ +#!/bin/awk -f + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +BEGIN { + print "/*" + print " * Copyright (C) 2022 Broadcom. All rights reserved." + print " * The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate" + print " * that owns the software below." + print " * This work is licensed under the OpenAFC Project License, a copy of which is" + print " * included with this software program." + print " *" + print " * This file creates ALS (AFC Request/Response/Config Logging System) database on PostgreSQL+PostGIS server" + print " * This file is generated, direct editing is not recommended." + print " * Intended maintenance sequence is as follows:" + print " * 1. Load (copypaste) als_db_schema/ALS.dbml into dbdiagram.io" + print " * 2. Modify as needed" + print " * 3. Save (copypaste) modified sources back to als_db_schema/ALS.dbml" + print " * 4. Also export schema in PostgreSQL format as als_db_schema/ALS_raw.sql" + print " * 5. Rectify exported schema with als_rectifier.awk (awk -f als_db_schema/als_rectifier.awk < als_db_schema/ALS_raw.sql > ALS.sql)" + print " */" + print "" + print "CREATE EXTENSION postgis;" + print "" + RS=ORS=";" +} + +/\w+ TABLE \"device_descriptor_certification\"/ {next} +/\w+ TABLE \"device_descriptor_regulatory_rule\"/ {next} + +{ print } \ No newline at end of file diff --git a/als/als_query.py b/als/als_query.py new file mode 100755 index 0000000..285377c --- /dev/null +++ b/als/als_query.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +# Tool for querying ALS database + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +import argparse +import csv +import datetime +import json +import logging +import os +import psycopg2 +import re +import sqlalchemy as sa +import sqlalchemy.dialects.postgresql as sa_pg +import subprocess +import sys +from typing import Any, List, NamedTuple, Optional, Set + +try: + import geoalchemy2 as ga # type: ignore +except ImportError: + pass + +VERSION = "0.1" + +DEFAULT_USER = "postgres" +DEFAULT_PORT = 5432 + +ALS_DB = "ALS" +LOG_DB = "AFC_LOGS" + +# Environment variables holding parts of database connection string +DbEnv = \ + NamedTuple( + "DbEnv", + [ + # Host name + ("host", str), + # Port + ("port", str), + # Username + ("user", str), + # Password + ("password", str), + # Options + ("options", str)]) + +# Environment variable names for ALS and JSON log databases' connection strings +DB_ENVS = {ALS_DB: + DbEnv(host="POSTGRES_HOST", port="POSTGRES_PORT", + user="POSTGRES_ALS_USER", password="POSTGRES_ALS_PASSWORD", + options="POSTGRES_ALS_OPTIONS"), + LOG_DB: + DbEnv(host="POSTGRES_HOST", port="POSTGRES_PORT", + user="POSTGRES_LOG_USER", password="POSTGRES_LOG_PASSWORD", + options="POSTGRES_LOG_OPTIONS")} + + +def error(msg: str) -> None: + """ Prints given msg as error message and exit abnormally """ + logging.error(msg) + sys.exit(1) + + +def error_if(cond: Any, msg: str) -> None: + """ If condition evaluates to true prints given msg as error message and + exits abnormally """ + if cond: + error(msg) + + +class DbConn: + """ Database connection encapsulation + + Attributes: + db_name -- Database name + engine -- Database engine + metadata -- Database metadata + conn -- Database connection + """ + + def __init__(self, conn_str: Optional[str], password: Optional[str], + db_name: str) -> None: + """ Constructor + + Arguments: + conn_str -- Abbreviated conneftion string, as specified in command + line. None means take from environment variable + password -- Optional password + db_name -- Database name + """ + self.db_name = db_name + + if conn_str: + m = re.match( + r"^(?P[^ :\?]+@)?" + r"(?P\^)?(?P[^ :?]+)" + r"(:(?P\d+))?" + r"(?P\?.+)?$", + conn_str) + error_if(not m, f"Server string '{conn_str}' has invalid format") + assert m is not None + + user = m.group("user") or DEFAULT_USER + host = m.group("host") + port = m.group("port") or str(DEFAULT_PORT) + options = m.group("options") or "" + if m.group("cont"): + try: + insp_str = \ + subprocess.check_output(["docker", "inspect", host]) + except (OSError, subprocess.CalledProcessError) as ex: + error(f"Failed to inspect container '{host}': {ex}") + insp = json.loads(insp_str) + try: + networks = insp[0]["NetworkSettings"]["Networks"] + host = networks[list(networks.keys())[0]]["IPAddress"] + except (LookupError, TypeError, ValueError) as ex: + error(f"Failed to find server IP address in container " + f"inspection: {ex}") + else: + db_env = DB_ENVS[db_name] + error_if(db_env.host not in os.environ, + f"PostgreSQL server neither specified explicitly (via " + f"--server parameter) nor via environment (via " + f"'{db_env.host}' variable and related ones)") + host = os.environ[db_env.host] + port = os.environ.get(db_env.port, str(DEFAULT_PORT)) + user = os.environ.get(db_env.user, str(DEFAULT_USER)) + options = os.environ.get(db_env.options, "") + password = password or os.environ.get(db_env.password) + try: + full_conn_str = \ + f"postgresql+psycopg2://{user}" \ + f"{(':' + password) if password else ''}@{host}:{port}/" \ + f"{db_name}{options}" + self.engine = sa.create_engine(full_conn_str) + self.metadata = sa.MetaData() + self.metadata.reflect(bind=self.engine) + self.conn = self.engine.connect() + except sa.exc.SQLAlchemyError as ex: + error(f"Failed to connect to '{db_name}' at '{conn_str}' " + f"('{full_conn_str}'): {ex}") + + +class JsonEncoder(json.JSONEncoder): + """ JSON encoder that handles unusual types """ + + def default(self, o: Any) -> Any: + """ Handles unusual data types """ + if isinstance(o, datetime.datetime): + return o.isoformat() + return super().default(o) + + +def do_log(args: Any) -> None: + """Execute "log" command. + + Arguments: + args -- Parsed command line arguments + """ + db_conn = \ + DbConn(conn_str=args.server, password=args.password, db_name=LOG_DB) + work_done = False + if args.topics: + work_done = True + for topic in sorted(db_conn.metadata.tables.keys()): + print(topic) + if args.sources is not None: + work_done = True + sources: Set[str] = set() + error_if( + args.sources and (args.sources not in db_conn.metadata.tables), + f"Topic '{args.sources}' not found") + for topic in db_conn.metadata.tables.keys(): + if "source" not in db_conn.metadata.tables[topic].c: + continue + if args.sources and (args.sources != topic): + continue + table_sources = \ + db_conn.conn.execute( + sa.text(f'SELECT DISTINCT source FROM "{topic}"')).\ + fetchall() + sources |= {s[0] for s in table_sources} + for source in sorted(sources): + print(source) + if args.SELECT: + work_done = True + try: + rp = db_conn.conn.execute( + sa.text("SELECT " + " ".join(args.SELECT))) + if args.format == "bare": + for record in rp: + error_if( + len(record) != 1, + f"Bare format assumes one field per result row " + f"(this query has {len(record)} fields per record)") + print(record[0]) + elif args.format == "json": + print("[") + for record in rp: + print(" " + json.dumps(record._asdict(), + cls=JsonEncoder)) + print("]") + elif args.format == "csv": + csv_writer = csv.writer(sys.stdout) + csv_writer.writerow(rp.keys()) + for record in rp: + csv_writer.writerow(record) + else: + error(f"Internal error: unsupported output format " + f"'{args.format}'") + except sa.exc.SQLAlchemyError as ex: + error(f"Database acces error: {ex}") + error_if(not work_done, "Nothing to do!") + + +def do_help(args: Any) -> None: + """Execute "help" command. + + Arguments: + args -- Parsed command line arguments (also contains 'argument_parser' and + 'subparsers' fields) + """ + if args.subcommand is None: + args.argument_parser.print_help() + else: + args.subparsers.choices[args.subcommand].print_help() + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + # Database connection switches + switches_server = argparse.ArgumentParser(add_help=False) + switches_server.add_argument( + "--server", "-s", metavar="[user@]{host|^container}[:port][?options]", + help=f"PostgreSQL server connection information. Host part may be a " + f"hostname, IP address, or container name or ID, preceded by '^' " + f"(specifying container name would not work if script runs inside the " + f"container). Default username is '{DEFAULT_USER}', default port is " + f"{DEFAULT_PORT}. Options may specify various (e.g. SSL-related) " + f"parameters (see " + f"https://www.postgresql.org/docs/current/libpq-connect.html" + f"#LIBPQ-CONNSTRING for details). If omitted, script tries to use " + f"data from POSTGRES_HOST, POSTGRES_PORT, POSTGRES_LOG_USER, " + f"POSTGRES_LOG_OPTIONS, POSTGRES_ALS_USER, POSTGRES_ALS_OPTIONS " + f"environment variables") + switches_server.add_argument( + "--password", metavar="PASSWORD", + help="Postgres connection password (if required). If omitted and " + "--server not specified then values from POSTGRES_LOG_PASSWORD and " + "POSTGRES_ALS_PASSWORD environment variables are used") + + # Top level parser + argument_parser = argparse.ArgumentParser( + description=f"Tool for querying ALS database. V{VERSION}") + subparsers = argument_parser.add_subparsers(dest="subcommand", + metavar="SUBCOMMAND") + + # Subparser for "log" command + parser_log = subparsers.add_parser( + "log", parents=[switches_server], + help="Read JSON log messages") + parser_log.add_argument( + "--topics", action="store_true", + help="Print list of topics, stored in database") + parser_log.add_argument( + "--sources", metavar="[TOPIC]", nargs="?", const="", + help="Print list of log sources - for all topics or for specific " + "topic") + parser_log.add_argument( + "--format", "-f", choices=["bare", "json", "csv"], default="csv", + help="Output format for 'SELECT' result. 'bare' is unadorned " + "value-per-line output (must be just one field per result row), " + "'csv' - CSV format (default), 'json' - JSON format") + + parser_log.add_argument( + "SELECT", nargs="*", + help="SELECT command body (without 'SELECT'). 'FROM' clause should " + "use topic name, column names are 'time' (timetag), 'source' (AFC or " + "whatever server ID) and 'log' (JSON log record). Surrounding quotes " + "are optional") + parser_log.set_defaults(func=do_log) + + # Subparser for 'help' command + parser_help = subparsers.add_parser( + "help", add_help=False, usage="%(prog)s subcommand", + help="Prints help on given subcommand") + parser_help.add_argument( + "subcommand", metavar="SUBCOMMAND", nargs="?", + choices=subparsers.choices, + help="Name of subcommand to print help about (use " + + "\"%(prog)s --help\" to get list of all subcommands)") + parser_help.set_defaults(func=do_help, subparsers=subparsers, + argument_parser=argument_parser) + + if not argv: + argument_parser.print_help() + sys.exit(1) + args = argument_parser.parse_args(argv) + + # Set up logging + console_handler = logging.StreamHandler() + console_handler.setFormatter( + logging.Formatter( + f"{os.path.basename(__file__)}. %(levelname)s: %(message)s")) + logging.getLogger().addHandler(console_handler) + logging.getLogger().setLevel(logging.INFO) + + # Do the needful + args.func(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/als/als_siphon.py b/als/als_siphon.py new file mode 100755 index 0000000..bc5717b --- /dev/null +++ b/als/als_siphon.py @@ -0,0 +1,3808 @@ +#!/usr/bin/env python3 +"""Tool for moving data from Kafka to PostgreSQL/PostGIS database """ + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=raise-missing-from, logging-fstring-interpolation +# pylint: disable=too-many-lines, invalid-name, consider-using-f-string +# pylint: disable=unnecessary-pass, unnecessary-ellipsis, too-many-arguments +# pylint: disable=too-many-instance-attributes, too-few-public-methods +# pylint: disable=wrong-import-order, too-many-locals, too-many-branches + +from abc import ABC, abstractmethod +import argparse +from collections.abc import Iterable +import confluent_kafka +import enum +import datetime +import dateutil.tz # type: ignore +import geoalchemy2 as ga # type: ignore +import hashlib +import heapq +import inspect +import json +import logging +import lz4.frame # type: ignore +import math +import os +import prometheus_client # type: ignore +import random +import re +import sqlalchemy as sa # type: ignore +import sqlalchemy.dialects.postgresql as sa_pg # type: ignore +import string +import sys +from typing import Any, Callable, Dict, Generic, List, NamedTuple, Optional, \ + Set, Tuple, Type, TypeVar, Union +import uuid + +# This script version +VERSION = "0.1" + +# Kafka topic for ALS logs +ALS_KAFKA_TOPIC = "ALS" + +# Type for JSON objects +JSON_DATA_TYPE = Union[Dict[str, Any], List[Any]] + +# Type for database column +COLUMN_DATA_TYPE = Optional[Union[int, float, str, bytes, bool, + datetime.datetime, uuid.UUID, Dict, List]] + +# Type for database row dictionary +ROW_DATA_TYPE = Dict[str, COLUMN_DATA_TYPE] + +# Type for database operation result row +RESULT_ROW_DATA_TYPE = Dict[int, ROW_DATA_TYPE] + +# Default port for Kafka servers +KAFKA_PORT = 9092 + +# Default Kafka server +DEFAULT_KAFKA_SERVER = f"localhost:{KAFKA_PORT}" + +# Default Kafka client ID +DEFAULT_KAFKA_CLIENT_ID = "siphon_@" + + +def dp(*args, **kwargs): + """Print debug message + + Arguments: + args -- Format and positional arguments. If latter present - formatted + with % + kwargs -- Keyword arguments. If present formatted with format() + """ + msg = args[0] if args else "" + if len(args) > 1: + msg = msg % args[1:] + if args and kwargs: + msg = msg.format(**kwargs) + fi = inspect.getframeinfo(inspect.currentframe().f_back) + timetag = datetime.datetime.now() + print( + f"DP {timetag.hour:02}:{timetag.minute:02}:{timetag.second:02}." + f"{timetag.microsecond // 1000:02} {fi.function}()@{fi.lineno}: {msg}", + flush=True) + + +def error(msg: str) -> None: + """ Prints given msg as error message and exit abnormally """ + logging.error(msg) + sys.exit(1) + + +def error_if(cond: Any, msg: str) -> None: + """ If condition evaluates to true prints given msg as error message and + exits abnormally """ + if cond: + error(msg) + + +class LineNumber: + """ Utility functions around line numbers """ + # Function names to ignore by LineNumber.exc() + _EXC_STACK_IGNORE: Set[str] = set() + + @classmethod + def exc(cls) -> Optional[int]: + """ Line number of last exception """ + def is_ignored_tb(t: Any) -> bool: + """ True if given frame should be ignored, as it is not in this + module or marked to be ignored """ + f_code = t.tb_frame.f_code + return (os.path.basename(f_code.co_filename) != + os.path.basename(__file__)) or \ + (f_code.co_name in cls._EXC_STACK_IGNORE) + + last_local_line: Optional[int] = None + tb = sys.exc_info()[2] + while tb is not None: + if not is_ignored_tb(tb): + last_local_line = tb.tb_lineno + tb = tb.tb_next + return last_local_line + + @classmethod + def current(cls) -> int: + """ Current caller's line number """ + f = inspect.currentframe() + assert (f is not None) and (f.f_back is not None) + return inspect.getframeinfo(f.f_back).lineno + + @classmethod + def stack_trace_ignore(cls, func): + """ Decorator to mark functions for ignore in exc() """ + cls._EXC_STACK_IGNORE.add(func.__name__) + return func + + +class ErrorBase(Exception): + """ Base class for exceptions in this module + + Attributes + msg -- Diagnostics message + code_line -- Source code line number + data -- Optional pertinent data - string or JSON dictionary + """ + + def __init__(self, msg: str, code_line: Optional[int], + data: Optional[Union[str, bytes, JSON_DATA_TYPE]] = None) \ + -> None: + """ Constructor + + Arguments: + msg -- Diagnostics message + code_line -- Source code line number + data -- Optional pertinent data - string or JSON dictionary + """ + super().__init__(msg) + self.msg = msg + self.code_line = code_line + self.data = data + + +class AlsProtocolError(ErrorBase): + """ Exception class for errors in ALS protocol """ + pass + + +class JsonFormatError(ErrorBase): + """ Exception class for errors in AFC response/request/config JSON data """ + pass + + +class DbFormatError(ErrorBase): + """ Exception class for DB format inconsistencies error """ + + def __init__(self, msg: str, code_line: Optional[int]) -> None: + """ Constructor + + Arguments: + msg -- Diagnostics message + code_line -- Source code line number + """ + super().__init__(msg, code_line=code_line) + + +# MYPY APPEASEMENT STUFF + +@LineNumber.stack_trace_ignore +def ms(s: Any) -> str: + """ Makes sure that given value is a string. + Asserts otherwise""" + assert isinstance(s, str) + return s + + +@LineNumber.stack_trace_ignore +def js(s: Any) -> str: + """ Makes sure that given value is a string. + raises TypeError otherwise """ + if not isinstance(s, str): + raise TypeError(f"Unexpected '{s}'. Should be string") + return s + + +@LineNumber.stack_trace_ignore +def jb(b: Any) -> bytes: + """ Makes sure that given value is a bytestring. + raises TypeError otherwise """ + if not isinstance(b, bytes): + raise TypeError(f"Unexpected '{b}'. Should be bytes") + return b + + +@LineNumber.stack_trace_ignore +def ji(i: Any) -> int: + """ Makes sure that given value is an integer. + raises TypeError otherwise """ + if not isinstance(i, int): + raise TypeError(f"Unexpected '{i}'. Should be integer") + return i + + +@LineNumber.stack_trace_ignore +def jd(d: Any) -> dict: + """ Makes sure that given value is a dictionary. + raises TypeError otherwise """ + if not isinstance(d, dict): + raise TypeError(f"Unexpected '{d}'. Should be dictionary") + return d + + +@LineNumber.stack_trace_ignore +def jod(d: Any) -> Optional[dict]: + """ Makes sure that given value is a dictionary or None. + raises TypeError otherwise """ + if not ((d is None) or isinstance(d, dict)): + raise TypeError(f"Unexpected '{d}'. Should be optional dictionary") + return d + + +@LineNumber.stack_trace_ignore +def jl(ll: Any) -> list: + """ Makes sure that given value is a list. + raises TypeError otherwise """ + if not isinstance(ll, list): + raise TypeError(f"Unexpected '{ll}'. Should be list") + return ll + + +@LineNumber.stack_trace_ignore +def ju(u: Any) -> uuid.UUID: + """ Makes sure that given value is an UUID. + raises TypeError otherwise """ + if not isinstance(u, uuid.UUID): + raise TypeError(f"Unexpected '{u}'. Should be UUID") + return u + + +@LineNumber.stack_trace_ignore +def jdt(dt: Any) -> datetime.datetime: + """ Makes sure that given value is a datetime.datetime object. + raises TypeError otherwise """ + if not isinstance(dt, datetime.datetime): + raise TypeError(f"Unexpected '{dt}'. Should be datetime") + return dt + + +def get_month_idx() -> int: + """ Computes month index """ + d = datetime.datetime.now() + return (d.year - 2022) * 12 + (d.month - 1) + + +class Metrics: + """ Wrapper around collection of Prometheus metrics + + Private attributes: + _metrics -- Dictionary of _metric objects, indexed by metric name + """ + # Value for 'id' label of all metrics + INSTANCE_ID = \ + "".join(random.choices(string.ascii_uppercase + string.digits, k=10)) + + class MetricDef(NamedTuple): + """ Metric definition """ + # Metric type ("Counter", "Summary", "Histogram" or "Info" + metric_type: str + # Metric name + name: str + # Metric description + dsc: str + # Optional list of additional labels (besides 'id') + labels: Optional[List[str]] = None + # Optional bucket list for histogram + buckets: Optional[List[float]] = None + + class _Metric: + """ Metric wrapper. + + Holds metric object, its call operator returns value that 'labels()', + of wrapped metric returns + + Private attributes: + _metric -- Metric objects + _labels -- List of additional (besides 'id') label names + """ + + def __init__(self, metric_def: "Metrics.MetricDef") -> None: + """ Constructor + + Arguments: + metric_def -- Metric definition + """ + metric_class = getattr(prometheus_client, metric_def.metric_type) + assert metric_class is not None + self._labels: List[str] = metric_def.labels or [] + kwargs: Dict[str, Any] = {} + if metric_def.buckets is not None: + kwargs["buckets"] = metric_def.buckets + self._metric = \ + metric_class(metric_def.name, metric_def.dsc, + ["id"] + self._labels, **kwargs) + + def __call__(self, *args, **kwargs) -> Any: + """ Returns output value of 'labels()' of wrapped metric. + + Arguments are values of additional labels + """ + assert (len(args) + len(kwargs)) == len(self._labels) + return self._metric.labels( + Metrics.INSTANCE_ID, + *[self._arg_to_str(arg) for arg in args], + **{arg_name: self._arg_to_str(arg_value) + for arg_name, arg_value in kwargs.items()}) + + def _arg_to_str(self, arg: Any) -> str: + """ Somehow onvert argument value to string """ + if isinstance(arg, bytes): + return arg.decode(encoding="utf-8", errors="backslashreplace") + return str(arg) + + def __init__(self, + metric_defs: List[Union["Metrics.MetricDef", Tuple]]) -> None: + """ Constructor + + Arguments: + metric_def -- List of metric definitions + """ + self._metrics: Dict[str, "Metrics._Metric"] = {} + for md in metric_defs: + if not isinstance(md, self._Metric): + md = self.MetricDef(*md) + assert md.name not in self._metrics + self._metrics[md.name] = self._Metric(md) + + def __getattr__(self, name: str) -> Any: + """ Metric lookup by name as attribute """ + metric = self._metrics.get(name) + assert metric is not None + return metric + + +class BytesUtils: + """ Various bytestring-related conversions """ + + @classmethod + def json_to_bytes(cls, j: JSON_DATA_TYPE) -> bytes: + """ Converts JSON dictionary to bytes. + Representation is the most compact: no whitespaces + + Arguments: + j -- Dictionary or list + Returns UTF8 bytes string + """ + return json.dumps(j, separators=(',', ':')).encode("utf-8") + + @classmethod + def text_to_uuid(cls, text: str) -> uuid.UUID: + """ Computes UUID, generated from text's MD5 digest + + Arguments: + text -- Text to compute UUID of + Returns UUID, made from MD5, computed from UTF8-encoded text + """ + return uuid.UUID(bytes=hashlib.md5(text.encode("utf-8")).digest()) + + @classmethod + def json_to_uuid(cls, j: JSON_DATA_TYPE) -> uuid.UUID: + """ Computes UUID, generated from JSON MD5 digest. + JSON first converted to compact (non spaces) string representation, + then to UTF-8 encoded bytes, then MD5 is computed + + Arguments: + j -- Dictionary or listUUIDbytes string + Returns UUID + """ + return uuid.UUID(bytes=hashlib.md5(cls.json_to_bytes(j)).digest()) + + +class DatabaseBase(ABC): + """ Base class of database handlers + + Attributes: + engine -- Database engine + metadata -- Database metadata + conn -- Default database connection + db_name -- Database name + _disposed -- True if disposed + """ + + # Driver part to use in Postgres database connection strings + DB_DRIVER = "postgresql+psycopg2://" + + # Parts of connection string + ConnStrParts = \ + NamedTuple( + "ConnStrParts", + # Driver part with trailing '://' + [("driver", str), + # Username + ("user", str), + # Host name + ("host", str), + # Port number + ("port", str), + # Database name + ("database", str), + # Parameters with leading '?' or empty string + ("params", str)]) + + def __init__(self, arg_conn_str: Optional[str], + arg_password: Optional[str]) -> None: + """ Constructor + + Arguments: + arg_conn_str -- Connection string from command line or None + arg_password -- Password from command line or None + """ + self._disposed = True + try: + self.engine = \ + self.create_engine(arg_conn_str=arg_conn_str, + arg_password=arg_password) + self.db_name = \ + self.parse_conn_str(arg_conn_str=arg_conn_str).database + self.metadata = sa.MetaData() + self.metadata.reflect(bind=self.engine) + self._disposed = False + self.conn = self.engine.connect() + logging.info(f"{self.name_for_logs()} database connected") + except sa.exc.SQLAlchemyError as ex: + error(f"Can't open {self.name_for_logs()} database: {ex}") + + def dispose(self) -> None: + """ Explicit cleanup """ + if not self._disposed: + self._disposed = True + self.engine.dispose() + + @classmethod + def parse_conn_str(cls, arg_conn_str: Optional[str]) \ + -> "DatabaseBase.ConnStrParts": + """ Parse argument/default connection string + + Arguments: + arg_conn_str -- Connection string from command line or None + Returns ConnStrParts + """ + cs_re = \ + re.compile( + r"^((?P[^/ ]+)://)?" + r"(?P[^@/ ]+?)?" + r"(@(?P[^:/ ]+)(:(?P\d+))?)?" + r"(/(?P[^?]+))?" + r"(?P\?\S.+)?$") + m_arg = cs_re.match((arg_conn_str or "").strip()) + error_if(not m_arg, + f"{cls.name_for_logs()} database connection string " + f"'{arg_conn_str or ''}' has invalid format") + assert m_arg is not None + m_def = cs_re.match(cls.default_conn_str()) + assert m_def is not None + error_if(m_arg.group("driver") and + (m_arg.group("driver") != m_def.group("driver")), + f"{cls.name_for_logs()} database connection string has " + f"invalid database driver name '{m_arg.group('driver')}'. " + f"If specified it must be '{m_def.group('driver')}'") + return \ + cls.ConnStrParts( + driver=m_def.group("driver"), + user=m_arg.group("user") or m_def.group("user"), + host=m_arg.group("host") or m_def.group("host"), + port=m_arg.group("port") or m_def.group("port"), + database=m_arg.group("database") or m_def.group("database"), + params=(m_arg.group("params") or m_def.group("params")) or "") + + @classmethod + def create_engine(cls, arg_conn_str: Optional[str], + arg_password: Optional[str]) -> sa.engine.base.Engine: + """ Creates SqlAlchemy engine + + Arguments: + arg_conn_str -- Connection string from command line or None + arg_password -- Password from command line or None + Returns SqlAlchemy engine + """ + conn_str_parts = cls.parse_conn_str(arg_conn_str=arg_conn_str) + return \ + sa.create_engine( + f"{cls.DB_DRIVER}" + f"{conn_str_parts.user}" + f"{(':' + arg_password) if arg_password else ''}" + f"@{conn_str_parts.host}:{conn_str_parts.port}/" + f"{conn_str_parts.database}{conn_str_parts.params}") + + @classmethod + @abstractmethod + def default_conn_str(cls) -> str: + """ Default connection string """ + ... + + @classmethod + @abstractmethod + def name_for_logs(cls) -> str: + """ Database alias name to use for logs and error messages """ + ... + + +class AlsDatabase(DatabaseBase): + """ ALS Database handler """ + + @classmethod + def default_conn_str(cls) -> str: + """ Default connection string """ + return "postgresql://postgres@localhost:5432/ALS" + + @classmethod + def name_for_logs(cls) -> str: + """ Database alias name to use for logs and error messages """ + return "ALS" + + +class LogsDatabase(DatabaseBase): + """ Log database handler """ + + # Log record with data to write to database + Record = NamedTuple("Record", [("source", str), + ("time", datetime.datetime), + ("log", JSON_DATA_TYPE)]) + + @classmethod + def default_conn_str(cls) -> str: + """ Default connection string """ + return "postgresql://postgres@localhost:5432/AFC_LOGS" + + @classmethod + def name_for_logs(cls) -> str: + """ Database alias name to use for logs and error messages """ + return "Logs" + + def write_log(self, topic: str, records: List["LogsDatabase.Record"]) \ + -> None: + """ Write a bunch of log records to database + + Arguments: + topic -- Kafka topic - serves a database table name + records -- List of records to write + """ + if not records: + return + try: + if topic not in self.metadata.tables: + sa.Table( + topic, self.metadata, + sa.Column("source", sa.Text(), index=True), + sa.Column("time", sa.DateTime(timezone=True), index=True), + sa.Column("log", sa_pg.JSON()), + keep_existing=True) + self.metadata.create_all(self.conn, checkfirst=True) + ins = sa.insert(self.metadata.tables[topic]).\ + values([{"source": r.source, + "time": r.time, + "log": r.log} + for r in records]) + self.conn.execute(ins) + except sa.exc.SQLAlchemyError as ex: + logging.error(f"Error writing {topic} log table: {ex}") + + +class InitialDatabase(DatabaseBase): + """ Initial Postgres database (context fro creation of other databases) + handler """ + + class IfExists(enum.Enum): + """ What to do if database being created already exists """ + Skip = "skip" + Drop = "drop" + Exc = "exc" + + @classmethod + def default_conn_str(cls) -> str: + """ Default connection string """ + return "postgresql://postgres@localhost:5432/postgres" + + @classmethod + def name_for_logs(cls) -> str: + """ Database alias name to use for logs and error messages """ + return "Initial" + + def create_db(self, db_name: str, + if_exists: "InitialDatabase.IfExists", + conn_str: str, password: Optional[str] = None, + template: Optional[str] = None) -> bool: + """ Create database + + Arguments: + db_name -- Name of database to create + if_exists -- What to do if database already exists + conn_str -- Connection string to database being created + password -- Password for connection string to database being created + template -- Name of template database to use + Returns True if database was created, False if it already exists + """ + with self.engine.connect() as conn: + try: + if if_exists == self.IfExists.Drop: + conn.execute(sa.text("commit")) + conn.execute( + sa.text(f'drop database if exists "{db_name}"')) + conn.execute(sa.text("commit")) + template_clause = f' template "{template}"' if template else "" + conn.execute( + sa.text(f'create database "{db_name}"{template_clause}')) + logging.info(f"Database '{db_name}' successfully created") + return True + except sa.exc.ProgrammingError: + if if_exists != self.IfExists.Skip: + raise + engine = self.create_engine(arg_conn_str=conn_str, + arg_password=password) + engine.dispose() + logging.info( + f"Already existing database '{db_name}' will be used") + return False + + def drop_db(self, db_name: str) -> None: + """ Drop given database """ + with self.engine.connect() as conn: + conn.execute(sa.text("commit")) + conn.execute(sa.text(f'drop database "{db_name}"')) + + +# Fully qualified position in Kafka queue on certain Kafka cluster +KafkaPosition = \ + NamedTuple("KafkaPosition", + [("topic", str), ("partition", int), ("offset", int)]) + + +class KafkaPositions: + """ Collection of partially-processed Kafka messages' offsets + + Private attributes: + _topics -- By-topic collection of by-partition collection of offset status + information + """ + class OffsetInfo: + """ Information about single offset + + Attributes: + kafka_offset -- Offset in partition + processed -- Processed status + """ + + def __init__(self, kafka_offset: int) -> None: + self.kafka_offset = kafka_offset + self.processed = False + + def __lt__(self, other: "KafkaPositions.OffsetInfo") -> bool: + """ Offset-based comparison for heap queue use """ + return self.kafka_offset < other.kafka_offset + + def __eq__(self, other: Any) -> bool: + """ Equality comparison in vase heap queue will need it """ + return isinstance(other, self.__class__) and \ + (self.kafka_offset == other.kafka_offset) + + def __repr__(self) -> str: + """ Debug print representation """ + return f"<{self.kafka_offset}, {'T' if self.processed else 'F'}>" + + class PartitionOffsets: + """ Collection of offset information objects within partition + + Private attributes: + _queue -- Heap queue of offset information objects + _catalog -- Catalog of offset information objects by offset + """ + + def __init__(self) -> None: + """ Constructor """ + self._queue: List["KafkaPositions.OffsetInfo"] = [] + self._catalog: Dict[int, "KafkaPositions.OffsetInfo"] = {} + + def is_empty(self) -> bool: + """ True if collection is empty (hence might be safely deleted) """ + return not self._queue + + def add(self, offset: int) -> None: + """ Add information about (not processed) offset to collection """ + if offset in self._catalog: + return + oi = KafkaPositions.OffsetInfo(offset) + heapq.heappush(self._queue, oi) + self._catalog[offset] = oi + + def mark_processed(self, offset: Optional[int]) -> None: + """ Mark given offset or all topic offsets as processed """ + if offset is not None: + if offset in self._catalog: + self._catalog[offset].processed = True + else: + for offset_info in self._catalog.values(): + offset_info.processed = True + + def get_processed_offset(self) -> Optional[int]: + """ Computes partition commit level + + Returns Maximum offset at and below which all offsets marked as + processed. None if there is no such offset. Offsets at and below + returned offset are removed from collection """ + ret: Optional[int] = None + while self._queue and self._queue[0].processed: + ret = heapq.heappop(self._queue).kafka_offset + assert ret is not None + del self._catalog[ret] + return ret + + def __repr__(self) -> str: + """ Debug print representation """ + return f"<{self._catalog}>" + + def __init__(self) -> None: + """ Constructor """ + self._topics: Dict[str, Dict[int, + "KafkaPositions.PartitionOffsets"]] = {} + + def add(self, kafka_position: KafkaPosition) -> None: + """ Add given position (topic/partition/offset) as nonprocessed """ + partition_offsets = \ + self._topics.setdefault(kafka_position.topic, {}).\ + get(kafka_position.partition) + if partition_offsets is None: + partition_offsets = self.PartitionOffsets() + self._topics[kafka_position.topic][kafka_position.partition] = \ + partition_offsets + partition_offsets.add(kafka_position.offset) + + def mark_processed(self, kafka_position: Optional[KafkaPosition] = None, + topic: Optional[str] = None) -> None: + """ Mark given position or all positions in a topic as processed + + Arguments: + kafka_position -- Position to mark as processed or None + topic -- Topic to mark as processed or None """ + if kafka_position is not None: + partition_offsets = \ + self._topics.setdefault(kafka_position.topic, {}).\ + get(kafka_position.partition) + if partition_offsets is not None: + partition_offsets.mark_processed(kafka_position.offset) + if topic is not None: + for partition_offsets in self._topics.get(topic, {}).values(): + partition_offsets.mark_processed(None) + + def get_processed_offsets(self) -> Dict[str, Dict[int, int]]: + """ Computes commit levels for all offsets in collection + + Returns by-topic/partition commit levels (offets at or below which are + all marked processed). Ofets at or below returned levels are removed + from collection """ + ret: Dict[str, Dict[int, int]] = {} + for topic, partitions in self._topics.items(): + for partition, partition_offsets in partitions.items(): + processed_offset = partition_offsets.get_processed_offset() + if processed_offset is not None: + ret.setdefault(topic, {})[partition] = processed_offset + for partition in ret.get(topic, {}): + if partitions[partition].is_empty(): + del partitions[partition] + return ret + + +# Type for Kafka keys of ALS messages +AlsMessageKeyType = bytes + + +class AlsMessage: + """ Single ALS message (AFC Request, Response or Config) + + Attributes: + raw_msg -- Message in raw form (as received from Kafka) + version -- Message format version + afc_server -- AFC Server ID + time_tag -- Time tag + msg_type -- Message type (one of AlsMessage.MsgType) + json_str -- Content of AFC Request/Response/Config as string + customer -- Customer (for Config) or None + geo_data_id -- Geodetic data ID (if Config) or None + uls_id -- ULS ID (if Config) or None + request_indexes -- Indexes of requests to which config is related (if + Config) or None + """ + # ALS message format version + FORMAT_VERSION = "1.0" + + class MsgType(enum.Enum): + """ ALS message type string """ + Request = "AFC_REQUEST" + Response = "AFC_RESPONSE" + Config = "AFC_CONFIG" + + # Maps values to MsgType enum instances + value_to_type = {t.value: t for t in MsgType} + + def __init__(self, raw_msg: bytes) -> None: + """ Constructor + + Arguments: + raw_msg -- Message value as retrieved from Kafka """ + self.raw_msg = raw_msg + try: + msg_dict = json.loads(raw_msg) + except json.JSONDecodeError as ex: + raise AlsProtocolError(f"Malforemed JSON of ALS message: {ex}", + code_line=LineNumber.exc()) + try: + self.version: str = msg_dict["version"] + self.afc_server: str = msg_dict["afcServer"] + self.time_tag = datetime.datetime.fromisoformat(msg_dict["time"]) + self.msg_type: "AlsMessage.MsgType" = \ + self.value_to_type[msg_dict["dataType"]] + self.json_str: str = msg_dict["jsonData"] + is_config = self.msg_type == self.MsgType.Config + self.customer: Optional[str] \ + = msg_dict["customer"] if is_config else None + self.geo_data_id: Optional[str] = \ + msg_dict["geoDataVersion"] if is_config else None + self.uls_id: Optional[str] = \ + msg_dict["ulsId"] if is_config else None + self.request_indexes: Optional[Set[int]] = \ + set(int(i) for i in msg_dict.get("requestIndexes", [])) \ + if is_config else None + except (LookupError, TypeError, ValueError) as ex: + raise AlsProtocolError(f"Invalid content of ALS message: {ex}", + code_line=LineNumber.exc(), data=msg_dict) + if self.version != self.FORMAT_VERSION: + raise AlsProtocolError( + f"Unsupported format version: '{self.version}'", + code_line=LineNumber.exc(), data=msg_dict) + if not isinstance(self.json_str, str): + raise AlsProtocolError("'jsonData' missing", + code_line=LineNumber.exc(), data=msg_dict) + if is_config and not \ + all(isinstance(x, str) for x in + (self.customer, self.geo_data_id, self.uls_id)): + raise AlsProtocolError( + "Missing config fields", + code_line=LineNumber.current(), data=msg_dict) + + +class AlsMessageBundle: + """ Request/Response/Config(s) bundle + + Private attributes: + _message_key -- Kafka message key + _kafka_positions -- KafkaPositions (registry of completed/incomplete + offsets) + _afc_server -- AFC Server ID + _last_update -- Time of last ALS message (from local clock) + _request_msg -- Request message as JSON dictionary (None if not yet + arrived) + _request_timetag -- Timetag of request message (None if not yet arrived) + _response_msg -- Response message as JSON dictionary (None if not yet + arrived) + _response_timetag -- Timetag of response message (None if not yet arrived) + _configs -- Dictionary of AfcConfigInfo objects, ordered by + individual request sequential indexes (or None if for + all requests) + _assembled -- True if bundle has all necessary parts + _store_parts -- Bundle in StoreParts representation. None if not yet + computed + _als_positions -- Set of positions of ALS messages used in this bundle + """ + # Top-level JSON keys in 'invariant_json' dictionary + JRR_REQUEST_KEY = "request" + JRR_RESPONSE_KEY = "response" + JRR_CONFIG_TEXT_KEY = "afc_config_text" + JRR_CUSTOMER_KEY = "customer" + JRR_ULS_KEY = "uls_data_id" + JRR_GEO_KEY = "geo_data_id" + + # Information about single AFC request/response + RequestResponse = \ + NamedTuple( + "RequestResponse", + # Dictionary indexed by 'JRR_...' keys. 'response' has empty (or + # nonexistent) 'availabilityExpireTime' field. Both 'request' and + # 'response' have 'requestId' field removed + [("invariant_json", JSON_DATA_TYPE), + # 'availabilityExpireTime', retrieved from 'response' + ("expire_time", Optional[datetime.datetime])]) + + # AFC Configuration information + AfcConfigInfo = \ + NamedTuple( + "AfcConfigInfo", [("config_str", str), ("customer", str), + ("geo_data_id", str), ("uls_id", str)]) + + # Messages arranged to form used in convenient for store in DB + StoreParts = \ + NamedTuple( + "StoreParts", + # AFC Server ID + [("afc_server", str), + # AFC Request message with empty + # 'availableSpectrumInquiryRequests' list + ("rx_envelope", JSON_DATA_TYPE), + # AFC Response message with empty + # 'availableSpectrumInquiryResponses' list + ("tx_envelope", JSON_DATA_TYPE), + # AFC Request message timetag + ("rx_timetag", datetime.datetime), + # AFC Response message timetag + ("tx_timetag", datetime.datetime), + # Dictionary of RequestResponse objects, indexed by 'requestId' + # field values + ("request_responses", Dict[str, RequestResponse]), + # List of requests with no responses + ("orphan_requests", List[JSON_DATA_TYPE]), + # List of responses with no requests + ("orphan_responses", List[JSON_DATA_TYPE])]) + + def __init__(self, message_key: AlsMessageKeyType, + kafka_positions: KafkaPositions) -> None: + """ Constructor + + message_key -- Raw key from Kafka message + kafka_positions -- KafkaPositions (registry of completed/incomplete + offsets) + """ + self._message_key = message_key + self._kafka_positions = kafka_positions + self._afc_server = "" + self._last_update = datetime.datetime.now() + self._request_msg: Optional[Dict[str, Any]] = None + self._request_timetag: Optional[datetime.datetime] = None + self._response_msg: Optional[JSON_DATA_TYPE] = None + self._response_timetag: Optional[datetime.datetime] = None + self._configs: Dict[Optional[int], + "AlsMessageBundle.AfcConfigInfo"] = {} + self._assembled = False + self._store_parts: Optional["AlsMessageBundle.StoreParts"] = None + self._als_positions: Set[KafkaPosition] = set() + + def message_key(self) -> AlsMessageKeyType: + """ Kafka message key """ + return self._message_key + + def assembled(self) -> bool: + """ True if bundle fully assembled (ave got all pertinent ALS messages) + """ + return self._assembled + + def last_update(self) -> datetime.datetime: + """ Local time of last update """ + return self._last_update + + def dump(self) -> JSON_DATA_TYPE: + """ Dump for debug purposes """ + if self._store_parts is not None: + return self._store_parts._asdict() + return \ + {"key": self._message_key.decode("latin-1"), + "afc_server": self._afc_server, + "last_update": self._last_update.isoformat(), + "request_msg": self._request_msg, + "request_timetag": + self._request_timetag.isoformat() if self._request_timetag + else None, + "response_msg": self._response_msg, + "response_timetag": + self._response_timetag.isoformat() if self._response_timetag + else None, + "configs": {k: cfg._asdict() for k, cfg in self._configs.items()} + if self._configs else None} + + def request_count(self) -> int: + """ Number of contained requests """ + assert self._request_msg is not None + try: + return \ + len(jl(self._request_msg["availableSpectrumInquiryRequests"])) + except (LookupError, TypeError, ValueError) as ex: + raise JsonFormatError(f"Requests not found: {ex}", + code_line=LineNumber.exc(), + data=self._request_msg) + + def update(self, message: AlsMessage, position: KafkaPosition) -> None: + """ Adds arrived ALS message + + Arguments: + message -- Kafka raw message value + position -- Kafka message position + """ + self._last_update = datetime.datetime.now() + if self._assembled: + return + try: + self._afc_server = message.afc_server + if message.msg_type == AlsMessage.MsgType.Request: + if self._request_msg is not None: + return + try: + self._request_msg = jd(json.loads(message.json_str)) + except json.JSONDecodeError: + raise JsonFormatError( + "Malformed JSON in AFC Request message", + code_line=LineNumber.exc(), data=message.json_str) + self._request_timetag = message.time_tag + elif message.msg_type == AlsMessage.MsgType.Response: + if self._response_msg is not None: + return + try: + self._response_msg = json.loads(message.json_str) + except json.JSONDecodeError: + raise JsonFormatError( + "Malformed JSON in AFC Response message", + code_line=LineNumber.exc(), data=message.json_str) + self._response_timetag = message.time_tag + else: + if message.msg_type != AlsMessage.MsgType.Config: + raise ValueError( + f"Unexpected ALS message type: {message.msg_type}") + assert message.msg_type == AlsMessage.MsgType.Config + + config_info = \ + self.AfcConfigInfo( + config_str=js(message.json_str), + customer=js(message.customer), + geo_data_id=js(message.geo_data_id), + uls_id=js(message.uls_id)) + if message.request_indexes: + for i in message.request_indexes: + self._configs[i] = config_info + else: + self._configs[None] = config_info + self._check_config_indexes(message) + self._assembled = (self._request_msg is not None) and \ + (self._response_msg is not None) and bool(self._configs) and \ + ((None in self._configs) or + (len(self._configs) == self.request_count())) + self._als_positions.add(position) + except (LookupError, TypeError, ValueError) as ex: + raise JsonFormatError(f"ALS message decoding problem: {ex}", + code_line=LineNumber.exc(), + data=message.raw_msg) + + def take_apart(self) -> "AlsMessageBundle.StoreParts": + """ Return (assembled) message contents in StoreParts form """ + assert self._assembled + if self._store_parts: + return self._store_parts + self._store_parts = \ + self.StoreParts( + afc_server=self._afc_server, + rx_envelope=jd(self._request_msg), + tx_envelope=jd(self._response_msg), + rx_timetag=jdt(self._request_timetag), + tx_timetag=jdt(self._response_timetag), + request_responses={}, orphan_requests=[], orphan_responses=[]) + requests: List[JSON_DATA_TYPE] = \ + jl(jd(self._request_msg)["availableSpectrumInquiryRequests"]) + responses: List[JSON_DATA_TYPE] = \ + jl(jd(self._response_msg)["availableSpectrumInquiryResponses"]) + jd(self._store_parts.rx_envelope)[ + "availableSpectrumInquiryRequests"] = [] + jd(self._store_parts.tx_envelope)[ + "availableSpectrumInquiryResponses"] = [] + response_map = {jd(r)["requestId"]: r for r in responses} + for req_idx, request in enumerate(requests): + req_id = js(jd(request)["requestId"]) + response = jod(response_map.get(req_id)) + if response is None: + self._store_parts.orphan_requests.append(request) + continue + del response_map[req_id] + + config_info = \ + self._configs[req_idx if req_idx in self._configs else None] + expire_time_str: Optional[str] = \ + response.get("availabilityExpireTime") + if expire_time_str is not None: + expire_time_str = expire_time_str.replace("Z", "+00:00") + if "+" not in expire_time_str: + expire_time_str += "+00:00" + expire_time = datetime.datetime.fromisoformat(expire_time_str) + response["availabilityExpireTime"] = "" + else: + expire_time = None + jd(request)["requestId"] = "" + response["requestId"] = "" + self._store_parts.request_responses[req_id] = \ + self.RequestResponse( + invariant_json={ + self.JRR_REQUEST_KEY: request, + self.JRR_RESPONSE_KEY: response, + self.JRR_CONFIG_TEXT_KEY: config_info.config_str, + self.JRR_CUSTOMER_KEY: config_info.customer, + self.JRR_ULS_KEY: config_info.uls_id, + self.JRR_GEO_KEY: config_info.geo_data_id}, + expire_time=expire_time) + for orphan in response_map.values(): + self._store_parts.orphan_responses.append(orphan) + return self._store_parts + + def _check_config_indexes(self, message: AlsMessage) -> None: + """ Ensure that config indexes in Config ALS message are valid """ + if (self._request_msg is None) or (not self._configs): + return + rc = self.request_count() + if not all(0 <= i < rc for i in self._configs if i is not None): + raise AlsProtocolError( + f"Out of range config indexes found while processing ALS " + f"message with key '{self._message_key!r}'", + code_line=LineNumber.current(), data=message.raw_msg) + + def __lt__(self, other) -> bool: + """ Comparison for by-time heap queue """ + assert isinstance(other, self.__class__) + return self._last_update < other._last_update + + def __eq__(self, other) -> bool: + """ Equality comparison for by-time heap queue """ + return isinstance(other, self.__class__) and \ + (self._message_key == other._message_key) + + def __del__(self) -> None: + """ Destructor (marks ALS messages as processed """ + for als_position in self._als_positions: + self._kafka_positions.mark_processed(als_position) + + +class CertificationList: + """ List of AP Certifications + + Private attributes: + _certifications -- Dictionary of certifications, indexed by 0-based indices + in list from 'certificationId' field + """ + # Single certification + Certification = \ + NamedTuple("Certification", [("ruleset_id", str), + ("certification_id", str)]) + + def __init__(self, json_data: Optional[List[JSON_DATA_TYPE]] = None) \ + -> None: + """ Constructor + + Arguments: + json_data -- Optional JSON dictionary - value of 'certificationId' + field to read self from + """ + self._certifications: Dict[int, "CertificationList.Certification"] = {} + if json_data is not None: + try: + for c in json_data: + cert: Dict[str, Any] = jd(c) + ruleset_id = cert["rulesetId"] + certification_id = cert["id"] + if not (isinstance(ruleset_id, str), + isinstance(certification_id, str)): + raise TypeError() + self._certifications[len(self._certifications)] = \ + self.Certification( + ruleset_id=js(ruleset_id), + certification_id=js(certification_id)) + except (LookupError, TypeError, ValueError): + raise JsonFormatError( + "Invalid DeviceDescriptor.certificationId format", + code_line=LineNumber.exc(), data=json_data) + + def add_certification( + self, index: int, + certification: "CertificationList.Certification") -> None: + """ Adds single certification + + Arguments: + index -- 0-based certification indexc in certification list + certification -- Certification to add + """ + self._certifications[index] = certification + + def get_uuid(self) -> uuid.UUID: + """ UUID of certification list (computed over JSON list of + certidications) """ + return \ + BytesUtils.json_to_uuid( + [{"rulesetId": self._certifications[idx].ruleset_id, + "id": self._certifications[idx].certification_id} + for idx in sorted(self._certifications.keys())]) + + def certifications(self) -> List["CertificationList.Certification"]: + """ List of Certification objects """ + return \ + [self._certifications[idx] for idx in sorted(self._certifications)] + + def __eq__(self, other: Any) -> bool: + """ Eqquality comparison """ + return isinstance(other, self.__class__) and \ + (self._certifications == other._certifications) + + def __hash__(self) -> int: + """ Hash over certifications """ + return \ + sum(idx + hash(cert) for idx, cert in self._certifications.items()) + + +class RegRuleList: + """ List of regulatory rules + + Privatew attributes: + _reg_rules - By-index in list dictionary of regulatory rules names """ + + def __init__(self, json_data: Optional[List[Any]] = None) -> None: + """ Constructor + + Arguments: + json_data -- Optional content of 'rulesetIds' field to read self from + """ + self._reg_rules: Dict[int, str] = {} + if json_data is not None: + try: + for reg_rule in json_data: + if not isinstance(reg_rule, str): + raise TypeError() + self._reg_rules[len(self._reg_rules)] = reg_rule + except (LookupError, TypeError, ValueError): + raise JsonFormatError("Invalid regulatory rule format", + code_line=LineNumber.exc(), + data=json_data) + + def add_rule(self, index: int, reg_rule: str) -> None: + """ Add regulatory rule: + + Arguments: + index -- 0-based rule index in rule list + reg_rule -- Rulew name + """ + self._reg_rules[index] = reg_rule + + def get_uuid(self) -> uuid.UUID: + """ Computes digest of self """ + return \ + BytesUtils.json_to_uuid( + [self._reg_rules[idx] + for idx in sorted(self._reg_rules.keys())]) + + def reg_rules(self) -> List[str]: + """ List of rule names """ + return [self._reg_rules[idx] for idx in sorted(self._reg_rules.keys())] + + def __eq__(self, other: Any) -> bool: + """ Equality comparison """ + return isinstance(other, self.__class__) and \ + (self._reg_rules == other._reg_rules) + + def __hash__(self) -> int: + """ Hash value """ + return sum(idx + hash(rr) for idx, rr in self._reg_rules.items()) + + +class AlsTableBase: + """ Common part of ALS database table initialization + + Protected attributes: + _adb -- AlsDatabase object + _table_name -- Table name + _table -- SQLAlchemy Table object + """ + # Name of month index column + MONTH_IDX_COL_NAME = "month_idx" + + def __init__(self, adb: AlsDatabase, table_name: str) -> None: + """ Constructor + adb -- AlsDatabase object + table_name -- List of sa + """ + self._adb = adb + self._table_name = table_name + if self._table_name not in self._adb.metadata.tables: + raise \ + DbFormatError( + f"'{self._table_name}' table not found in ALS database", + code_line=LineNumber.current()) + self._table: sa.Table = self._adb.metadata.tables[self._table_name] + + def get_column(self, name: str, expected_type: Optional[Type] = None) \ + -> sa.Column: + """ Returns given column object + + Arguments: + name -- Column name + expected_type -- Expected column type (None to not check) + Returns correspondent sa.Column object + """ + ret = self._table.c.get(name) + if ret is None: + raise DbFormatError(f"Column '{name}' not found in table " + f"'{self._table_name}' of ALS database", + code_line=LineNumber.current()) + if (expected_type is not None) and \ + (not isinstance(ret.type, expected_type)): + raise DbFormatError(f"Column '{name}' of '{self._table_name}' " + f"table of ALS database has unexpected type", + code_line=LineNumber.current()) + return ret + + def get_month_idx_col(self) -> sa.Column: + """ Returns an instance of 'month_idx' column """ + return self.get_column(self.MONTH_IDX_COL_NAME, sa.SmallInteger) + + +class Lookups: + """ Collection of lookups + + Private attributes: + _lookups -- List of registered LookupBase objects + """ + + def __init__(self) -> None: + """ Constructor """ + self._lookups: List["LookupBase"] = [] + + def register(self, lookup: "LookupBase") -> None: + """ Register newly-created lookup """ + self._lookups.append(lookup) + + def reread(self) -> None: + """ Signal all lookups to reread self (e.g. after transsaction failure) + """ + for lookup in self._lookups: + lookup.reread() + + +# Generic type name for lookup key value (usually int or UUID) +LookupKey = TypeVar("LookupKey") +# Generic type name for lookup table value +LookupValue = TypeVar("LookupValue") + + +class LookupBase(AlsTableBase, Generic[LookupKey, LookupValue], ABC): + """ Generic base class for lookup tables (database tables, also contained + in memory for speed of access) + + Private attributes: + _by_value -- Dictionary of lookup keys, ordered by (value, month_index) + keys + _value_column -- SQLALchemy column for lookup tables where value contained + in some column of a single row. None for other cases (e.g. + when value should be constructed from several rows) + _need_reread -- True if dictionary should be reread from database on next + update_db() + """ + + def __init__(self, adb: AlsDatabase, table_name: str, lookups: Lookups, + value_column_name: Optional[str] = None) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + table_name -- Database table name + lookups -- Lookup collection to register self in + value_column_name -- Optional name of column containing lookup value. + None for lookups that contain value in more than + one row/column + """ + AlsTableBase.__init__(self, adb=adb, table_name=table_name) + lookups.register(self) + self._value_column: Optional[sa.Column] = \ + None if value_column_name is None \ + else self.get_column(value_column_name) + self._by_value: Dict[Tuple[LookupValue, int], LookupKey] = {} + self._need_reread = True + + def reread(self) -> None: + """ Request reread on next update """ + self._need_reread = True + + def update_db(self, values: Iterable[LookupValue], month_idx: int) -> None: + """ Update lookup table with new lookup values (if any) + + Arguments: + values -- Sequence of lookup values (some of which may, other may not + already be in the table) + month_id -- Month index to use in new records + """ + self._reread_if_needed() + new_value_months: Set[Tuple[LookupValue, int]] = \ + {(value, month_idx) for value in values} - \ + set(self._by_value.keys()) + if not new_value_months: + return + rows: List[ROW_DATA_TYPE] = [] + for value_month in new_value_months: + if self._value_column is None: + self._by_value[value_month] = \ + self._key_from_value(value_month[0]) + rows += self._rows_from_value(*value_month) + try: + ins = sa_pg.insert(self._table).values(rows).\ + on_conflict_do_nothing() + if self._value_column is not None: + ins = ins.returning(self._table) + result = self._adb.conn.execute(ins) + if self._value_column is not None: + for row in result: + key = self._key_from_row(row) + value = self._value_from_row_create(row) + self._by_value[(value, month_idx)] = key + assert isinstance(key, int) + new_value_months.remove((value, month_idx)) + for value_month in new_value_months: + s = sa.select([self._table]).\ + where(self._value_column == value_month[0]) + result = self._adb.conn.execute(s) + self._by_value[value_month] = \ + self._key_from_row(list(result)[0]) + except (sa.exc.SQLAlchemyError, TypeError, ValueError) as ex: + raise DbFormatError( + f"Error updating '{self._table_name}': {ex}", + code_line=LineNumber.exc()) + + def key_for_value(self, value: LookupValue, month_idx: int) \ + -> LookupKey: + """ Returns lookup key for given value """ + return self._by_value[(value, month_idx)] + + @abstractmethod + def _key_from_row(self, row: ROW_DATA_TYPE) -> LookupKey: + """ Required 'virtual' function. Returns key contained in given table + row dictionary """ + ... + + @abstractmethod + def _value_from_row_create(self, row: ROW_DATA_TYPE) -> LookupValue: + """ Required 'virtual' function. Creates (possibly incomplete) lookup + value contained in given table row dictionary """ + ... + + def _reread_if_needed(self) -> None: + """ Reread lookup from database if requested """ + if not self._need_reread: + return + by_key: Dict[Tuple[LookupKey, int], LookupValue] = {} + try: + for row in self._adb.conn.execute(sa.select(self._table)): + key = (self._key_from_row(row), + row[AlsTableBase.MONTH_IDX_COL_NAME]) + value = by_key.get(key) + if value is None: + by_key[key] = \ + self._value_from_row_create(row) + else: + self._value_from_row_update(row, value) + except (sa.exc.SQLAlchemyError, TypeError, ValueError) as ex: + raise DbFormatError(f"Error reading '{self._table_name}': {ex}", + code_line=LineNumber.exc()) + self._by_value = {(by_key[(key, month_idx)], month_idx): key + for key, month_idx in by_key} + self._need_reread = False + + def _value_from_row_update(self, row: ROW_DATA_TYPE, + value: LookupValue) -> None: + """ Optional 'virtual' function. Updates incomplete value with data + from given row dictionary. Call of this function only happens for + lookup tables, whose values contained in a single row """ + raise NotImplementedError(f"_value_from_row_update() not implemented " + f"for '{self._table_name}' table") + + def _key_from_value(self, value: LookupValue) -> LookupKey: + """ Optional 'virtual' function. Computes lookup key from lookup value. + Only called for tables without 'value_column' """ + raise NotImplementedError(f"_key_from_value() not implemented for " + f"'{self._table_name}' table") + + @abstractmethod + def _rows_from_value(self, value: LookupValue, month_idx: int) \ + -> List[ROW_DATA_TYPE]: + """ Required 'virtual' function. List of database rows from given + lookup value and month index """ + ... + + +class CertificationsLookup(LookupBase[uuid.UUID, CertificationList]): + """ Certifications' lookup + + Private attributes: + _col_digest -- Certifications' digest column + _col_index -- Index in Certifications' list column + _col_month_idx -- Month index column + _col_ruleset_id -- National Registration Authority name column + _col_id -- Certificate ID column + """ + # Table name + TABLE_NAME = "certification" + + def __init__(self, adb: AlsDatabase, lookups: Lookups) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + lookups -- Lookup collection to register self in + """ + super().__init__(adb=adb, table_name=self.TABLE_NAME, lookups=lookups) + self._col_digest = self.get_column("certifications_digest", sa_pg.UUID) + self._col_index = self.get_column("certification_index", + sa.SmallInteger) + self._col_month_idx = self.get_month_idx_col() + self._col_ruleset_id = self.get_column("ruleset_id", sa.Text) + self._col_id = self.get_column("certification_id", sa.Text) + + def _key_from_row(self, row: ROW_DATA_TYPE) -> uuid.UUID: + """ Certifications' digest for given row dictionary """ + return uuid.UUID(js(row[ms(self._col_digest.name)])) + + def _value_from_row_create(self, row: ROW_DATA_TYPE) -> CertificationList: + """ Returns partial certification list from given row dictionary """ + ret = CertificationList() + self._value_from_row_update(row, ret) + return ret + + def _value_from_row_update(self, row: ROW_DATA_TYPE, + value: CertificationList) -> None: + """ Updates given partial certification list from given row dictionary + """ + value.add_certification( + index=ji(row[ms(self._col_index.name)]), + certification=CertificationList.Certification( + ruleset_id=js(row[ms(self._col_ruleset_id.name)]), + certification_id=js(row[ms(self._col_id.name)]))) + + def _key_from_value(self, value: CertificationList) -> uuid.UUID: + """ Table (semi) key from Certifications object """ + return value.get_uuid() + + def _rows_from_value(self, value: CertificationList, month_idx: int) \ + -> List[ROW_DATA_TYPE]: + """ List of rows dictionaries, representing given Certifications object + """ + ret: List[ROW_DATA_TYPE] = [] + for cert_idx, certification in enumerate(value.certifications()): + ret.append( + {ms(self._col_digest.name): value.get_uuid().urn, + ms(self._col_index.name): cert_idx, + ms(self._col_month_idx.name): month_idx, + ms(self._col_ruleset_id.name): certification.ruleset_id, + ms(self._col_id.name): certification.certification_id}) + return ret + + +class AfcConfigLookup(LookupBase[uuid.UUID, str]): + """ AFC Configs lookup table + + Private attributes: + _col_digest -- Digest computed over AFC Config string column + _col_month_idx -- Month index column + _col_text -- AFC Config text representation column + _col_json -- AFC Config JSON representation column + """ + # Table name + TABLE_NAME = "afc_config" + + def __init__(self, adb: AlsDatabase, lookups: Lookups) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + lookups -- Lookup collection to register self in + """ + super().__init__(adb=adb, table_name=self.TABLE_NAME, lookups=lookups) + self._col_digest = self.get_column("afc_config_text_digest", + sa_pg.UUID) + self._col_month_idx = self.get_month_idx_col() + self._col_text = self.get_column("afc_config_text", sa.Text) + self._col_json = self.get_column("afc_config_json", sa_pg.JSON) + + def _key_from_row(self, row: ROW_DATA_TYPE) -> uuid.UUID: + """ Returns AFC Config digest stored in a row dictionary """ + return uuid.UUID(js(row[ms(self._col_digest.name)])) + + def _value_from_row_create(self, row: ROW_DATA_TYPE) -> str: + """ Returns AFC Config string stored in row dictionary """ + return js(row[ms(self._col_text.name)]) + + def _key_from_value(self, value: str) -> uuid.UUID: + """ Computes AFC config digest from AFC Config string """ + return BytesUtils.text_to_uuid(value) + + def _rows_from_value(self, value: str, month_idx: int) \ + -> List[ROW_DATA_TYPE]: + """ Returns row dictionary from AFC Config string """ + try: + config_json = json.loads(value) + except json.JSONDecodeError as ex: + raise JsonFormatError(f"Malformed AFC Config JSON: {ex}", + code_line=LineNumber.exc(), data=value) + return [{ms(self._col_digest.name): + BytesUtils.text_to_uuid(value).urn, + ms(self._col_month_idx.name): month_idx, + ms(self._col_text.name): value, + ms(self._col_json.name): config_json}] + + +class StringLookup(LookupBase[int, str]): + """ Lookup table with string values and sequential integer keys + + Private attributes: + _col_id -- Sequential index column + _col_month_idx -- Month index column + _col_value -- String value column + """ + # Lookup parameters + Params = NamedTuple( + "Params", + # Table name + [("table_name", str), + # Sequential index column name + ("id_col_name", str), + # String value column name + ("value_col_name", str)]) + # Parameter for AFC Server name lookup + AFC_SERVER_PARAMS = Params(table_name="afc_server", + id_col_name="afc_server_id", + value_col_name="afc_server_name") + # Parameters for Customer name lookup + CUSTOMER_PARAMS = Params(table_name="customer", + id_col_name="customer_id", + value_col_name="customer_name") + # Parameters for ULS ID lookup + ULS_PARAMS_PARAMS = Params(table_name="uls_data_version", + id_col_name="uls_data_version_id", + value_col_name="uls_data_version") + # Parameters for Geodetic data ID lookup + GEO_DATA_PARAMS = Params(table_name="geo_data_version", + id_col_name="geo_data_version_id", + value_col_name="geo_data_version") + + def __init__(self, adb: AlsDatabase, params: "StringLookup.Params", + lookups: Lookups) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + params -- Lookup parameters + lookups -- Lookup collection to register self in + """ + super().__init__(adb=adb, table_name=params.table_name, + value_column_name=params.value_col_name, + lookups=lookups) + self._col_id = self.get_column(params.id_col_name, sa.Integer) + self._col_month_idx = self.get_month_idx_col() + self._col_value = self.get_column(params.value_col_name, sa.Text) + + def _key_from_row(self, row: ROW_DATA_TYPE) -> int: + """ Key from row dictionary """ + return ji(row[ms(self._col_id.name)]) + + def _value_from_row_create(self, row: ROW_DATA_TYPE) -> str: + """ Value from row dictionary """ + return js(row[ms(self._col_value.name)]) + + def _rows_from_value(self, value: str, month_idx: int) \ + -> List[ROW_DATA_TYPE]: + """ Lookup table row dictionary for a value """ + return [{ms(self._col_month_idx.name): month_idx, + ms(self._col_value.name): value}] + + +# Generic type parameter for data key, used in data dictionaries, passed to +# update_db(). +# Nature of this key might be different - it can be primary data key (usually +# data digest), foreign key or even key for consistency (to not create +# list-based version of update_db()). +# Digest primary keys (the most typical option) are expensive to compute, so +# their values should be reused, not recomputed +TableUpdaterDataKey = TypeVar("TableUpdaterDataKey") +# Generic type parameter for data value stored in table - type for values of +# data dictionary passed to update_db(). Most often a JSON object (e.g. from +# AFC message) to be written to table - maybe along with to dependent tables +TableUpdaterData = TypeVar("TableUpdaterData") + + +class TableUpdaterBase(AlsTableBase, + Generic[TableUpdaterDataKey, TableUpdaterData], ABC): + """ Base class for tables being updated (no in-memory data copy) + + Private attributes: + _json_obj_name -- Name of JSON object that corresponds to data in + table (or something descriptive) for error + reporting purposes + _data_key_columns -- List of columns that correspond to data key + (usually - primary table key without 'month_idx' + column). Used to collect information for + _update_foreign_sources(), empty means not to call + _update_foreign_sources() + """ + + def __init__(self, adb: AlsDatabase, table_name: str, json_obj_name: str, + data_key_column_names: Optional[List[str]] = None) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + table_name -- Table name + json_obj_name -- Name of JSON object that corresponds to data + in table (or something descriptive) for error + reporting purposes + data_key_column_names -- List of names of columns that correspond to + data key (usually - primary table key without + 'month_idx' column). Used to collect + information for _update_foreign_sources(), + None means not to call + _update_foreign_sources() + """ + AlsTableBase.__init__(self, adb=adb, table_name=table_name) + self._json_obj_name = json_obj_name + self._data_key_columns = \ + [self.get_column(col_name, None) + for col_name in (data_key_column_names or [])] + + def update_db(self, + data_dict: Dict[TableUpdaterDataKey, TableUpdaterData], + month_idx: int) -> None: + """ Write data to table (unless they are already there) + + Arguments: + data_dict -- Data dictionary (one item per record) + month_idx -- Value for 'month_idx' column + """ + try: + if not data_dict: + return + rows: List[ROW_DATA_TYPE] = [] + self._update_lookups(data_objects=data_dict.values(), + month_idx=month_idx) + row_infos: \ + Dict[TableUpdaterDataKey, + Tuple[TableUpdaterData, List[ROW_DATA_TYPE]]] = {} + for data_key, data_object in data_dict.items(): + rows_for_data = self._make_rows(data_key=data_key, + data_object=data_object, + month_idx=month_idx) + rows += rows_for_data + row_infos[data_key] = (data_object, rows_for_data) + self._update_foreign_targets(row_infos=row_infos, + month_idx=month_idx) + except (LookupError, TypeError, ValueError) as ex: + raise JsonFormatError( + f"Invalid {self._json_obj_name} object format: {ex}", + code_line=LineNumber.exc()) + ins = sa_pg.insert(self._table).values(rows).on_conflict_do_nothing() + if self._data_key_columns: + ins = ins.returning(*self._data_key_columns) + try: + result = self._adb.conn.execute(ins) + except (sa.exc.SQLAlchemyError) as ex: + raise DbFormatError(f"Error updating '{self._table_name}': {ex}", + code_line=LineNumber.exc()) + if not self._data_key_columns: + return + inserted_rows: \ + Dict[TableUpdaterDataKey, + Tuple[ROW_DATA_TYPE, TableUpdaterData, + RESULT_ROW_DATA_TYPE]] = {} + for result_row_idx, result_row in enumerate(result): + data_key = \ + self._data_key_from_result_row( + result_row=result_row, result_row_idx=result_row_idx) + inserted_rows[data_key] = (row_infos[data_key][1][0], + data_dict[data_key], result_row) + self._update_foreign_sources(inserted_rows=inserted_rows, + month_idx=month_idx) + + def _update_lookups(self, data_objects: Iterable[TableUpdaterData], + month_idx: int) -> None: + """ Updates lookups, references by current table. + Optional 'virtual' function + + Arguments: + data_objects -- Sequence of data objects being inserted + month_idx -- Value for 'month_idx' column + """ + pass + + @abstractmethod + def _make_rows(self, data_key: TableUpdaterDataKey, + data_object: TableUpdaterData, + month_idx: int) -> List[ROW_DATA_TYPE]: + """ Generates list of table row dictionaries for given object. + Mandatory 'virtual' function + + Argument: + data_key -- Data key for a data object + data_object -- Data object + month_idx -- Value for 'month_idx' column + Returns list of row dictionaries + """ + ... + + def _update_foreign_targets( + self, + row_infos: Dict[TableUpdaterDataKey, + Tuple[TableUpdaterData, List[ROW_DATA_TYPE]]], + month_idx: int) -> None: + """ Updates tables pointed to by foreign keys of this table. + Optional 'virtual' function + + Arguments: + row_infos -- Dictionary of data objects and row lists generated from + them, indexed by data keys + month_idx -- Value for 'month_idx' column + """ + pass + + def _data_key_from_result_row( + self, result_row: Tuple[Any, ...], result_row_idx: int) \ + -> TableUpdaterDataKey: + """ Returns data key from given result row (comprised of columns, + passed to constructor as 'data_key_column_names' argument). Called only + if _update_foreign_sources() should be called. Default implementation + (presented here) returns value from first and only column + + Arguments: + result_row -- Result row (list of values of columns, passed as + 'data_key_columns' parameter + result_row_idx -- 0-based index of result row + Returns data key, computed from result row and row index + """ + assert len(result_row) == 1 + return result_row[0] + + def _update_foreign_sources( + self, + inserted_rows: Dict[TableUpdaterDataKey, + Tuple[ROW_DATA_TYPE, TableUpdaterData, + RESULT_ROW_DATA_TYPE]], + month_idx: int) -> None: + """ Updates tables whose foreign keys point to this table. + Optional 'virtual' function that only called if 'data_key_column_names' + was passed to constructor + + Arguments: + inserted_rows -- Information about newly-inserted rows (dictionary of + (row_dictionary, data_object, result_row) tuples, + ordered by data keys + month_idx -- Month index + """ + raise NotImplementedError(f"_update_foreign_sources() not implemented " + f"for table '{self._table_name}'") + + +class DeviceDescriptorTableUpdater(TableUpdaterBase[uuid.UUID, + JSON_DATA_TYPE]): + """ Updater of device descriptor table. + Data key is digest, computed over device descriptor JSON string + + Private data: + _cert_lookup -- Certificates' lookup + _col_digest -- Digest column + _col_month_idx -- Month index column + _col_serial -- AP Serial Number column + _col_cert_digest -- Certificates' digest column + """ + TABLE_NAME = "device_descriptor" + + def __init__(self, adb: AlsDatabase, cert_lookup: CertificationsLookup) \ + -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + cert_lookup -- Certificates' lookup + """ + super().__init__(adb=adb, table_name=self.TABLE_NAME, + json_obj_name="DeviceDescriptor") + self._cert_lookup = cert_lookup + self._col_digest = self.get_column("device_descriptor_digest", + sa_pg.UUID) + self._col_month_idx = self.get_month_idx_col() + self._col_serial = self.get_column("serial_number", sa.Text) + self._col_cert_digest = self.get_column("certifications_digest", + sa_pg.UUID) + + def _update_lookups(self, data_objects: Iterable[JSON_DATA_TYPE], + month_idx: int) -> None: + """ Update used lookups + + Arguments: + data_objects -- Sequence of JSON dictionaries with device descriptors + row_lookup -- Rows to be written to database, ordered by device + descriptor digests + month_idx -- Month index + """ + cert_lists: List[CertificationList] = [] + for d in data_objects: + j = jd(d) + try: + cert_lists.append(CertificationList(jl(j["certificationId"]))) + except LookupError as ex: + raise JsonFormatError( + f"Certifications not found: {ex}", + code_line=LineNumber.exc(), data=j) + self._cert_lookup.update_db(cert_lists, month_idx=month_idx) + + def _make_rows(self, data_key: uuid.UUID, data_object: JSON_DATA_TYPE, + month_idx: int) -> List[ROW_DATA_TYPE]: + """ Generates table row dictionary for given device descriptor JSON + + Arguments: + data_key -- Data key (DeviceDescriptor JSON digest) + data_object -- DeviceDescriptor JSON + month_idx -- Month index + + Returns list of single row dictionary + """ + try: + json_object = jd(data_object) + return [{ms(self._col_digest.name): data_key.urn, + ms(self._col_month_idx.name): month_idx, + ms(self._col_serial.name): json_object["serialNumber"], + ms(self._col_cert_digest.name): + self._cert_lookup.key_for_value( + CertificationList(json_object["certificationId"]), + month_idx=month_idx).urn}] + except (LookupError, TypeError, ValueError) as ex: + raise \ + JsonFormatError( + f"Invalid device DeviceDescriptor format: '{ex}'", + code_line=LineNumber.exc(), data=data_object) + + +class LocationTableUpdater(TableUpdaterBase[uuid.UUID, JSON_DATA_TYPE]): + """ Locations table updater. + Data key is digest over JSON Location object, data value is JSON Location + object + + Private attributes: + _col_digest -- Digest over JSON Locatopn object column + _col_month_idx -- Month index column + _col_location -- Geodetic location column + _col_loc_uncertainty -- Location uncertainty in meters column + _col_loc_type -- Location type + (ellipse/radialPolygon/linearPolygon) column + _col_deployment_type -- Location deployment (indoor/outdoor) column + _col_height -- Height in meters column + _col_height_uncertainty -- Height uncertainty in meters column + _col_height_type -- Height type (AGL/AMSL) column + """ + # Table name + TABLE_NAME = "location" + # Point geodetic coordinates - in North/East positive degrees + Point = NamedTuple("Point", [("lat", float), ("lon", float)]) + # Length of one degree in meters in latitudinal direction + DEGREE_M = 6_371_000 * math.pi / 180 + + def __init__(self, adb: AlsDatabase) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + """ + super().__init__(adb=adb, table_name=self.TABLE_NAME, + json_obj_name="Location") + self._col_digest = self.get_column("location_digest", sa_pg.UUID) + self._col_month_idx = self.get_month_idx_col() + self._col_location = self.get_column("location_wgs84", ga.Geography) + self._col_loc_uncertainty = self.get_column("location_uncertainty_m", + sa.Float) + self._col_loc_type = self.get_column("location_type", sa.Text) + self._col_deployment_type = self.get_column("deployment_type", + sa.Integer) + self._col_height = self.get_column("height_m", sa.Float) + self._col_height_uncertainty = \ + self.get_column("height_uncertainty_m", sa.Float) + self._col_height_type = self.get_column("height_type", sa.Text) + + def _make_rows(self, data_key: uuid.UUID, data_object: JSON_DATA_TYPE, + month_idx: int) -> List[ROW_DATA_TYPE]: + """ Makes table row dictionary from Location JSON data object + + Arguments: + data_key -- Data key (Location JSON digest) + data_object -- Data object (Location JSON) + month_idx -- Month index + Returns single-element row dictionary list + """ + try: + json_object = jd(data_object) + j_elev = json_object["elevation"] + ret: ROW_DATA_TYPE = \ + {ms(self._col_digest.name): data_key.urn, + ms(self._col_month_idx.name): month_idx, + ms(self._col_deployment_type.name): + ji(json_object.get("indoorDeployment", 0)), + ms(self._col_height.name): float(j_elev["height"]), + ms(self._col_height_type.name): str(j_elev["heightType"]), + ms(self._col_height_uncertainty.name): + float(j_elev["verticalUncertainty"])} + loc_uncertainty: float + if "ellipse" in json_object: + ret[ms(self._col_loc_type.name)] = "ellipse" + j_ellipse = jd(json_object["ellipse"]) + center = self._get_point(jd(j_ellipse["center"])) + loc_uncertainty = float(j_ellipse["majorAxis"]) + elif "radialPolygon" in json_object: + ret[ms(self._col_loc_type.name)] = "radialPolygon" + j_r_poly = jd(json_object["radialPolygon"]) + loc_uncertainty = 0 + center = self._get_point(jd(j_r_poly["center"])) + for j_v in j_r_poly["outerBoundary"]: + loc_uncertainty = max(loc_uncertainty, + float(j_v["length"])) + else: + j_l_poly = jd(json_object["linearPolygon"]) + ret[ms(self._col_loc_type.name)] = "linearPolygon" + center_lat: float = 0 + center_lon: float = 0 + lon0: Optional[float] = None + for j_p in j_l_poly["outerBoundary"]: + p = self._get_point(jd(j_p)) + center_lat += p.lat + if lon0 is None: + lon0 = p.lon + center_lon += self._same_hemisphere(p.lon, lon0) + center_lat /= len(j_l_poly["outerBoundary"]) + center_lon /= len(j_l_poly["outerBoundary"]) + if center_lon <= -180: + center_lon += 360 + elif center_lon > 180: + center_lon -= 360 + center = self.Point(lat=center_lat, lon=center_lon) + loc_uncertainty = 0 + for j_p in jl(j_l_poly["outerBoundary"]): + p = self._get_point(jd(j_p)) + loc_uncertainty = max(loc_uncertainty, + self._dist(center, p)) + ret[ms(self._col_loc_uncertainty.name)] = loc_uncertainty + ret[ms(self._col_location.name)] = \ + f"POINT({center.lon} {center.lat})" + return [ret] + except (LookupError, TypeError, ValueError) as ex: + raise JsonFormatError(f"Invalid Location format: '{ex}'", + code_line=LineNumber.exc(), + data=data_object) + + def _get_point(self, j_p: dict) -> "LocationTableUpdater.Point": + """ Point object from JSON + + Arguments: + j_p -- JSON Point object + Returns Point object + """ + return self.Point(lat=j_p["latitude"], lon=j_p["longitude"]) + + def _same_hemisphere(self, lon: float, root_lon: float) -> float: + """ Makes actually close longitudes numerically close + + Arguments: + lon -- Longitude in question + root_lon -- Longitude in hemisphere in question + Returns if longitudes are on the opposite sides of 180 - returns + appropriately corrected first one (even if it will go beyond + [-180, 180]. Otherwise - just returns first longitude + """ + if lon < (root_lon - 180): + lon += 360 + elif lon > (root_lon + 180): + lon -= 360 + return lon + + def _dist(self, p1: "LocationTableUpdater.Point", + p2: "LocationTableUpdater.Point") -> float: + """ Approximate distance in meters beteen two geodetic points """ + lat_dist = (p1.lat - p2.lat) * self.DEGREE_M + lon_dist = \ + (p1.lon - self._same_hemisphere(p2.lon, p1.lon)) * \ + math.cos((p1.lat + p2.lat) / 2 * math.pi / 180) * \ + self.DEGREE_M + return math.sqrt(lat_dist * lat_dist + lon_dist * lon_dist) + + +class CompressedJsonTableUpdater(TableUpdaterBase[uuid.UUID, JSON_DATA_TYPE]): + """ Compressed JSONs table + + Private attributes: + _col_digest -- Digest over uncompressed JSON column + _col_month_idx -- Month index column + _col_data -- Compressed JSON column + """ + # Table name + TABLE_NAME = "compressed_json" + + def __init__(self, adb: AlsDatabase) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + """ + super().__init__(adb=adb, table_name=self.TABLE_NAME, + json_obj_name="Request/Response") + self._col_digest = self.get_column("compressed_json_digest", + sa_pg.UUID) + self._col_month_idx = self.get_month_idx_col() + self._col_data = self.get_column("compressed_json_data", + sa.LargeBinary) + + def _make_rows(self, data_key: uuid.UUID, data_object: JSON_DATA_TYPE, + month_idx: int) -> List[ROW_DATA_TYPE]: + """ Makes row dictionary + + Arguments: + data_key -- Digest over JSON + data_object -- JSON itself + month_idx -- Month index + """ + return [{ms(self._col_digest.name): data_key.urn, + ms(self._col_month_idx.name): month_idx, + ms(self._col_data.name): + lz4.frame.compress(BytesUtils.json_to_bytes(data_object))}] + + +class MaxEirpTableUpdater(TableUpdaterBase[uuid.UUID, JSON_DATA_TYPE]): + """ Updater for Max EIRP table. + Data key is digest, used in request_response table (i.e. digest computed + over AlsMessageBundle.RequestResponse.invariant_json). + Data value is this object itself + (AlsMessageBundle.RequestResponse.invariant_json) + + Private attributes: + _col_digest -- Digest over invariant_json column + _col_month_idx -- Month index column + _col_op_class -- Channel operating class column + _col_channel -- Channel number column + _col_eirp -- Maximum EIRP in dBm column + """ + # Table name + TABLE_NAME = "max_eirp" + + def __init__(self, adb: AlsDatabase) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + """ + super().__init__(adb=adb, table_name=self.TABLE_NAME, + json_obj_name="AvailableChannelInfo") + self._col_digest = self.get_column("request_response_digest", + sa_pg.UUID) + self._col_month_idx = self.get_month_idx_col() + self._col_op_class = self.get_column("channel", sa.SmallInteger) + self._col_channel = self.get_column("channel", sa.SmallInteger) + self._col_eirp = self.get_column("max_eirp_dbm", sa.Float) + + def _make_rows(self, data_key: uuid.UUID, data_object: JSON_DATA_TYPE, + month_idx: int) -> List[ROW_DATA_TYPE]: + """ Prepares list of row dictionaries + + Arguments: + data_key -- Digest over 'invariant_json' + data_object -- 'invariant_json' itself + month_idx -- Month index + Returns list or row dictionaries + """ + ret: List[ROW_DATA_TYPE] = [] + try: + for av_chan_info in data_object: + av_chan_info_j = jd(av_chan_info) + op_class: int = av_chan_info_j["globalOperatingClass"] + for channel, eirp in zip(av_chan_info_j["channelCfi"], + av_chan_info_j["maxEirp"]): + ret.append({ms(self._col_digest.name): data_key.urn, + ms(self._col_month_idx.name): month_idx, + ms(self._col_op_class.name): op_class, + ms(self._col_channel.name): channel, + ms(self._col_eirp.name): eirp}) + except (LookupError, TypeError, ValueError) as ex: + raise \ + JsonFormatError(f"Invalid AvailableChannelInfo format: '{ex}'", + code_line=LineNumber.exc(), data=data_object) + return ret + + +class MaxPsdTableUpdater(TableUpdaterBase[uuid.UUID, JSON_DATA_TYPE]): + """ Updater for Max PSD table. + Data key is digest, used in request_response table (i.e. digest computed + over AlsMessageBundle.RequestResponse.invariant_json). + Data value is thsi object itsef + (AlsMessageBundle.RequestResponse.invariant_json) + + Private attributes: + _col_digest -- Digest over invariant_json column + _col_month_idx -- Month index column + _col_low -- Lower frequency range bound in MHz column + _col_high -- High frequency range bound in MHz column + _col_psd -- Maximum PSD in dBm/MHz column + """ + TABLE_NAME = "max_psd" + + def __init__(self, adb: AlsDatabase) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + """ + super().__init__(adb=adb, table_name=self.TABLE_NAME, + json_obj_name="AvailableFrequencyInfo") + self._col_digest = self.get_column("request_response_digest", + sa_pg.UUID) + self._col_month_idx = self.get_month_idx_col() + self._col_low = self.get_column("low_frequency_mhz", sa.SmallInteger) + self._col_high = self.get_column("high_frequency_mhz", sa.SmallInteger) + self._col_psd = self.get_column("max_psd_dbm_mhz", sa.Float) + + def _make_rows(self, data_key: uuid.UUID, data_object: JSON_DATA_TYPE, + month_idx: int) -> List[ROW_DATA_TYPE]: + """ Prepares list of row dictionaries + + Arguments: + data_key -- Digest over 'invariant_json' + data_object -- 'invariant_json' itself + month_idx -- Month index + Returns list or row dictionaries + """ + ret: List[ROW_DATA_TYPE] = [] + try: + for av_freq_info in data_object: + av_freq_info_j = jd(av_freq_info) + freq_range = jd(av_freq_info_j["frequencyRange"]) + ret.append( + {ms(self._col_digest.name): data_key.urn, + ms(self._col_month_idx.name): month_idx, + ms(self._col_low.name): freq_range["lowFrequency"], + ms(self._col_high.name): freq_range["highFrequency"], + ms(self._col_psd.name): av_freq_info_j["maxPsd"]}) + except (LookupError, TypeError, ValueError) as ex: + raise \ + JsonFormatError( + f"Invalid AvailableFrequencyInfo format: '{ex}'", + code_line=LineNumber.exc(), data=data_object) + return ret + + +class RequestResponseTableUpdater(TableUpdaterBase[uuid.UUID, JSON_DATA_TYPE]): + """ Updater for request/response table + Data key is digest, used in request_response table (i.e. digest computed + over AlsMessageBundle.RequestResponse.invariant_json). + Data value is thsi object itsef + (AlsMessageBundle.RequestResponse.invariant_json) + + Private attributes: + _afc_config_lookup -- AFC Config lookup + _customer_lookup -- Customer name lookup + _uls_lookup -- ULS ID lookup + _geo_data_lookup -- Geodetic data lookup + _compressed_json_updater -- Compressed JSON table updater + _dev_desc_updater -- Device Descriptor table updater + _location_updater -- Location table updater + _max_eirp_updater -- Maximum EIRP table updater + _max_psd_updater -- Maximum PSD table updater + _col_digest -- Digest over 'invariant_json' column + _col_month_idx -- Month index column + _col_afc_config_digest -- AFC Config digest column + _col_customer_id -- Customer ID column + _col_uls_data_id -- ULS Data ID column + _col_geo_data_id -- Geodetic data ID column + _col_req_digest -- Request digest column + _col_resp_digest -- Response digest column + _col_dev_desc_digest -- Device Descriptor digets column + _col_loc_digest -- Location digest column + _col_response_code -- Response code column + _col_response_description -- Response description column + _col_response_data -- Response data column + """ + TABLE_NAME = "request_response" + + def __init__(self, adb: AlsDatabase, afc_config_lookup: AfcConfigLookup, + customer_lookup: StringLookup, uls_lookup: StringLookup, + geo_data_lookup: StringLookup, + compressed_json_updater: CompressedJsonTableUpdater, + dev_desc_updater: DeviceDescriptorTableUpdater, + location_updater: LocationTableUpdater, + max_eirp_updater: MaxEirpTableUpdater, + max_psd_updater: MaxPsdTableUpdater) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + afc_config_lookup -- AFC Config lookup + customer_lookup -- Customer name lookup + uls_lookup -- ULS ID lookup + geo_data_lookup -- Geodetic data lookup + compressed_json_updater -- Compressed JSON table updater + dev_desc_updater -- Device Descriptor table updater + location_updater -- Location table updater + max_eirp_updater -- Maximum EIRP table updater + max_psd_updater -- Maximum PSD table updater + """ + super().__init__(adb=adb, table_name=self.TABLE_NAME, + json_obj_name="RequestResponse") + self._afc_config_lookup = afc_config_lookup + self._customer_lookup = customer_lookup + self._uls_lookup = uls_lookup + self._geo_data_lookup = geo_data_lookup + self._compressed_json_updater = compressed_json_updater + self._dev_desc_updater = dev_desc_updater + self._location_updater = location_updater + self._max_eirp_updater = max_eirp_updater + self._max_psd_updater = max_psd_updater + self._col_digest = self.get_column("request_response_digest", + sa_pg.UUID) + self._col_month_idx = self.get_month_idx_col() + self._col_afc_config_digest = self.get_column("afc_config_text_digest", + sa_pg.UUID) + self._col_customer_id = self.get_column("customer_id", sa.Integer) + self._col_uls_data_id = self.get_column("uls_data_version_id", + sa.Integer) + self._col_geo_data_id = self.get_column("geo_data_version_id", + sa.Integer) + self._col_req_digest = self.get_column("request_json_digest", + sa_pg.UUID) + self._col_resp_digest = self.get_column("response_json_digest", + sa_pg.UUID) + self._col_dev_desc_digest = self.get_column("device_descriptor_digest", + sa_pg.UUID) + self._col_loc_digest = self.get_column("location_digest", sa_pg.UUID) + self._col_response_code = self.get_column("response_code", sa.Integer) + self._col_response_description = \ + self.get_column("response_description", sa.Text) + self._col_response_data = self.get_column("response_data", sa.Text) + + def _update_lookups(self, data_objects: Iterable[JSON_DATA_TYPE], + month_idx: int) -> None: + """ Update used lookups + + Arguments: + data_objects -- Sequence of 'invariant_json' objects + month_idx -- Month index + """ + configs: List[str] = [] + customers: List[str] = [] + ulss: List[str] = [] + geos: List[str] = [] + for json_obj in data_objects: + json_object = jd(json_obj) + configs.append( + js(json_object[AlsMessageBundle.JRR_CONFIG_TEXT_KEY])) + customers.append( + js(json_object[AlsMessageBundle.JRR_CUSTOMER_KEY])) + ulss.append(js(json_object[AlsMessageBundle.JRR_ULS_KEY])) + geos.append(js(json_object[AlsMessageBundle.JRR_GEO_KEY])) + self._afc_config_lookup.update_db(values=configs, month_idx=month_idx) + self._customer_lookup.update_db(values=customers, month_idx=month_idx) + self._uls_lookup.update_db(values=ulss, month_idx=month_idx) + self._geo_data_lookup.update_db(values=geos, month_idx=month_idx) + + def _make_rows(self, data_key: uuid.UUID, data_object: JSON_DATA_TYPE, + month_idx: int) -> List[ROW_DATA_TYPE]: + """ Makes database row dictionary + + Arguments: + data_key -- Digest of 'invariant_json' object + data_object -- 'invariant_json' object itself + month_idx -- Month index + Returns single-element row dictionary + """ + try: + json_dict = jd(data_object) + resp_status: Dict[str, Any] = \ + jd(json_dict[AlsMessageBundle.JRR_RESPONSE_KEY]["response"]) + success = resp_status["responseCode"] == 0 + resp_data: Optional[str] = None + if not success: + resp_data = "" + for field_name, field_value in \ + (resp_status.get("supplementalInfo", {}) or + {}).items(): + if field_value and \ + (field_name in ("missingParams", "invalidParams", + "unexpectedParams")): + resp_data += \ + ("," if resp_data else "") + ",".join(field_value) + return [{ + ms(self._col_digest.name): data_key.urn, + ms(self._col_month_idx.name): month_idx, + ms(self._col_afc_config_digest.name): + self._afc_config_lookup.key_for_value( + json_dict[AlsMessageBundle.JRR_CONFIG_TEXT_KEY], + month_idx=month_idx).urn, + ms(self._col_customer_id.name): + self._customer_lookup.key_for_value( + json_dict[AlsMessageBundle.JRR_CUSTOMER_KEY], + month_idx=month_idx), + ms(self._col_uls_data_id.name): + self._uls_lookup.key_for_value( + json_dict[AlsMessageBundle.JRR_ULS_KEY], + month_idx=month_idx), + ms(self._col_geo_data_id.name): + self._geo_data_lookup.key_for_value( + json_dict[AlsMessageBundle.JRR_GEO_KEY], + month_idx=month_idx), + ms(self._col_req_digest.name): + BytesUtils.json_to_uuid( + json_dict[AlsMessageBundle.JRR_REQUEST_KEY]).urn, + ms(self._col_resp_digest.name): + BytesUtils.json_to_uuid( + json_dict[AlsMessageBundle.JRR_RESPONSE_KEY]).urn, + ms(self._col_dev_desc_digest.name): + BytesUtils.json_to_uuid( + json_dict[AlsMessageBundle.JRR_REQUEST_KEY][ + "deviceDescriptor"]).urn, + ms(self._col_loc_digest.name): + BytesUtils.json_to_uuid( + json_dict[AlsMessageBundle.JRR_REQUEST_KEY][ + "location"]).urn, + ms(self._col_response_code.name): + json_dict[AlsMessageBundle.JRR_RESPONSE_KEY][ + "response"]["responseCode"], + ms(self._col_response_description.name): + resp_status.get("shortDescription"), + ms(self._col_response_data.name): resp_data}] + except (LookupError, TypeError, ValueError) as ex: + raise JsonFormatError( + f"Invalid Request or Response format: '{ex}'", + code_line=LineNumber.exc(), data=data_object) + + def _update_foreign_targets( + self, + row_infos: Dict[uuid.UUID, + Tuple[JSON_DATA_TYPE, List[ROW_DATA_TYPE]]], + month_idx: int) -> None: + """ Updates tables this one references + + Arguments: + row_infos -- Dictionary of data objects and row lists generated from + them, indexed by data keys + month_idx -- Month index + """ + updated_jsons: Dict[uuid.UUID, JSON_DATA_TYPE] = {} + updated_dev_desc: Dict[uuid.UUID, JSON_DATA_TYPE] = {} + updated_locations: Dict[uuid.UUID, JSON_DATA_TYPE] = {} + for json_obj, rows in row_infos.values(): + json_object = jd(json_obj) + row = rows[0] + updated_jsons[ + uuid.UUID(js(row[ms(self._col_req_digest.name)]))] = \ + json_object[AlsMessageBundle.JRR_REQUEST_KEY] + updated_jsons[ + uuid.UUID(js(row[ms(self._col_resp_digest.name)]))] = \ + json_object[AlsMessageBundle.JRR_RESPONSE_KEY] + updated_dev_desc[uuid.UUID( + js(row[ms(self._col_dev_desc_digest.name)]))] = \ + jd(json_object[AlsMessageBundle.JRR_REQUEST_KEY])[ + "deviceDescriptor"] + updated_locations[ + uuid.UUID(js(row[ms(self._col_loc_digest.name)]))] = \ + jd(jd(json_object[AlsMessageBundle.JRR_REQUEST_KEY])[ + "location"]) + self._compressed_json_updater.update_db(data_dict=updated_jsons, + month_idx=month_idx) + self._dev_desc_updater.update_db(data_dict=updated_dev_desc, + month_idx=month_idx) + self._location_updater.update_db(data_dict=updated_locations, + month_idx=month_idx) + + def _update_foreign_sources( + self, + inserted_rows: Dict[uuid.UUID, Tuple[ROW_DATA_TYPE, + JSON_DATA_TYPE, + RESULT_ROW_DATA_TYPE]], + month_idx: int) -> None: + """ Updates compressed JSONs, device descriptors, locations, EIRPs, PSD + tables for those table rows that were inserted + + Arguments: + inserted_rows -- Ordered by digest rows/objects/inserted rows that + were inserted + conflicting rows -- Ordered by digest rows/objects that were not + inserted (ignored) + month_idx -- Month index + """ + updated_eirps: Dict[uuid.UUID, JSON_DATA_TYPE] = {} + updated_psds: Dict[uuid.UUID, JSON_DATA_TYPE] = {} + for digest, (_, json_obj, _) in inserted_rows.items(): + json_object = jd(json_obj) + updated_eirps[digest] = \ + json_object[AlsMessageBundle.JRR_RESPONSE_KEY].get( + "availableChannelInfo") or [] + updated_psds[digest] = \ + json_object[AlsMessageBundle.JRR_RESPONSE_KEY].get( + "availableFrequencyInfo") or [] + self._max_eirp_updater.update_db(data_dict=updated_eirps, + month_idx=month_idx) + self._max_psd_updater.update_db(data_dict=updated_psds, + month_idx=month_idx) + + +class EnvelopeTableUpdater(TableUpdaterBase[uuid.UUID, JSON_DATA_TYPE]): + """ Request/response envelope tables. + Keys are digests over envelope JSON + Values are envelope JSONs themselves + + Private attributes + _col_digest -- Digest column + _col_month_idx -- Month index column + _col_data -- Envelope JSON column + """ + # Parameters + Params = NamedTuple( + "Params", + # Table name + [("table_name", str), + # Name of digest column + ("digest_col_name", str), + # Name of envelope JSON column + ("value_col_name", str)]) + # Parameters for AFC Request envelope table + RX_ENVELOPE_PARAMS = Params(table_name="rx_envelope", + digest_col_name="rx_envelope_digest", + value_col_name="envelope_json") + # Parameters for AFC Response envelope table + TX_ENVELOPE_PARAMS = Params(table_name="tx_envelope", + digest_col_name="tx_envelope_digest", + value_col_name="envelope_json") + + def __init__(self, adb: AlsDatabase, + params: "EnvelopeTableUpdater.Params") -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + params -- Table parameters + """ + super().__init__(adb=adb, table_name=params.table_name, + json_obj_name="RequestResponseMessageEnvelope") + self._col_digest = self.get_column(params.digest_col_name, + sa_pg.UUID) + self._col_month_idx = self.get_month_idx_col() + self._col_data = self.get_column(params.value_col_name, sa_pg.JSON) + + def _make_rows(self, data_key: uuid.UUID, data_object: JSON_DATA_TYPE, + month_idx: int) -> List[ROW_DATA_TYPE]: + """ Makes row dictionary + + Arguments: + data_key -- Digest of envelope JSON + data_object -- Envelope JSON + month_idx -- Month index + """ + return \ + [{ms(self._col_digest.name): data_key.urn, + ms(self._col_month_idx.name): month_idx, + ms(self._col_data.name): data_object}] + + +# Type for key of request/response association key data +RequestResponseAssociationTableDataKey = \ + NamedTuple( + "RequestResponseAssociationTableDataKey", + # Serial ID of AFC message + [("message_id", int), + # Id of request/response within message + ("request_id", str)]) + + +class RequestResponseAssociationTableUpdater( + TableUpdaterBase[RequestResponseAssociationTableDataKey, + AlsMessageBundle.RequestResponse]): + """ Updater of request/response association table (intermediary between + message table and request/response table) + + Private attributes: + _col_message_id -- Message serial ID column + _col_req_id -- Request ID column + _col_month_idx -- Month index column + _col_rr_digest -- Request/response (invariant_json) digest + column + _col_expire_time -- Response expiration time column + _request_response_updater -- Request/response table updater + """ + TABLE_NAME = "request_response_in_message" + + def __init__( + self, adb: AlsDatabase, + request_response_updater: RequestResponseTableUpdater) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + request_response_updater -- Request/response table updater + """ + super().__init__( + adb=adb, table_name=self.TABLE_NAME, + json_obj_name="RequestResponseAssociation") + self._col_message_id = self.get_column("message_id", sa.BigInteger) + self._col_req_id = self.get_column("request_id", sa.Text) + self._col_month_idx = self.get_month_idx_col() + self._col_rr_digest = self.get_column("request_response_digest", + sa_pg.UUID) + self._col_expire_time = self.get_column("expire_time", + sa.DateTime) + self._request_response_updater = request_response_updater + + def _make_rows(self, + data_key: RequestResponseAssociationTableDataKey, + data_object: AlsMessageBundle.RequestResponse, + month_idx: int) -> List[ROW_DATA_TYPE]: + """ Make row dictionary + + Argument: + data_key -- Row in message table and request index + data_object -- AlsMessageBundle.RequestResponse object + month_idx -- Month index + Returns single-element row dictionary list + """ + return [{ms(self._col_message_id.name): data_key.message_id, + ms(self._col_req_id.name): data_key.request_id, + ms(self._col_month_idx.name): month_idx, + ms(self._col_rr_digest.name): + BytesUtils.json_to_uuid(data_object.invariant_json).urn, + ms(self._col_expire_time.name): data_object.expire_time}] + + def _update_foreign_targets( + self, + row_infos: Dict[RequestResponseAssociationTableDataKey, + Tuple[AlsMessageBundle.RequestResponse, + List[ROW_DATA_TYPE]]], + month_idx: int) -> None: + """ Updates tables pointed to by foreign keys of this table. + + Arguments: + row_infos -- Dictionary of data objects and row lists generated from + them, indexed by data keys + month_idx -- Value for 'month_idx' column + """ + self._request_response_updater.update_db( + data_dict={ + uuid.UUID(js(rows[0][ms(self._col_rr_digest.name)])): + rr.invariant_json + for rr, rows in row_infos.values()}, + month_idx=month_idx) + + +class DecodeErrorTableWriter(AlsTableBase): + """ Writer of decode error table + + Private attributes: + _col_msg -- Error message column + _col_line -- Code line number column + _col_data -- Supplementary data column + _col_time -- Timetag column + _col_month_idx -- Month index column + """ + TABLE_NAME = "decode_error" + + def __init__(self, adb: AlsDatabase) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + """ + super().__init__(adb=adb, table_name=self.TABLE_NAME) + self._col_id = self.get_column("id", sa.BigInteger) + self._col_time = self.get_column("time", sa.DateTime) + self._col_msg = self.get_column("msg", sa.Text) + self._col_line = self.get_column("code_line", sa.Integer) + self._col_data = self.get_column("data", sa.Text) + self._col_month_idx = self.get_month_idx_col() + self._conn = self._adb.engine.connect() + + def write_decode_error( + self, msg: str, line: Optional[int], + data: Optional[Union[bytes, str, JSON_DATA_TYPE]] = None) -> None: + """ Writes decode error to table + + Arguments: + msg -- Error message + line -- Script line number + data -- Supplementary data + """ + if isinstance(data, bytes): + data = data.decode("latin-1") + elif isinstance(data, (list, dict)): + data = json.dumps(data) + ins = sa.insert(self._table).values( + {ms(self._col_month_idx.name): get_month_idx(), + ms(self._col_msg.name): msg, + ms(self._col_line.name): line, + ms(self._col_data.name): data, + ms(self._col_time.name): + datetime.datetime.now(dateutil.tz.tzlocal())}) + self._conn.execute(ins) + + +class AfcMessageTableUpdater(TableUpdaterBase[int, AlsMessageBundle]): + """ AFC Message table + Keys are, for no better alternatives, 0-based indices of messages, passed + to update_db() + Data objects are AlsMessageBundle objects + + Private attributes: + _col_message_id -- Message serial ID column + _col_month_idx -- Month index column + _col_afc_server -- AFC Server ID column + _col_rx_time -- AFC Request timetag column + _col_tx_time -- AFC Response timetag column + _rx_envelope_digest -- AFC Request envelope digest column + _tx_envelope_digest -- AFC Response envelope digest column + _afc_server_lookup -- Lookup fr AFC Server names + _rr_assoc_updater -- Updater of message to request/response association + table + _rx_envelope_updater -- Updater for AFC Request envelope table + _tx_envelope_updater -- Updater for AFC Response envelope table + _decode_error_writer -- Decode error table writer + """ + # Table name + TABLE_NAME = "afc_message" + + def __init__(self, adb: AlsDatabase, afc_server_lookup: StringLookup, + rr_assoc_updater: RequestResponseAssociationTableUpdater, + rx_envelope_updater: EnvelopeTableUpdater, + tx_envelope_updater: EnvelopeTableUpdater, + decode_error_writer: DecodeErrorTableWriter) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object + rr_assoc_updater -- Updater of message to request/response + association table + rx_envelope_updater -- Updater for AFC Request envelope table + tx_envelope_updater -- Updater for AFC Response envelope table + decode_error_writer -- Decode error table writer + """ + super().__init__(adb=adb, table_name=self.TABLE_NAME, + json_obj_name="AlsMessageBundle", + data_key_column_names=["message_id"]) + self._col_message_id = self.get_column("message_id", sa.BigInteger) + self._col_month_idx = self.get_month_idx_col() + self._col_afc_server = self.get_column("afc_server", sa.Integer) + self._col_rx_time = self.get_column("rx_time", sa.DateTime) + self._col_tx_time = self.get_column("tx_time", sa.DateTime) + self._rx_envelope_digest = self.get_column("rx_envelope_digest", + sa_pg.UUID) + self._tx_envelope_digest = self.get_column("tx_envelope_digest", + sa_pg.UUID) + + self._afc_server_lookup = afc_server_lookup + self._rr_assoc_updater = rr_assoc_updater + self._rx_envelope_updater = rx_envelope_updater + self._tx_envelope_updater = tx_envelope_updater + self._decode_error_writer = decode_error_writer + + def _update_lookups(self, data_objects: Iterable[AlsMessageBundle], + month_idx: int) -> None: + """ Update used lookups + + Arguments: + data_objects -- Sequence of AlsMessageBundle being added to database + month_idx -- Month index + """ + self._afc_server_lookup.update_db( + values=[b.take_apart().afc_server for b in data_objects], + month_idx=month_idx) + + def _make_rows(self, data_key: int, data_object: AlsMessageBundle, + month_idx: int) -> List[ROW_DATA_TYPE]: + """ Makes table row dictionary from data object + + Arguments: + data_key -- Data key (sequential index in dictionary, passed to + update_db()) + data_object -- AlsMessageBundle to make row from + month_idx -- Month index + Single-element list of row dictionaries + """ + parts = data_object.take_apart() + for orphans, name in \ + [(parts.orphan_requests, "Requests without responses"), + (parts.orphan_responses, "Responses without requests")]: + for orphan in orphans: + self._decode_error_writer.write_decode_error( + msg=f"{name} received from {parts.afc_server}", + line=LineNumber.current(), + data=orphan) + return [{ms(self._col_month_idx.name): month_idx, + ms(self._col_afc_server.name): + self._afc_server_lookup.key_for_value(parts.afc_server, + month_idx), + ms(self._col_rx_time.name): parts.rx_timetag, + ms(self._col_tx_time.name): parts.tx_timetag, + ms(self._rx_envelope_digest.name): + BytesUtils.json_to_uuid(parts.rx_envelope).urn, + ms(self._tx_envelope_digest.name): + BytesUtils.json_to_uuid(parts.tx_envelope).urn}] + + def _update_foreign_targets( + self, + row_infos: Dict[int, Tuple[AlsMessageBundle, List[ROW_DATA_TYPE]]], + month_idx: int) -> None: + """ Update RX/TX envelopes that are not yet in database + + Arguments: + row_infos -- Dictionary of data objects and row lists generated from + them, indexed by data keys + month_idx -- Month index + """ + self._rx_envelope_updater.update_db( + data_dict={uuid.UUID(js(rows[0] + [ms(self._rx_envelope_digest.name)])): + bundle.take_apart().rx_envelope + for bundle, rows in row_infos.values()}, + month_idx=month_idx) + self._tx_envelope_updater.update_db( + data_dict={uuid.UUID(js(rows[0] + [ms(self._tx_envelope_digest.name)])): + bundle.take_apart().tx_envelope + for bundle, rows in row_infos.values()}, + month_idx=month_idx) + + def _update_foreign_sources( + self, + inserted_rows: Dict[int, Tuple[ROW_DATA_TYPE, + AlsMessageBundle, + RESULT_ROW_DATA_TYPE]], + month_idx: int) -> None: + """ Updates request/response association tables and (for unique + requests/responses) dependent tables + + inserted_rows -- Information about inserted rows - row dictionaries, + data objects, result rows. Ordered by 0-based + indices of inserted rows + month_idx -- Month index + """ + rr_dict: Dict[RequestResponseAssociationTableDataKey, + AlsMessageBundle.RequestResponse] = {} + for _, message_bundle, inserted_row in inserted_rows.values(): + parts = message_bundle.take_apart() + for req_id, request_response in parts.request_responses.items(): + rr_dict[ + RequestResponseAssociationTableDataKey( + message_id=ji(inserted_row[0]), request_id=req_id)] = \ + request_response + self._rr_assoc_updater.update_db(data_dict=rr_dict, + month_idx=month_idx) + + def _data_key_from_result_row(self, result_row: Tuple[Any, ...], + result_row_idx: int) -> int: + """ Data key from rows written to database + + Arguments: + result_row -- Insert result tuple + result_row_idx -- 0-based index i insert results + Returns the latter + """ + return result_row_idx + + +class IncompleteAlsBundles: + """ Collection of ALS bundles, for which not all messages arrived yet + + Private attributes: + _kafka_positions -- Collection of uncommitted Kafka positions + _bundle_queue -- Heap queue of ALS bundles, arranged by last update + _bundle_map -- Maps Kafka message keys to bundles + """ + + def __init__(self, kafka_positions: KafkaPositions) -> None: + """ Constructor + + Arguments: + kafka_positions -- Collection of uncommitted Kafka positions + """ + self._kafka_positions = kafka_positions + self._bundle_queue: List[AlsMessageBundle] = [] + self._bundle_map: Dict[AlsMessageKeyType, AlsMessageBundle] = {} + + def add_message(self, message_key: AlsMessageKeyType, message: AlsMessage, + kafka_position: KafkaPosition) -> bool: + """ Adds ALS message + + Arguments: + message_key -- Kafka message key + message -- AlsMessage + kafka_position -- Message's position in Kafka queue + Returns True if new bundle was created + """ + ret = False + bundle = self._bundle_map.get(message_key) + if bundle is None: + ret = True + bundle = AlsMessageBundle(message_key=message_key, + kafka_positions=self._kafka_positions) + heapq.heappush(self._bundle_queue, bundle) + self._bundle_map[message_key] = bundle + bundle.update(message, kafka_position) + heapq.heapify(self._bundle_queue) + return ret + + def get_oldest_bundle(self) -> Optional[AlsMessageBundle]: + """ Get the oldest bundle (None if collection is empty) """ + return self._bundle_queue[0] if self._bundle_queue else None + + def get_incomplete_count(self) -> int: + """ Number of incomplete (not yet assembled) bundles """ + return sum((0 if b.assembled() else 1) for b in self._bundle_queue) + + def remove_oldest_bundle(self) -> None: + """ Removes oldest bundle from collection """ + assert self._bundle_queue + ret = heapq.heappop(self._bundle_queue) + del self._bundle_map[ret.message_key()] + + def fetch_assembled( + self, max_bundles: Optional[int] = None, + max_requests: Optional[int] = None) -> List[AlsMessageBundle]: + """ Fetch and remove from collection assembled bundles (all or some) + + Arguments: + max_bundles -- Maximum number of bundles or None + max_requests -- Maximum total number of requests or None + Returns list of bundles + """ + ret: List[AlsMessageBundle] = [] + idx = 0 + num_requests = 0 + while (idx < len(self._bundle_queue)) and \ + ((max_bundles is None) or (len(ret) < max_bundles)): + bundle = self._bundle_queue[idx] + if not bundle.assembled(): + idx += 1 + continue + if (max_requests is not None) and \ + ((num_requests + bundle.request_count()) > max_requests): + break + ret.append(bundle) + del self._bundle_map[bundle.message_key()] + self._bundle_queue.pop(idx) + heapq.heapify(self._bundle_queue) + return ret + + +class KafkaClient: + """ Wrapper over confluent_kafka.Consumer object + + Private attributes: + _consumer -- confluent_kafka.Consumer object + _subscribed_topics -- Set of currently subscribed topics + _resubscribe_interval -- Minimum time interval before subscription + checks + _last_subscription_check -- Moment when subscription was last time checked + _subscribe_als -- True if ALS topic should be subscribed + _subscribe_log -- True if log topics should be subscribed + _metrics -- Metric collection + """ + # Kafka message data + MessageInfo = \ + NamedTuple( + "MessageInfo", + # Message position (topic/partition/offset) + [("position", KafkaPosition), + # Message raw key + ("key", Optional[bytes]), + # Message raw value + ("value", bytes)]) + + class _ArgDsc(NamedTuple): + """ confluent_kafka.Consumer() config argument descriptor """ + # confluent_kafka.Consumer() parameter + config: str + + # Correspondent command line parameter (if any) + cmdline: Optional[str] = None + + # Default value + default: Any = None + + def get_value(self, args: Any) -> Any: + """ Returns value for parameter (from command line or default) + + Arguments: + args -- Parsed command line object + Returns None or parameter value + """ + assert (self.cmdline is not None) or (self.default is not None) + ret: Any = None + if self.cmdline is not None: + assert hasattr(args, self.cmdline) + ret = getattr(args, self.cmdline) + return ret if ret is not None else self.default + + # Supported confluent_kafka.Consumer() config arguments + _ARG_DSCS = [ + _ArgDsc(config="bootstrap.servers", cmdline="kafka_servers"), + _ArgDsc(config="client.id", cmdline="kafka_client_id"), + _ArgDsc(config="security.protocol", cmdline="kafka_security_protocol"), + _ArgDsc(config="ssl.keystore.location", cmdline="kafka_ssl_keyfile"), + _ArgDsc(config="ssl.truststore.location", cmdline="kafka_ssl_cafile"), + _ArgDsc(config="ssl.cipher.suites", cmdline="kafka_ssl_ciphers"), + _ArgDsc(config="max.partition.fetch.bytes", + cmdline="kafka_max_partition_fetch_bytes"), + _ArgDsc(config="enable.auto.commit", default=True), + _ArgDsc(config="group.id", default="ALS"), + _ArgDsc(config="auto.offset.reset", default="earliest")] + + def __init__(self, args: Any, subscribe_als: bool, subscribe_log: bool, + resubscribe_interval_s: int) -> None: + """ Constructor + + Arguments: + args -- Parsed command line parameters + subscribe_als -- True if ALS topic should be subscribed + subscribe_log -- True if log topics should be subscribed + resubscribe_interval_s -- How often (interval in seconds) + subscription_check() will actually check + subscription. 0 means - on each call. + """ + config: Dict[str, Any] = {} + for ad in self._ARG_DSCS: + v = ad.get_value(args) + if v is not None: + config[ad.config] = v + if config.get("client.id", "").endswith("@"): + config["client.id"] = config["client.id"][:-1] + \ + "".join(f"{b:02X}" for b in os.urandom(10)) + try: + self._consumer = confluent_kafka.Consumer(config) + except confluent_kafka.KafkaException as ex: + logging.error(f"Error creating Kafka Consumer: {ex.args[0].str}") + raise + self._subscribe_als = subscribe_als + self._subscribe_log = subscribe_log + self._resubscribe_interval = \ + datetime.timedelta(seconds=resubscribe_interval_s) + self._subscribed_topics: Set[str] = set() + self._last_subscription_check = \ + datetime.datetime.now() - self._resubscribe_interval + self._metrics = \ + Metrics([("Gauge", "siphon_fetched_offsets", + "Fetched Kafka offsets", ["topic", "partition"]), + ("Counter", "siphon_kafka_errors", + "Messages delivered with errors", ["topic", "code"]), + ("Gauge", "siphon_comitted_offsets", + "Comitted Kafka offsets", ["topic", "partition"])]) + + def subscription_check(self) -> None: + """ If it's time - check if new matching topics arrived and resubscribe + if so """ + if (datetime.datetime.now() - self._last_subscription_check) < \ + self._resubscribe_interval: + return + try: + current_topics: Set[str] = set() + for topic in self._consumer.list_topics().topics.keys(): + if (self._subscribe_als and (topic == ALS_KAFKA_TOPIC)) or \ + (self._subscribe_log and + (not topic.startswith("__"))): + current_topics.add(topic) + if current_topics <= self._subscribed_topics: + return + self._consumer.subscribe(list(current_topics)) + self._subscribed_topics = current_topics + self._last_subscription_check = datetime.datetime.now() + except confluent_kafka.KafkaException as ex: + logging.error(f"Topic subscription error: {ex.args[0].str}") + raise + + def poll(self, timeout_ms: int, max_records: int) \ + -> Dict[str, List["KafkaClient.MessageInfo"]]: + """ Poll for new messages + + Arguments: + timeout_ms -- Poll timeout in milliseconds. 0 to return immediately + max_records -- Maximum number of records to poll + Returns by-topic dictionary of MessageInfo objects + """ + timeout_s = timeout_ms / 1000 + try: + fetched_offsets: Dict[Tuple[str, int], int] = {} + ret: Dict[str, List["KafkaClient.MessageInfo"]] = {} + start_time = datetime.datetime.now() + for _ in range(max_records): + message = self._consumer.poll(timeout_s) + if (message is None) or \ + ((datetime.datetime.now() - + start_time).total_seconds() > timeout_s): + break + kafka_error = message.error() + topic = message.topic() + if kafka_error is not None: + self._metrics.siphon_kafka_errors( + message.topic() or "None", + str(kafka_error.code())).inc() + else: + partition = message.partition() + offset = message.offset() + ret.setdefault(message.topic(), []).\ + append( + self.MessageInfo( + position=KafkaPosition( + topic=topic, partition=partition, + offset=offset), + key=message.key(), + value=message.value())) + previous_offset = \ + fetched_offsets.setdefault((topic, partition), -1) + fetched_offsets[(topic, partition)] = \ + max(previous_offset, offset) + except confluent_kafka.KafkaException as ex: + logging.error(f"Message fetch error: {ex.args[0].str}") + raise + for (topic, partition), offset in fetched_offsets.items(): + self._metrics.siphon_fetched_offsets(str(topic), + str(partition)).set(offset) + return ret + + def commit(self, positions: Dict[str, Dict[int, int]]) -> None: + """ Commit given message positions + + Arguments: + positions -- By-topic then by-partition maximum committed offsets + """ + offsets: List[confluent_kafka.TopicPartition] = [] + for topic, offset_dict in positions.items(): + for partition, offset in offset_dict.items(): + offsets.append( + confluent_kafka.TopicPartition( + topic=topic, partition=partition, offset=offset + 1)) + self._metrics.siphon_comitted_offsets(topic, + partition).set(offset) + try: + self._consumer.commit(offsets=offsets) + except confluent_kafka.KafkaException as ex: + logging.error(f"Offset commit error: {ex.args[0].str}") + raise + + +class Siphon: + """ Siphon (Kafka reader / DB updater + + Private attributes: + _adb -- AlsDatabase object or None + _ldb -- LogsDatabase object or None + _kafka_client -- KafkaClient consumer wrapper object + _decode_error_writer -- Decode error table writer + _lookups -- Lookup collection + _cert_lookup -- Certificates' lookup + _afc_config_lookup -- AFC Configs lookup + _afc_server_lookup -- AFC Server name lookup + _customer_lookup -- Customer name lookup + _uls_lookup -- ULS ID lookup + _geo_data_lookup -- Geodetic Data ID lookup + _dev_desc_updater -- DeviceDescriptor table updater + _location_updater -- Location table updater + _compressed_json_updater -- Compressed JSON table updater + _max_eirp_updater -- Maximum EIRP table updater + _max_psd_updater -- Maximum PSD table updater + _req_resp_updater -- Request/Response table updater + _rx_envelope_updater -- AFC Request envelope table updater + _tx_envelope_updater -- AFC Response envelope tabgle updater + _req_resp_assoc_updater -- Request/Response to Message association + table updater + _afc_message_updater -- Message table updater + _kafka_positions -- Nonprocessed Kafka positions' collection + _als_bundles -- Incomplete ALS Bundler collection + _metrics -- Collection of Prometheus metrics + """ + # Number of messages fetched from Kafka in single access + KAFKA_MAX_RECORDS = 1000 + # Kafka server access timeout if system is idle + KAFKA_IDLE_TIMEOUT_MS = 1000 + # Maximum age (time since last update) of ALS Bundle + ALS_MAX_AGE_SEC = 1000 + # Maximum number of requests in bundles to write to database + ALS_MAX_REQ_UPDATE = 5000 + + def __init__(self, adb: Optional[AlsDatabase], ldb: Optional[LogsDatabase], + kafka_client: KafkaClient) -> None: + """ Constructor + + Arguments: + adb -- AlsDatabase object or None + ldb -- LogsDatabase object or None + kafka_client -- KafkaClient + """ + error_if(not (adb or ldb), + "Neither ALS nor Logs database specified. Nothing to do") + self._metrics = \ + Metrics([("Counter", "siphon_kafka_polls", + "Number of Kafka polls"), + ("Counter", "siphon_als_received", + "Number of ALS records received from Kafka"), + ("Counter", "siphon_als_malformed", + "Number of malformed ALS records received from Kafka"), + ("Counter", "siphon_log_received", + "Number of LOG records received from Kafka", ["topic"]), + ("Counter", "siphon_log_malformed", + "Number of malformed LOG records received from Kafka", + ["topic"]), + ("Counter", "siphon_afc_msg_received", + "Number of AFC Request messages received"), + ("Counter", "siphon_afc_msg_completed", + "Number of completed AFC Request messages"), + ("Counter", "siphon_afc_req_completed", + "Number of completed AFC Requests"), + ("Counter", "siphon_afc_msg_dropped", + "Number of incomplete AFC Request messages"), + ("Gauge", "siphon_afc_msg_in_progress", + "Number of AFC Request messages awaiting completion")]) + self._adb = adb + self._ldb = ldb + self._kafka_client = kafka_client + if self._adb: + self._decode_error_writer = DecodeErrorTableWriter(adb=self._adb) + self._lookups = Lookups() + self._cert_lookup = CertificationsLookup(adb=self._adb, + lookups=self._lookups) + self._afc_config_lookup = AfcConfigLookup(adb=self._adb, + lookups=self._lookups) + self._afc_server_lookup = \ + StringLookup(adb=self._adb, + params=StringLookup.AFC_SERVER_PARAMS, + lookups=self._lookups) + self._customer_lookup = \ + StringLookup(adb=self._adb, + params=StringLookup.CUSTOMER_PARAMS, + lookups=self._lookups) + self._uls_lookup = \ + StringLookup(adb=self._adb, + params=StringLookup.ULS_PARAMS_PARAMS, + lookups=self._lookups) + self._geo_data_lookup = \ + StringLookup(adb=self._adb, + params=StringLookup.GEO_DATA_PARAMS, + lookups=self._lookups) + self._dev_desc_updater = \ + DeviceDescriptorTableUpdater( + adb=self._adb, cert_lookup=self._cert_lookup) + self._location_updater = LocationTableUpdater(adb=self._adb) + self._compressed_json_updater = \ + CompressedJsonTableUpdater(adb=self._adb) + self._max_eirp_updater = MaxEirpTableUpdater(adb=self._adb) + self._max_psd_updater = MaxPsdTableUpdater(adb=self._adb) + self._req_resp_updater = \ + RequestResponseTableUpdater( + adb=self._adb, afc_config_lookup=self._afc_config_lookup, + customer_lookup=self._customer_lookup, + uls_lookup=self._uls_lookup, + geo_data_lookup=self._geo_data_lookup, + compressed_json_updater=self._compressed_json_updater, + dev_desc_updater=self._dev_desc_updater, + location_updater=self._location_updater, + max_eirp_updater=self._max_eirp_updater, + max_psd_updater=self._max_psd_updater) + self._rx_envelope_updater = \ + EnvelopeTableUpdater( + adb=self._adb, + params=EnvelopeTableUpdater.RX_ENVELOPE_PARAMS) + self._tx_envelope_updater = \ + EnvelopeTableUpdater( + adb=self._adb, + params=EnvelopeTableUpdater.TX_ENVELOPE_PARAMS) + self._req_resp_assoc_updater = \ + RequestResponseAssociationTableUpdater( + adb=self._adb, + request_response_updater=self._req_resp_updater) + self._afc_message_updater = \ + AfcMessageTableUpdater( + adb=self._adb, afc_server_lookup=self._afc_server_lookup, + rr_assoc_updater=self._req_resp_assoc_updater, + rx_envelope_updater=self._rx_envelope_updater, + tx_envelope_updater=self._tx_envelope_updater, + decode_error_writer=self._decode_error_writer) + self._kafka_positions = KafkaPositions() + self._als_bundles = \ + IncompleteAlsBundles(kafka_positions=self._kafka_positions) + + def main_loop(self) -> None: + """ Read/write loop """ + busy = True + while True: + kafka_messages_by_topic = \ + self._kafka_client.poll( + timeout_ms=0 if busy else self.KAFKA_IDLE_TIMEOUT_MS, + max_records=self.KAFKA_MAX_RECORDS) + self._metrics.siphon_kafka_polls().inc() + + busy = bool(kafka_messages_by_topic) + for topic, kafka_messages in kafka_messages_by_topic.items(): + if topic == ALS_KAFKA_TOPIC: + self._read_als_kafka_messages(kafka_messages) + else: + self._process_log_kafka_messages(topic, kafka_messages) + if self._adb: + busy |= self._write_als_messages() + busy |= self._timeout_als_messages() + busy |= self._commit_kafka_offsets() + self._kafka_client.subscription_check() + + def _read_als_kafka_messages( + self, kafka_messages: List[KafkaClient.MessageInfo]) -> None: + """ Put fetched ALS Kafka messages to store of incomplete bundles + + Arguments: + topic -- ALS Topic name + kafka_messages -- List of raw Kafka messages + """ + for kafka_message in kafka_messages: + self._kafka_positions.add(kafka_message.position) + self._metrics.siphon_als_received().inc() + try: + assert kafka_message.key is not None + if self._als_bundles.add_message( + message_key=kafka_message.key, + message=AlsMessage(raw_msg=kafka_message.value), + kafka_position=kafka_message.position): + self._metrics.siphon_afc_msg_received().inc() + self._metrics.siphon_afc_msg_in_progress().set( + self._als_bundles.get_incomplete_count()) + except (AlsProtocolError, JsonFormatError) as ex: + self._metrics.siphon_als_malformed().inc() + self._decode_error_writer.write_decode_error( + msg=ex.msg, line=ex.code_line, data=ex.data) + self._kafka_positions.mark_processed( + kafka_position=kafka_message.position) + + def _process_log_kafka_messages( + self, topic: str, kafka_messages: List[KafkaClient.MessageInfo]) \ + -> None: + """ Process non-ALS (i.e. JSON Log) messages for one topic + + Arguments: + topic -- ALS Topic name + kafka_messages -- List of Kafka messages + """ + records: List[LogsDatabase.Record] = [] + for kafka_message in kafka_messages: + self._metrics.siphon_log_received(topic).inc() + self._kafka_positions.add(kafka_message.position) + try: + log_message = json.loads(kafka_message.value) + records.append( + LogsDatabase.Record( + source=log_message["source"], + time=datetime.datetime.fromisoformat( + log_message["time"]), + log=json.loads(log_message["jsonData"]))) + except (json.JSONDecodeError, LookupError, TypeError, ValueError) \ + as ex: + self._metrics.siphon_log_malformed(topic).inc() + logging.error( + f"Can't decode log message '{kafka_message.value!r}': " + f"{repr(ex)}") + if records and (self._ldb is not None): + transaction: Optional[Any] = None + try: + transaction = self._ldb.conn.begin() + self._ldb.write_log(topic=topic, records=records) + transaction.commit() + transaction = None + finally: + if transaction is not None: + transaction.rollback() + self._kafka_positions.mark_processed(topic=topic) + + def _write_als_messages(self) -> bool: + """ Write complete ALS Bundles to ALS database. + Returns True if any work was done """ + assert self._adb is not None + month_idx = get_month_idx() + transaction: Optional[Any] = None + try: + data_dict = \ + dict( + enumerate(self._als_bundles.fetch_assembled( + max_requests=self.ALS_MAX_REQ_UPDATE))) + if not data_dict: + return False + req_count = \ + sum(bundle.request_count() for bundle in data_dict.values()) + transaction = self._adb.conn.begin() + self._afc_message_updater.update_db(data_dict, month_idx=month_idx) + transaction.commit() + self._metrics.siphon_afc_msg_completed().inc(len(data_dict)) + self._metrics.siphon_afc_req_completed().inc(req_count) + self._metrics.siphon_afc_msg_in_progress().set( + self._als_bundles.get_incomplete_count()) + transaction = None + except JsonFormatError as ex: + if transaction is not None: + transaction.rollback() + transaction = None + self._lookups.reread() + self._decode_error_writer.write_decode_error( + ex.msg, line=ex.code_line, data=ex.data) + finally: + if transaction is not None: + transaction.rollback() + return True + + def _timeout_als_messages(self) -> bool: + """ Throw away old incomplete ALS messages. + Returns True if any work was done """ + boundary = datetime.datetime.now() - \ + datetime.timedelta(seconds=self.ALS_MAX_AGE_SEC) + ret = False + while True: + oldest_bundle = self._als_bundles.get_oldest_bundle() + if (oldest_bundle is None) or \ + (oldest_bundle.last_update() > boundary): + break + ret = True + self._als_bundles.remove_oldest_bundle() + self._decode_error_writer.write_decode_error( + "Incomplete message bundle removed", + line=LineNumber.current(), + data=oldest_bundle.dump()) + self._metrics.siphon_afc_msg_dropped().inc() + return ret + + def _commit_kafka_offsets(self) -> bool: + """ Commit completed Kafka offsets. + Returns True if any work was done """ + completed_offsets = self._kafka_positions.get_processed_offsets() + if not completed_offsets: + return False + self._kafka_client.commit(completed_offsets) + return True + + +def read_sql_file(sql_file: str) -> str: + """ Returns content of SQL file properly cleaned """ + with open(sql_file, encoding="ascii", newline=None) as f: + content = f.read() + + # Removing -- and /* */ comments. Courtesy of stackoverflow :) + def replacer(match: re.Match) -> str: + """ Replacement callback """ + s = match.group(0) + return " " if s.startswith('/') else s # /* */ comment is separator + + return re.sub( + r'--.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', + replacer, content, flags=re.DOTALL | re.MULTILINE) + + +ALS_PATCH = ["ALTER TABLE afc_server DROP CONSTRAINT IF EXISTS " + "afc_server_afc_server_name_key"] + + +def do_init_db(args: Any) -> None: + """Execute "init" command. + + Arguments: + args -- Parsed command line arguments + """ + databases: Set[DatabaseBase] = set() + try: + try: + init_db = InitialDatabase(arg_conn_str=args.init_postgres, + arg_password=args.init_postgres_password) + databases.add(init_db) + except sa.exc.SQLAlchemyError as ex: + error(f"Connection to {InitialDatabase.name_for_logs()} database " + f"failed: {ex}") + nothing_done = True + patch: List[str] + for conn_str, password, sql_file, template, db_class, sql_required, \ + patch in \ + [(args.als_postgres, args.als_postgres_password, args.als_sql, + args.als_template, AlsDatabase, True, ALS_PATCH), + (args.log_postgres, args.log_postgres_password, args.log_sql, + args.log_template, LogsDatabase, False, [])]: + if not (conn_str or sql_file or template): + continue + nothing_done = False + error_if(sql_file and (not os.path.isfile(sql_file)), + f"SQL file '{sql_file}' not found") + error_if( + sql_required and not sql_file, + f"SQL file is required for {db_class.name_for_logs()} " + f"database") + created = False + try: + database = db_class.parse_conn_str(conn_str).database + created = \ + init_db.create_db( + db_name=database, + if_exists=InitialDatabase.IfExists(args.if_exists), + template=template, conn_str=conn_str, + password=password) + db = db_class(arg_conn_str=conn_str, arg_password=password) + databases.add(db) + with db.engine.connect() as conn: + if created and sql_file: + conn.execute(sa.text(read_sql_file(sql_file))) + if not created: + for cmd in patch: + conn.execute(sa.text(cmd)) + except sa.exc.SQLAlchemyError as ex: + error(f"{db_class.name_for_logs()} database initialization " + f"failed: {ex}") + if created: + try: + init_db.drop_db(database) + except sa.exc.SQLAlchemyError: + pass + error_if(nothing_done, "Nothing to do") + finally: + for db in databases: + db.dispose() + + +def do_siphon(args: Any) -> None: + """Execute "siphon" command. + + Arguments: + args -- Parsed command line arguments + """ + if args.prometheus_port is not None: + prometheus_client.start_http_server(args.prometheus_port) + adb = AlsDatabase(arg_conn_str=args.als_postgres, + arg_password=args.als_postgres_password) \ + if args.als_postgres else None + ldb = LogsDatabase(arg_conn_str=args.log_postgres, + arg_password=args.log_postgres_password) \ + if args.log_postgres else None + try: + kafka_client = \ + KafkaClient(args=args, subscribe_als=adb is not None, + subscribe_log=ldb is not None, + resubscribe_interval_s=5) + siphon = Siphon(adb=adb, ldb=ldb, kafka_client=kafka_client) + siphon.main_loop() + finally: + if adb is not None: + adb.dispose() + if ldb is not None: + ldb.dispose() + + +def do_init_siphon(args: Any) -> None: + """Execute "init_siphon" command. + + Arguments: + args -- Parsed command line arguments + """ + do_init_db(args) + do_siphon(args) + + +def do_help(args: Any) -> None: + """Execute "help" command. + + Arguments: + args -- Parsed command line arguments (also contains 'argument_parser' and + 'subparsers' fields) + """ + if args.subcommand is None: + args.argument_parser.print_help() + else: + args.subparsers.choices[args.subcommand].print_help() + + +def docker_arg_type(final_type: Callable[[Any], Any], default: Any = None, + required: bool = False) -> Callable[[str], Any]: + """ Generator of argument converter for Docker environment + + Empty argument value passed from environment-variable-initialized argument + (e.g. from Docker) should be treated as nonspecified. Boolean values + passed from environment-variable-initialized argument should also be + treated specially + + Arguments: + final_type -- Type converter for nonempty argument + default -- Default value for empty argument + required -- True if argument is required (can't be empty) + Returns argument converter function + """ + assert (not required) or (default is None) + + def arg_converter(arg: str) -> Any: + """ Type conversion function that will be used by argparse """ + try: + if arg in ("", None): + if required: + raise ValueError("Parameter is required") + return default + if final_type == bool: + if arg.lower() in ("yes", "true", "+", "1"): + return True + if arg.lower() in ("no", "false", "-", "0"): + return False + raise \ + argparse.ArgumentTypeError( + "Wrong representation for boolean argument") + return final_type(arg) + except Exception as ex: + raise \ + argparse.ArgumentTypeError( + f"Command line argument '{arg}' has invalid format: " + f"{repr(ex)}") + + return arg_converter + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + # Kafka server connection switches + switches_kafka = argparse.ArgumentParser(add_help=False) + switches_kafka.add_argument( + "--kafka_servers", "-k", metavar="SERVER[:PORT][,SERVER2[:PORT2]...]", + type=docker_arg_type(str, default=DEFAULT_KAFKA_SERVER), + help=f"Comma-separated Kafka bootstrap server(s). Default is " + f"'{DEFAULT_KAFKA_SERVER}'") + switches_kafka.add_argument( + "--kafka_client_id", metavar="CLIENT_ID[@]", + type=docker_arg_type(str, default=DEFAULT_KAFKA_CLIENT_ID), + help=f"ID of this instance to be used in Kafka logs. If ends with " + f"'@' - supplemented with random string (to achieve uniqueness). " + f"Default is '{DEFAULT_KAFKA_CLIENT_ID}'") + switches_kafka.add_argument( + "--kafka_security_protocol", choices=["", "PLAINTEXT", "SSL"], + type=docker_arg_type(str, default="PLAINTEXT"), + help="Security protocol to use. Default is 'PLAINTEXT'") + switches_kafka.add_argument( + "--kafka_ssl_keyfile", metavar="FILENAME", type=docker_arg_type(str), + help="Client private key file for SSL authentication") + switches_kafka.add_argument( + "--kafka_ssl_cafile", metavar="FILENAME", type=docker_arg_type(str), + help="CA file for certificate verification") + switches_kafka.add_argument( + "--kafka_ssl_ciphers", metavar="CIPHERS", type=docker_arg_type(str), + help="Available ciphers in OpenSSL cipher list format") + switches_kafka.add_argument( + "--kafka_max_partition_fetch_bytes", metavar="SIZE_IN_BYTES", + type=docker_arg_type(int), + help="Maximum size of Kafka message (default is 1MB)") + + switches_als_db = argparse.ArgumentParser(add_help=False) + switches_als_db.add_argument( + "--als_postgres", + metavar="[driver://][user][@host][:port][/database][?...]", + type=docker_arg_type(str), + help=f"ALS Database connection string. If some part (driver, user, " + f"host port database) is missing - it is taken from the default " + f"connection string (which is '{AlsDatabase.default_conn_str()}'. " + f"Connection parameters may be specified after '?' - see " + f"https://www.postgresql.org/docs/current/libpq-connect.html" + f"#LIBPQ-CONNSTRING for details") + switches_als_db.add_argument( + "--als_postgres_password", metavar="PASSWORD", + type=docker_arg_type(str), + help="Password to use for ALS Database connection") + + switches_log_db = argparse.ArgumentParser(add_help=False) + switches_log_db.add_argument( + "--log_postgres", + metavar="[driver://][user][@host][:port][/database][?...]", + type=docker_arg_type(str), + help=f"Log Database connection string. If some part (driver, user, " + f"host port database) is missing - it is taken from the default " + f"connection string (which is '{LogsDatabase.default_conn_str()}'. " + f"Connection parameters may be specified after '?' - see " + f"https://www.postgresql.org/docs/current/libpq-connect.html" + f"#LIBPQ-CONNSTRING for details. Default is not use log database") + switches_log_db.add_argument( + "--log_postgres_password", metavar="PASSWORD", + type=docker_arg_type(str), + help="Password to use for Log Database connection") + + switches_init = argparse.ArgumentParser(add_help=False) + switches_init.add_argument( + "--init_postgres", + metavar="[driver://][user][@host][:port][/database][?...]", + type=docker_arg_type(str), + help=f"Connection string to initial database used as a context for " + "other databases' creation. If some part (driver, user, host port " + f"database) is missing - it is taken from the default connection " + f"string (which is '{InitialDatabase.default_conn_str()}'. Connection " + f"parameters may be specified after '?' - see " + f"https://www.postgresql.org/docs/current/libpq-connect.html" + f"#LIBPQ-CONNSTRING for details") + switches_init.add_argument( + "--init_postgres_password", metavar="PASSWORD", + type=docker_arg_type(str), + help="Password to use for initial database connection") + switches_init.add_argument( + "--if_exists", choices=["skip", "drop"], + type=docker_arg_type(str, default="exc"), + help="What to do if database already exist: nothing (skip) or " + "recreate (drop). Default is to fail") + switches_init.add_argument( + "--als_template", metavar="DB_NAME", type=docker_arg_type(str), + help="Template database (e.g. bearer of required extensions) to use " + "for ALS database creation. E.g. postgis/postgis image strangely " + "assigns Postgis extension on 'template_postgis' database instead of " + "on default 'template0/1'") + switches_init.add_argument( + "--log_template", metavar="DB_NAME", type=docker_arg_type(str), + help="Template database to use for JSON Logs database creation") + switches_init.add_argument( + "--als_sql", metavar="SQL_FILE", type=docker_arg_type(str), + help="SQL command file that creates tables, relations, etc. in ALS " + "database. If neither this parameter nor --als_postgres is specified " + "ALS database is not being created") + switches_init.add_argument( + "--log_sql", metavar="SQL_FILE", type=docker_arg_type(str), + help="SQL command file that creates tables, relations, etc. in JSON " + "log database. By default database created (if --log_postgres is " + "specified) empty") + + switches_siphon = argparse.ArgumentParser(add_help=False) + switches_siphon.add_argument( + "--prometheus_port", metavar="PORT", type=docker_arg_type(int), + help="Port to serve Prometheus metrics on") + + # Top level parser + argument_parser = argparse.ArgumentParser( + description=f"Tool for moving data from Kafka to PostgreSQL/PostGIS " + f"database. V{VERSION}") + subparsers = argument_parser.add_subparsers(dest="subcommand", + metavar="SUBCOMMAND") + parser_init_db = subparsers.add_parser( + "init_db", parents=[switches_init, switches_als_db, switches_log_db], + help="Initialize ALS and/or JSON Log database") + parser_init_db.set_defaults(func=do_init_db) + + parser_siphon = subparsers.add_parser( + "siphon", + parents=[switches_kafka, switches_als_db, switches_log_db, + switches_siphon], + help="Siphon data from Kafka queue to ALS database") + parser_siphon.set_defaults(func=do_siphon) + + parser_init_siphon = subparsers.add_parser( + "init_siphon", + parents=[switches_init, switches_kafka, switches_als_db, + switches_log_db, switches_siphon], + help="Combination of 'db_init' and 'siphon' for Docker convenience") + parser_init_siphon.set_defaults(func=do_init_siphon) + + # Subparser for 'help' command + parser_help = subparsers.add_parser( + "help", add_help=False, usage="%(prog)s subcommand", + help="Prints help on given subcommand") + parser_help.add_argument( + "subcommand", metavar="SUBCOMMAND", nargs="?", + choices=subparsers.choices, + help="Name of subcommand to print help about (use " + + "\"%(prog)s --help\" to get list of all subcommands)") + parser_help.set_defaults(func=do_help, subparsers=subparsers, + argument_parser=argument_parser) + + if not argv: + argument_parser.print_help() + sys.exit(1) + args = argument_parser.parse_args(argv) + + # Set up logging + console_handler = logging.StreamHandler() + console_handler.setFormatter( + logging.Formatter( + f"{os.path.basename(__file__)}. %(levelname)s: %(message)s")) + logging.getLogger().addHandler(console_handler) + logging.getLogger().setLevel(logging.INFO) + + if args.func != do_help: + logging.info("Arguments:") + for arg, value in \ + sorted(args.__dict__.items(), key=lambda kvp: kvp[0]): + if (arg != "func") and (value is not None): + logging.info(f" {arg}: {value}") + + # Do the needful + args.func(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/als/requirements.txt b/als/requirements.txt new file mode 100644 index 0000000..7d7be32 --- /dev/null +++ b/als/requirements.txt @@ -0,0 +1,12 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +GeoAlchemy==0.7.2 +GeoAlchemy2==0.12.5 +postgis==1.0.4 +prometheus-client==0.17.1 +pyparsing==3.0.9 +python-dateutil==2.8.2 diff --git a/build-rpm.sh b/build-rpm.sh new file mode 100755 index 0000000..066acb4 --- /dev/null +++ b/build-rpm.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Build the RPM packages for CPO. +# Any arguments to this script are passed along to each of the "rpmbuild" commands. +set -e + +N_THREADS=$(nproc --all --ignore=2) +ROOTDIR=$(readlink -f $(dirname "${BASH_SOURCE[0]}")) +CMAKE="cmake3 -G Ninja -DSVN_LAST_REVISION=$BUILDREV" +NINJA="ninja-build -j$N_THREADS" +RPMBUILD="rpmbuild -ba --without apidoc $@" +RPMLINT="rpmlint" + +source /opt/rh/$(scl -l)/enable +export CC=$(which gcc) +export CXX=$(which g++) + +if [[ -d "/usr/include/boost169" ]]; then + CMAKE="${CMAKE} -DBOOST_INCLUDEDIR=/usr/include/boost169 -DBOOST_LIBRARYDIR=/usr/lib64/boost169" +fi + +BUILDDIR=${ROOTDIR}/build +mkdir -p $BUILDDIR +pushd $BUILDDIR + +${CMAKE} .. +rm -rf dist +${NINJA} rpm-prep +# Run rpmbuild directly to get unbuffered output +${RPMBUILD} --define "_topdir ${PWD}/dist" dist/SPECS/*.spec + +popd +cd / && ${RPMLINT} --file fbrat.rpmlintrc build/dist/SRPMS build/dist/RPMS diff --git a/build@tmp/durable-760e39dc/jenkins-log.txt b/build@tmp/durable-760e39dc/jenkins-log.txt new file mode 100644 index 0000000..3a5952b --- /dev/null +++ b/build@tmp/durable-760e39dc/jenkins-log.txt @@ -0,0 +1,5 @@ ++ runcmd.py build.log -- ninja-build -v rpm-prep +[0/2] cd /home/jenkins/slave/workspace/RAT-release-RPM/build && /usr/bin/cpack3 --config ./CPackSourceConfig.cmake /home/jenkins/slave/workspace/RAT-release-RPM/build/CPackSourceConfig.cmake +CPack3: Create package using TBZ2 +CPack3: Install projects +CPack3: - Install directory: /home/jenkins/slave/workspace/RAT-release-RPM diff --git a/bulk_postgres/Dockerfile b/bulk_postgres/Dockerfile new file mode 100644 index 0000000..0f61704 --- /dev/null +++ b/bulk_postgres/Dockerfile @@ -0,0 +1,17 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# Dockerfile for PostgreSQL+PostGIS server used for ALS log storage + +FROM postgis/postgis:14-3.3 + +ENV POSTGRES_PASSWORD=postgres +ENV POSTGRES_HOST_AUTH_METHOD=trust +ENV AFC_BULKDB_CONNS=${AFC_BULKDB_CONNS:-1000} +ENTRYPOINT docker-entrypoint.sh postgres -c max_connections=$AFC_BULKDB_CONNS +HEALTHCHECK --start-period=20s --interval=10s --timeout=5s \ + CMD pg_isready -U postgres || exit 1 diff --git a/ca-bundle.crt b/ca-bundle.crt new file mode 100644 index 0000000..ac9c691 --- /dev/null +++ b/ca-bundle.crt @@ -0,0 +1,3828 @@ +# E=BSipos@rkf-eng.com,CN=watt-ca,OU=development,O=RKF Engineering Solutions\, LLC,L=Washington,ST=DC,C=US +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQsFADCBozELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkRDMRMwEQYDVQQHEwpXYXNoaW5ndG9uMScwJQYDVQQKEx5SS0Yg +RW5naW5lZXJpbmcgU29sdXRpb25zLCBMTEMxFDASBgNVBAsTC2RldmVsb3BtZW50 +MRAwDgYDVQQDEwd3YXR0LWNhMSEwHwYJKoZIhvcNAQkBFhJCU2lwb3NAcmtmLWVu +Zy5jb20wHhcNMTQwMTAxMDAwMDAwWhcNMjMxMjMxMjM1OTU5WjCBozELMAkGA1UE +BhMCVVMxCzAJBgNVBAgTAkRDMRMwEQYDVQQHEwpXYXNoaW5ndG9uMScwJQYDVQQK +Ex5SS0YgRW5naW5lZXJpbmcgU29sdXRpb25zLCBMTEMxFDASBgNVBAsTC2RldmVs +b3BtZW50MRAwDgYDVQQDEwd3YXR0LWNhMSEwHwYJKoZIhvcNAQkBFhJCU2lwb3NA +cmtmLWVuZy5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5s9Av +yNSKMo9QRQti2eNpn23tm5tscPyWW3VQ9pw2ngod6csB5NNfnKrXfFyCre/K6gyp +/UN5INSNNgQF/PI6EW/GmAqHEUUBDlb5J7jL/6kraWodlfGbYsozbuznoCO1YIhN +RLNId0ruoJ/MEMvQqKS5nXiQAu6Z7sXlN2Z8AlaY8A4I9LgrqHc5HTdm8TcUqurn +CfmifZOB0amkGaOOPqry4ks56pu0177XV6I9EkDxkYrymFKC7jDoTDTeWNb3M4bi +dCa6zrs0O/yPg6WtEvAIhW8rSWU/JQUvaVLaXLgyQTvzJjUw8afKbQH1qFZrlCkN +15ibqxSMXceFUPrhAgMBAAGjUjBQMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FCA/WxHDrjAGMVYDlbmCPciXoP2ZMAsGA1UdDwQEAwIBBjARBglghkgBhvhCAQEE +BAMCAAcwDQYJKoZIhvcNAQELBQADggEBAISRpIPCEe5JueRL1aD5nwrH9lZQTn+D +g288sV0iN73Bzc4XXHFsTMkIy0xxhhMVfn1UTCRbR4HMQ4gPOTfJcblUpAw69QHY +agQ4Iv43GpBMLXMe+rmmMMpJkru75mcEiOWpbQokkypJaagL2abQnfXExs0Lkp1T +9UMogE+PnT4wfGFZ7ObGIHq79mqvc64vMlzzJHA4oA/4Mk8ZPJoveBhHmHOmbxSE +K3tMF1ZjaRRzUdUWqsOwlHtGMLz9xeantO0uSRQFHOo08uQGyfd+j02G4bY6fkIt +ZNFA+fbUiZuPoeH9SGnWfairOgrJIzHA+hujzmkXcLUS/340f7GdMFc= +-----END CERTIFICATE----- + +# IPA.RKF-ENGINEERING.COM IPA CA +-----BEGIN CERTIFICATE----- +MIIEajCCA1KgAwIBAgIBKTANBgkqhkiG9w0BAQsFADCBozELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkRDMRMwEQYDVQQHEwpXYXNoaW5ndG9uMScwJQYDVQQKEx5SS0Yg +RW5naW5lZXJpbmcgU29sdXRpb25zLCBMTEMxFDASBgNVBAsTC2RldmVsb3BtZW50 +MRAwDgYDVQQDEwd3YXR0LWNhMSEwHwYJKoZIhvcNAQkBFhJCU2lwb3NAcmtmLWVu +Zy5jb20wHhcNMTYwNTI3MDAwMDAwWhcNMjMxMjMxMjM1OTU5WjBCMSAwHgYDVQQK +ExdJUEEuUktGLUVOR0lORUVSSU5HLkNPTTEeMBwGA1UEAxMVQ2VydGlmaWNhdGUg +QXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1EjvpoWm +jmlIPMBNccYEFLSEZL8Z442GWNx8feKwHgD9VDX0e/Bnr5dWrrrLLs12f29weomF +e14r/70TcHkOqOhOx41FvT5iM8toNtaREtSC/De6rd1mCk3agiFcoWlAPFnEFw6a +9xgER3CxnLYZ3y76O3OPkefYKt8o3SJPu9YSt77+X+V2Ip8F/MFO7F/Yizv/fMQ/ +Js6yif3UdnRQ3faVhdAxrwA51teBGcKzLEUBxrOxHSoMuCFcxey27pgrWPCXt+xJ +o57tssnEpVsuxsc7l23UvGViWx4DelnaTxCsmoGy847OMQ6zaIpGZMk8O5CcPsK1 +1lGqrvCvzNiDmQIDAQABo4IBBzCCAQMwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUjjsLgayqRMI+aQ6L+tC6VqywK7wwgdAGA1UdIwSByDCBxYAUID9bEcOuMAYx +VgOVuYI9yJeg/ZmhgamkgaYwgaMxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJEQzET +MBEGA1UEBxMKV2FzaGluZ3RvbjEnMCUGA1UEChMeUktGIEVuZ2luZWVyaW5nIFNv +bHV0aW9ucywgTExDMRQwEgYDVQQLEwtkZXZlbG9wbWVudDEQMA4GA1UEAxMHd2F0 +dC1jYTEhMB8GCSqGSIb3DQEJARYSQlNpcG9zQHJrZi1lbmcuY29tggEBMA0GCSqG +SIb3DQEBCwUAA4IBAQA+4DTD5pV4PTlZWak3KMQ4RoHj1DW60ir16YvnEJiPApE1 +omPeY4tpdPKwL8dNNcTaVx5+EHmht38qoGrODfaKtP7RgYh+rdsOxcicatX1sOy8 +5qzgCKO5xCRezZmkOT4hS0xYOGpv2DI3IIqJUsGqF+ch/0nX9E+VcYjBgxYtu1a2 +JiyTAxDcnX0eslpNrG3Rujr8NhYFlx8PROnrqD4BOF6NX4PPK8Lnfwapckzcsb1e +tUUNFKz48DTOo2bIm1U/byTj8a5/cPDed4rlH772SVZQkmhNpy2T3u4AxTW2TwOy +j9boCkLjxaymJVTSBQmOVGuyQdqNIv6chCXWxp4C +-----END CERTIFICATE----- + +# ad-DC01-CA-1 +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQanX+13g9Za9NJ8k5uxUZ1zANBgkqhkiG9w0BAQUFADBh +MRMwEQYKCZImiZPyLGQBGRYDY29tMR8wHQYKCZImiZPyLGQBGRYPcmtmLWVuZ2lu +ZWVyaW5nMRIwEAYKCZImiZPyLGQBGRYCYWQxFTATBgNVBAMTDGFkLURDMDEtQ0Et +MTAeFw0xNjA2MDIxNTQ5NTVaFw0yNjA2MDIxNTU5NTRaMGExEzARBgoJkiaJk/Is +ZAEZFgNjb20xHzAdBgoJkiaJk/IsZAEZFg9ya2YtZW5naW5lZXJpbmcxEjAQBgoJ +kiaJk/IsZAEZFgJhZDEVMBMGA1UEAxMMYWQtREMwMS1DQS0xMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2sk8Ll8emyHkPnhP75ptJdNNKOCcftemH80n +iSImHwtzl00ZrDcz2VBRgFpqhU7In8yXTh6GZZ3zJcHkLMZZLY5J9adOPcvu4xcc +A2wgKFL+OD5kQqoxaT+rDztsgCD98OX+P8ba+TVN0Q2/t1H8pCRoiICIr+2mH1vJ ++3fXi6qKT/kS6ecRGSSy127qAoYxIHS1FwxM0RaSytJDz4d0RaQUwNxEhg2K5xD5 +XuNS5ABRa0t2FzKK2ebOLB6wNHnKauMOCPY+QW7iillJsJmoif7oyGg1LUUPKuhE +NlS59gf/TcHlXBPIdh0jT0TB0URkYbrzDka9aYAxhOQ9MRpnlwIDAQABo2kwZzAT +BgkrBgEEAYI3FAIEBh4EAEMAQTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQU6ieirMxD1JJQ+UfCyacZa7zEF+AwEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEp6apkSUjyTKowDRjDh5W14F0wPnbPC +g/HYrwcBr9liPkWnFfnFzueu7zab5ugtD1OpJpZevPcGmS0ZM8Vq+3G55n+xj7WT +4DvyN1rhcMarev5qgfiNcdVGpHtBXP+FcdaBCdUpA3a5qYmKM3SPJnjdD87P61PY +4+fT1yNrw9o5WXHIPmAlNPZoTkwv5roL3bhik14kECvg7ZL+BjiC3H9FTtDI7Ki5 +yxwa5dLb/AOX9iC3vv+RagEDZ3tWSWQu7IASfZec/jK/iCL4tQGJ7bivvnE44HOT +g4WsQyFsNTB8JujvVDwnIKc3jHPpGKo34MqnZHibqsR2Vlsvm8VNC1s= +-----END CERTIFICATE----- + +# ACCVRAIZ1 +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +# AC RAIZ FNMT-RCM +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +# Actalis Authentication Root CA +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +# AffirmTrust Commercial +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +# AffirmTrust Networking +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +# AffirmTrust Premium +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- + +# AffirmTrust Premium ECC +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- + +# Amazon Root CA 1 +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +# Amazon Root CA 2 +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- + +# Amazon Root CA 3 +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +# Amazon Root CA 4 +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +# Atos TrustedRoot 2011 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +# Autoridad de Certificacion Firmaprofesional CIF A62634068 +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy +MzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD +VR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv +ACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl +AGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF +661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9 +am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1 +ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481 +PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS +3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k +SeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF +3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM +ZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g +StRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz +Q0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB +jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +# Baltimore CyberTrust Root +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +# Buypass Class 2 Root CA +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +# Buypass Class 3 Root CA +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +# CA Disig Root R2 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +# CFCA EV ROOT +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +# COMODO Certification Authority +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- + +# COMODO ECC Certification Authority +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# COMODO RSA Certification Authority +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +# Certigna +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +# Certigna Root CA +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +# Certum Trusted Network CA +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +# Certum Trusted Network CA 2 +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- + +# Chambers of Commerce Root - 2008 +-----BEGIN CERTIFICATE----- +MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz +IG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz +MTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj +dXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw +EAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp +MCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9 +28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq +VKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q +DuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR +5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL +ZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a +Sd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl +UlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s ++12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5 +Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj +ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx +hduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV +HQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1 ++HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN +YWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t +L2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy +ZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt +IDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV +HSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w +DQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW +PJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF +5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1 +glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH +FoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2 +pSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD +xvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG +tjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq +jktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De +fhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg +OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ +d0jQ +-----END CERTIFICATE----- + +# Comodo AAA Services root +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +# Cybertrust Global Root +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG +A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh +bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE +ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5 +7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS +J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y +HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP +t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz +FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY +XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw +hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js +MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA +A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj +Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx +XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o +omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc +A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +# D-TRUST Root Class 3 CA 2 2009 +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +# D-TRUST Root Class 3 CA 2 EV 2009 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +# DST Root CA X3 +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- + +# DigiCert Assured ID Root CA +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +# DigiCert Assured ID Root G2 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# DigiCert Assured ID Root G3 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# DigiCert Global Root CA +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +# DigiCert Global Root G2 +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# DigiCert Global Root G3 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# DigiCert High Assurance EV Root CA +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- + +# DigiCert Trusted Root G4 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# E-Tugra Certification Authority +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV +BAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC +aWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV +BAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1 +Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz +MDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+ +BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp +em1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN +ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY +B4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH +D5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF +Q9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo +q1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D +k14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH +fC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut +dEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM +ti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8 +zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn +rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX +U8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6 +Jyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5 +XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF +Nzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR +HTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY +GwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c +77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3 ++GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK +vJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6 +FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl +yb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P +AJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD +y4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d +NL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA== +-----END CERTIFICATE----- + +# EC-ACC +-----BEGIN CERTIFICATE----- +MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB +8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy +dGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1 +YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3 +dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh +IEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD +LUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG +EwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g +KE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD +ZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu +bmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg +ZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R +85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm +4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV +HMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd +QlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t +lGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB +o4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4 +opvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo +dHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW +ZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN +AQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y +/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k +SBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy +Rp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS +Agu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl +nJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI= +-----END CERTIFICATE----- + +# EE Certification Centre Root CA +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG +CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy +MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl +ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS +b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy +euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO +bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw +WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d +MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE +1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/ +zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB +BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF +BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV +v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG +E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u +uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW +iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v +GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0= +-----END CERTIFICATE----- + +# Entrust.net Premium 2048 Secure Server CA +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 +MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub +j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo +U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b +u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ +bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er +fF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +# Entrust Root Certification Authority +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +# Entrust Root Certification Authority - EC1 +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +# Entrust Root Certification Authority - G2 +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- + +# Entrust Root Certification Authority - G4 +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw +gb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL +Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg +MjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw +BgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0 +MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1 +c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ +bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ +2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E +T+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j +5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM +C1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T +DtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX +wbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A +2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm +nqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 +dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl +N4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj +c0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS +5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS +Gwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr +hFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/ +B7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI +AeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw +H5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+ +b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk +2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol +IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk +5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY +n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw== +-----END CERTIFICATE----- + +# GDCA TrustAUTH R5 ROOT +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +# GTS Root R1 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBH +MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM +QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy +MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl +cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM +f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vX +mX7wCl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7 +zUjwTcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0P +fyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtc +vfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4 +Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUsp +zBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOO +Rc92wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYW +k70paDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+ +DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgF +lQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBADiW +Cu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1 +d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6Z +XPYfcX3v73svfuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZR +gyFmxhE+885H7pwoHyXa/6xmld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3 +d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9bgsiG1eGZbYwE8na6SfZu6W0eX6Dv +J4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq4BjFbkerQUIpm/Zg +DdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWErtXvM ++SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyy +F62ARPBopY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9 +SQ98POyDGCBDTtWTurQ0sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdws +E3PYJ/HQcu51OyLemGhmW/HGY0dVHLqlCFF1pkgl +-----END CERTIFICATE----- + +# GTS Root R2 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBH +MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM +QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy +MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl +cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3Lv +CvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3Kg +GjSY6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9Bu +XvAuMC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOd +re7kRXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXu +PuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1 +mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K +8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqj +x5RWIr9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsR +nTKaG73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0 +kzCqgc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9Ok +twIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBALZp +8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT +vhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiT +z9D2PGcDFWEJ+YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiA +pJiS4wGWAqoC7o87xdFtCjMwc3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvb +pxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3DaWsYDQvTtN6LwG1BUSw7YhN4ZKJmB +R64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5rn/WkhLx3+WuXrD5R +RaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56GtmwfuNmsk +0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC +5AwiWVIQ7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiF +izoHCBy69Y9Vmhh1fuXsgWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLn +yOd/xCxgXS/Dr55FBcOEArf9LAhST4Ldo/DUhgkC +-----END CERTIFICATE----- + +# GTS Root R3 +-----BEGIN CERTIFICATE----- +MIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout +736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2A +DDL24CejQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFuk +fCPAlaUs3L6JbyO5o91lAFJekazInXJ0glMLfalAvWhgxeG4VDvBNhcl2MG9AjEA +njWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOaKaqW04MjyaR7YbPMAuhd +-----END CERTIFICATE----- + +# GTS Root R4 +-----BEGIN CERTIFICATE----- +MIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu +hXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/l +xKvRHYqjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0 +CMRw3J5QdCHojXohw0+WbhXRIjVhLfoIN+4Zba3bssx9BzT1YBkstTTZbyACMANx +sbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11xzPKwTdb+mciUqXWi4w== +-----END CERTIFICATE----- + +# GeoTrust Global CA +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- + +# GeoTrust Primary Certification Authority +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo +R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx +MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 +AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA +ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 +7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W +kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI +mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ +KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 +6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl +4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K +oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj +UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU +AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +# GeoTrust Primary Certification Authority - G2 +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL +MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj +KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2 +MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw +NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV +BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL +So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal +tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG +CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT +qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz +rD6ogRLQy7rQkgu2npaqBA+K +-----END CERTIFICATE----- + +# GeoTrust Primary Certification Authority - G3 +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT +MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ +BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0 +BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz ++uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm +hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn +5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W +JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL +DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC +huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB +AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB +zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN +kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH +SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G +spki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +# GeoTrust Universal CA +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy +c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 +IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV +VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 +cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT +QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh +F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v +c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w +mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd +VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX +teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ +f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe +Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ +nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY +MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX +IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn +ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z +uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN +Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja +QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW +koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 +ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt +DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm +bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +# GeoTrust Universal CA 2 +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy +c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD +VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 +c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 +WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG +FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq +XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL +se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb +KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd +IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 +y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt +hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc +QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 +Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV +HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ +KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ +L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr +Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo +ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY +T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz +GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m +1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV +OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH +6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX +QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +# GlobalSign ECC Root CA - R4 +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ +FspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F +uOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX +kPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs +ewv4n4Q= +-----END CERTIFICATE----- + +# GlobalSign ECC Root CA - R5 +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +# GlobalSign Root CA +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +# GlobalSign Root CA - R2 +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +# GlobalSign Root CA - R3 +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# GlobalSign Root CA - R6 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +# Global Chambersign Root - 2008 +-----BEGIN CERTIFICATE----- +MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD +aGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx +MjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy +cmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG +A1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl +BgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed +KYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7 +G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2 +zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4 +ddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG +HoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2 +Id3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V +yJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e +beksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r +6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh +wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog +zCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW +BBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr +ru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp +ZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk +cmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt +YSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC +CQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow +KAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI +hvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ +UohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz +X1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x +fxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz +a2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd +Yhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd +SqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O +AP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso +M0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge +v8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z +09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B +-----END CERTIFICATE----- + +# Go Daddy Class 2 CA +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- + +# Go Daddy Root Certificate Authority - G2 +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Hellenic Academic and Research Institutions ECC RootCA 2015 +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +# Hellenic Academic and Research Institutions RootCA 2011 +-----BEGIN CERTIFICATE----- +MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix +RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p +YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw +NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK +EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl +cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz +dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ +fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns +bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD +75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP +FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV +HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp +5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu +b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA +A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p +6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 +TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7 +dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys +Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI +l7WdmplNsDz4SgCbZN2fOUvRJ9e4 +-----END CERTIFICATE----- + +# Hellenic Academic and Research Institutions RootCA 2015 +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +# Hongkong Post Root CA 1 +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx +FjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg +Um9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG +A1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr +b25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ +jVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn +PzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh +ZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9 +nnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h +q5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED +MA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC +mEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3 +7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB +oiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs +EhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO +fMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi +AmvZWg== +-----END CERTIFICATE----- + +# Hongkong Post Root CA 3 +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE----- + +# ISRG Root X1 +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +# IdenTrust Commercial Root CA 1 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +# IdenTrust Public Sector Root CA 1 +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +# Izenpe.com +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +# LuxTrust Global Root 2 +-----BEGIN CERTIFICATE----- +MIIFwzCCA6ugAwIBAgIUCn6m30tEntpqJIWe5rgV0xZ/u7EwDQYJKoZIhvcNAQEL +BQAwRjELMAkGA1UEBhMCTFUxFjAUBgNVBAoMDUx1eFRydXN0IFMuQS4xHzAdBgNV +BAMMFkx1eFRydXN0IEdsb2JhbCBSb290IDIwHhcNMTUwMzA1MTMyMTU3WhcNMzUw +MzA1MTMyMTU3WjBGMQswCQYDVQQGEwJMVTEWMBQGA1UECgwNTHV4VHJ1c3QgUy5B +LjEfMB0GA1UEAwwWTHV4VHJ1c3QgR2xvYmFsIFJvb3QgMjCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANeFl78RmOnwYoNMPIf5U2o3C/IPPIfOb9wmKb3F +ibrJgz337spbxm1Jc7TJRqMbNBM/wYlFV/TZsfs2ZUv7COJIcRHIbjuend+JZTem +hfY7RBi2xjcwYkSSl2l9QjAk5A0MiWtj3sXh306pFGxT4GHO9hcvHTy95iJMHZP1 +EMShduxq3sVs35a0VkBCwGKSMKEtFZSg0iAGCW5qbeXrt77U8PEVfIvmTroTzEsn +Xpk8F12PgX8zPU/TPxvsXD/wPEx1bvKm1Z3aLQdjAsZy6ZS8TEmVT4hSyNvoaYL4 +zDRbIvCGp4m9SAptZoFtyMhk+wHh9OHe2Z7d21vUKpkmFRseTJIpgp7VkoGSQXAZ +96Tlk0u8d2cx3Rz9MXANF5kM+Qw5GSoXtTBxVdUPrljhPS80m8+f9niFwpN6cj5m +j5wWEWCPnolvZ77gR1o7DJpni89Gxq44o/KnvObWhWszJHAiS8sIm7vI+AIpHb4g +DEa/a4ebsypmQjVGbKq6rfmYe+lQVRQxv7HaLe2ArWgk+2mr2HETMOZns4dA/Yl+ +8kPREd8vZS9kzl8UubG/Mb2HeFpZZYiq/FkySIbWTLkpS5XTdvN3JW1CHDiDTf2j +X5t/Lax5Gw5CMZdjpPuKadUiDTSQMC6otOBttpSsvItO13D8xTiOZCXhTTmQzsmH +hFhxAgMBAAGjgagwgaUwDwYDVR0TAQH/BAUwAwEB/zBCBgNVHSAEOzA5MDcGByuB +KwEBAQowLDAqBggrBgEFBQcCARYeaHR0cHM6Ly9yZXBvc2l0b3J5Lmx1eHRydXN0 +Lmx1MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBT/GCh2+UgFLKGu8SsbK7JT ++Et8szAdBgNVHQ4EFgQU/xgodvlIBSyhrvErGyuyU/hLfLMwDQYJKoZIhvcNAQEL +BQADggIBAGoZFO1uecEsh9QNcH7X9njJCwROxLHOk3D+sFTAMs2ZMGQXvw/l4jP9 +BzZAcg4atmpZ1gDlaCDdLnINH2pkMSCEfUmmWjfrRcmF9dTHF5kH5ptV5AzoqbTO +jFu1EVzPig4N1qx3gf4ynCSecs5U89BvolbW7MM3LGVYvlcAGvI1+ut7MV3CwRI9 +loGIlonBWVx65n9wNOeD4rHh4bhY79SV5GCc8JaXcozrhAIuZY+kt9J/Z93I055c +qqmkoCUUBpvsT34tC38ddfEz2O3OuHVtPlu5mB0xDVbYQw8wkbIEa91WvpWAVWe+ +2M2D2RjuLg+GLZKecBPs3lHJQ3gCpU3I+V/EkVhGFndadKpAvAefMLmx9xIX3eP/ +JEAdemrRTxgKqpAd60Ae36EeRJIQmvKN4dFLRp7oRUKX6kWZ8+xm1QL68qZKJKre +zrnK+T+Tb/mjuuqlPpmt/f97mfVl7vBZKGfXkJWkE4SphMHozs51k2MavDzq1WQf +LSoSOcbDWjLtR5EWDrw4wVDej8oqkDQc7kGUnF4ZLvhFSZl0kbAEb+MEWrGrKqv+ +x9CWttrhSmQGbmBNvUJO/3jaJMobtNeWOWyu8Q6qp31IiyBMz2TWuJdGsE7RKlY6 +oJO9r4Ak4Ap+58rVyuiFVdw2KuGUaJPHZnJED4AhMmwlxyOAgwrr +-----END CERTIFICATE----- + +# Microsec e-Szigno Root CA 2009 +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +# NetLock Arany (Class Gold) Főtanúsítvány +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +# Network Solutions Certificate Authority +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi +MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV +UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO +ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz +c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP +OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl +mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF +BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4 +qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw +gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu +bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp +dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8 +6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/ +h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH +/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN +pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +# OISTE WISeKey Global Root GA CA +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB +ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly +aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w +NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G +A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX +SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR +VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2 +w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF +mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg +4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9 +4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw +EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx +SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2 +ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8 +vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa +hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi +Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ +/L7fCg0= +-----END CERTIFICATE----- + +# OISTE WISeKey Global Root GB CA +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +# OISTE WISeKey Global Root GC CA +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +# QuoVadis Root CA +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz +MzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw +IwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR +dW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp +li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D +rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ +WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug +F+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU +xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC +Ak4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv +dmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw +ggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl +IG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh +c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy +ZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI +KwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T +KbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq +y+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p +dGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD +VQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk +fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8 +7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R +cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y +mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW +xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK +SnQ2+Q== +-----END CERTIFICATE----- + +# QuoVadis Root CA 1 G3 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +# QuoVadis Root CA 2 +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +# QuoVadis Root CA 2 G3 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +# QuoVadis Root CA 3 +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +# QuoVadis Root CA 3 G3 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +# SSL.com EV Root Certification Authority ECC +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +# SSL.com EV Root Certification Authority RSA R2 +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +# SSL.com Root Certification Authority ECC +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +# SSL.com Root Certification Authority RSA +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +# SZAFIR ROOT CA2 +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +# SecureSign RootCA11 +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr +MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG +A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0 +MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp +Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD +QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz +i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8 +h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV +MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9 +UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni +8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC +h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD +VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB +AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm +KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ +X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr +QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5 +pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN +QSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +# SecureTrust CA +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +# Secure Global CA +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +# Security Communication RootCA2 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +# Security Communication Root CA +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY +MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t +dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 +WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD +VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 +9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ +DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 +Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N +QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ +xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G +A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG +kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr +Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 +Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU +JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot +RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== +-----END CERTIFICATE----- + +# Sonera Class 2 Root CA +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP +MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx +MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV +BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o +Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt +5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s +3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej +vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu +8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw +DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG +MA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil +zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/ +3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD +FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6 +Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2 +ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M +-----END CERTIFICATE----- + +# Staat der Nederlanden EV Root CA +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gRVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0y +MjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5MMR4wHAYDVQQKDBVTdGFhdCBkZXIg +TmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRlcmxhbmRlbiBFViBS +b290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkkSzrS +M4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nC +UiY4iKTWO0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3d +Z//BYY1jTw+bbRcwJu+r0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46p +rfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13l +pJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gVXJrm0w912fxBmJc+qiXb +j5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr08C+eKxC +KFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS +/ZbV0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0X +cgOPvZuM5l5Tnrmd74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH +1vI4gnPah1vlPNOePqc7nvQDs/nxfRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrP +px9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwaivsnuL8wbqg7 +MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u +2dfOWBfoqSmuc0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHS +v4ilf0X8rLiltTMMgsT7B/Zq5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTC +wPTxGfARKbalGAKb12NMcIxHowNDXLldRqANb/9Zjr7dn3LDWyvfjFvO5QxGbJKy +CqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tNf1zuacpzEPuKqf2e +vTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi5Dp6 +Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIa +Gl6I6lD4WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeL +eG9QgkRQP2YGiqtDhFZKDyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8 +FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGyeUN51q1veieQA6TqJIc/2b3Z6fJfUEkc +7uzXLg== +-----END CERTIFICATE----- + +# Staat der Nederlanden Root CA - G3 +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX +DTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4yolQP +cPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WW +IkYFsO2tx1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqX +xz8ecAgwoNzFs21v0IJyEavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFy +KJLZWyNtZrVtB0LrpjPOktvA9mxjeM3KTj215VKb8b475lRgsGYeCasH/lSJEULR +9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUurmkVLoR9BvUhTFXFkC4az +5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU51nus6+N8 +6U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7 +Ngzp07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHP +bMk7ccHViLVlvMDoFxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXt +BznaqB16nzaeErAMZRKQFWDZJkBE41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTt +XUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleuyjWcLhL75Lpd +INyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwp +LiniyMMB8jPqKqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8 +Ipf3YF3qKS9Ysr1YvY2WTxB1v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixp +gZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA8KCWAg8zxXHzniN9lLf9OtMJgwYh +/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b8KKaa8MFSu1BYBQw +0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0rmj1A +fsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq +4BZ+Extq1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR +1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/ +QFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM +94B7IWcnMFk= +-----END CERTIFICATE----- + +# Starfield Class 2 CA +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +# Starfield Root Certificate Authority - G2 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Starfield Services Root Certificate Authority - G2 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# SwissSign Gold CA - G2 +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +# SwissSign Silver CA - G2 +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE +BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu +IFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow +RzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY +U3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv +Fz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br +YT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF +nbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH +6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt +eJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/ +c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ +MoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH +HTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf +jNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6 +5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB +rDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +F6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c +wpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB +AHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp +WJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9 +xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ +2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ +IseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8 +aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X +em1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR +dAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/ +OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+ +hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy +tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- + +# T-TeleSec GlobalRoot Class 2 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +# T-TeleSec GlobalRoot Class 3 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +# TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +# TWCA Global Root CA +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +# TWCA Root Certification Authority +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +# Taiwan GRCA +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/ +MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow +PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR +IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q +gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy +yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts +F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2 +jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx +ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC +VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK +YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH +EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN +Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud +DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE +MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK +UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ +TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf +qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK +ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE +JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7 +hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1 +EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm +nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX +udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz +ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe +LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl +pYYsfPQS +-----END CERTIFICATE----- + +# TeliaSonera Root CA v1 +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +# TrustCor ECA-1 +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYD +VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk +MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxFzAVBgNVBAMMDlRydXN0Q29y +IEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3MjgwN1owgZwxCzAJBgNV +BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw +IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy +dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3Ig +RUNBLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb +3w9U73NjKYKtR8aja+3+XzP4Q1HpGjORMRegdMTUpwHmspI+ap3tDvl0mEDTPwOA +BoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23xFUfJ3zSCNV2HykVh0A5 +3ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmcp0yJF4Ou +owReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/ +wZ0+fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZF +ZtS6mFjBAgMBAAGjYzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAf +BgNVHSMEGDAWgBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/ +MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAQEABT41XBVwm8nHc2Fv +civUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u/ukZMjgDfxT2 +AHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F +hcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50 +soIipX1TH0XsJ5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BI +WJZpTdwHjFGTot+fDz2LYLSCjaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1Wi +tJ/X5g== +-----END CERTIFICATE----- + +# TrustCor RootCert CA-1 +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYD +VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk +MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29y +IFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkxMjMxMTcyMzE2WjCB +pDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFuYW1h +IENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUG +A1UECwweVHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZU +cnVzdENvciBSb290Q2VydCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAv463leLCJhJrMxnHQFgKq1mqjQCj/IDHUHuO1CAmujIS2CNUSSUQIpid +RtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4pQa81QBeCQryJ3pS/C3V +seq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0JEsq1pme +9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CV +EY4hgLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorW +hnAbJN7+KIor0Gqw/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/ +DeOxCbeKyKsZn3MzUOcwHwYDVR0jBBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5mDo4Nvu7Zp5I +/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf +ke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZ +yonnMlo2HD6CqFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djts +L1Ac59v2Z3kf9YKVmgenFK+P3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdN +zl/HHk484IkzlQsPpTLWPFp5LBk= +-----END CERTIFICATE----- + +# TrustCor RootCert CA-2 +-----BEGIN CERTIFICATE----- +MIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNV +BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw +IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy +dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEfMB0GA1UEAwwWVHJ1c3RDb3Ig +Um9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEyMzExNzI2MzlaMIGk +MQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEg +Q2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYD +VQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRy +dXN0Q29yIFJvb3RDZXJ0IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCnIG7CKqJiJJWQdsg4foDSq8GbZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+ +QVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9NkRvRUqdw6VC0xK5mC8tkq +1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1oYxOdqHp +2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nK +DOObXUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hape +az6LMvYHL1cEksr1/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF +3wP+TfSvPd9cW436cOGlfifHhi5qjxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88 +oWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQPeSghYA2FFn3XVDjxklb9tTNM +g9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+CtgrKAmrhQhJ8Z3 +mjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh +8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAd +BgNVHQ4EFgQU2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6U +nrybPZx9mCAZ5YwwYrIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/hOsh80QA9z+LqBrWyOrsGS2h60COX +dKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnpkpfbsEZC89NiqpX+ +MWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv2wnL +/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RX +CI/hOWB3S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYa +ZH9bDTMJBzN7Bj8RpFxwPIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW +2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dvDDqPys/cA8GiCcjl/YBeyGBCARsaU1q7 +N6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYURpFHmygk71dSTlxCnKr3 +Sewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANExdqtvArB +As8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp +5KeXRKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu +1uwJ +-----END CERTIFICATE----- + +# Trustis FPS Root CA +-----BEGIN CERTIFICATE----- +MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL +ExNUcnVzdGlzIEZQUyBSb290IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTEx +MzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc +MBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQRUN+ +AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihH +iTHcDnlkH5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjj +vSkCqPoc4Vu5g6hBSLwacY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA +0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zto3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlB +OrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEAAaNTMFEwDwYDVR0TAQH/ +BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAdBgNVHQ4E +FgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01 +GX2cGE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmW +zaD+vkAMXBJV+JOCyinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP4 +1BIy+Q7DsdwyhEQsb8tGD+pmQQ9P8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZE +f1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHVl/9D7S3B2l0pKoU/rGXuhg8F +jZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN +ZetX2fNXlrtIzYE= +-----END CERTIFICATE----- + +# UCA Extended Validation Root +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF +eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx +MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV +BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog +D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS +sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop +O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk +sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi +c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj +VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz +KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ +TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G +sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs +1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD +fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN +l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ +VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 +c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp +4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s +t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj +2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO +vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C +xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx +cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM +fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax +-----END CERTIFICATE----- + +# UCA Global G2 Root +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH +bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x +CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds +b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr +b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 +kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm +VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R +VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc +C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj +tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY +D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv +j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl +NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 +iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP +O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV +ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj +L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl +1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU +b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV +PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj +y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb +EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg +DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI ++Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy +YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX +UB+K+wb1whnw0A== +-----END CERTIFICATE----- + +# USERTrust ECC Certification Authority +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +# USERTrust RSA Certification Authority +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +# VeriSign Class 3 Public Primary Certification Authority - G4 +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp +U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg +SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln +biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm +GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve +fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ +aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj +aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW +kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC +4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga +FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +# VeriSign Class 3 Public Primary Certification Authority - G5 +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +# VeriSign Universal Root Certification Authority +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB +vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W +ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0 +IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y +IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh +bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF +9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH +H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H +LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN +/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT +rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw +WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs +exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4 +sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+ +seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz +4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+ +BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR +lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 +7M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +# Verisign Class 3 Public Primary Certification Authority - G3 +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +# XRamp Global CA Root +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +# certSIGN ROOT CA +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +# ePKI Root Certification Authority +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +# emSign ECC Root CA - C3 +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG +EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx +IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND +IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci +MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti +sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O +BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c +3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J +0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +# emSign ECC Root CA - G3 +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +# emSign Root CA - C1 +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG +A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg +SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v +dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ +BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ +HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH +3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH +GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c +xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 +aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq +TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 +/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 +kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG +YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT ++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo +WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +# emSign Root CA - G1 +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +# thawte Primary Root CA +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- + +# thawte Primary Root CA - G2 +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp +IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi +BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw +MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig +YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v +dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/ +BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6 +papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K +DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3 +KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox +XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +# thawte Primary Root CA - G3 +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB +rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV +BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa +Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl +LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u +MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm +gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8 +YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf +b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9 +9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S +zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk +OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV +HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA +2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW +oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c +KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM +m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu +MdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- diff --git a/cert_db/Dockerfile b/cert_db/Dockerfile new file mode 100644 index 0000000..7b6a1dc --- /dev/null +++ b/cert_db/Dockerfile @@ -0,0 +1,69 @@ +# +# Copyright 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +# Install required packages +# +FROM alpine:3.18 as cert_db.preinstall +ENV PYTHONUNBUFFERED=1 +RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python && \ +apk add --update --no-cache py3-sqlalchemy py3-cryptography py3-numpy \ +py3-requests py3-flask py3-psycopg2 py3-pydantic=~1.10 && python3 -m ensurepip && \ +apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \ +py3-confluent-kafka && \ +pip3 install --no-cache --upgrade pip setuptools + +COPY cert_db/requirements.txt /wd/ +RUN pip3 install -r /wd/requirements.txt && mkdir -p /etc/xdg/fbrat +COPY config/ratapi.conf /etc/xdg/fbrat/ +RUN echo "AFC_APP_TYPE = 'cert_db'" >> /etc/xdg/fbrat/ratapi.conf +# +# Build Message Handler application +# +FROM alpine:3.18 as cert_db.build +COPY --from=cert_db.preinstall / / +# Development env +RUN apk add --update --no-cache cmake ninja +# +COPY CMakeLists.txt LICENSE.txt version.txt Doxyfile.in /wd/ +COPY cmake /wd/cmake/ +COPY pkg /wd/pkg/ +COPY src /wd/src/ +RUN mkdir -p -m 777 /wd/build +ARG BUILDREV=localbuild +RUN cd /wd/build && \ +cmake -DCMAKE_INSTALL_PREFIX=/wd/__install -DCMAKE_PREFIX_PATH=/usr -DCMAKE_BUILD_TYPE=RatapiRelease -DSVN_LAST_REVISION=$BUILDREV -G Ninja /wd && \ +ninja -j$(nproc) install +# +# Install FCCID_DB application +# +FROM alpine:3.18 as cert_db.install +COPY --from=cert_db.preinstall / / +COPY --from=cert_db.build /wd/__install /usr/ +COPY src/afc-packages /wd/afc-packages +RUN pip3 install --use-pep517 --root-user-action=ignore \ + -r /wd/afc-packages/pkgs.cert_db \ + && rm -rf /wd/afc-packages +RUN mkdir -m 755 -p /var/lib/fbrat +RUN mkdir -m 755 -p /var/spool/fbrat + +# Add user and group +RUN addgroup -g 1003 fbrat && \ +adduser -g '' -D -u 1003 -G fbrat -h /var/lib/fbrat -s /sbin/nologin fbrat && \ +chown fbrat:fbrat /var/lib/fbrat +# +LABEL revision="afc-cert_db" +WORKDIR /wd +COPY cert_db/entrypoint.sh / +# Add debugging env if configured +ARG AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +COPY cert_db/devel.sh /wd/ +RUN chmod +x /wd/devel.sh +RUN /wd/devel.sh +# +ADD cert_db/sweep.sh /etc/periodic/daily/ +RUN chmod 744 /etc/periodic/daily/sweep.sh +RUN chmod +x /entrypoint.sh +CMD ["/entrypoint.sh"] diff --git a/cert_db/devel.sh b/cert_db/devel.sh new file mode 100644 index 0000000..637cd56 --- /dev/null +++ b/cert_db/devel.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# +# Copyright 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +case "$AFC_DEVEL_ENV" in + "devel") + echo "Debug profile" + export NODE_OPTIONS='--openssl-legacy-provider' + apk add --update --no-cache cmake bash + ;; + "production") + echo "Production profile" + ;; + *) + echo "Uknown profile" + ;; +esac + +exit $? diff --git a/cert_db/entrypoint.sh b/cert_db/entrypoint.sh new file mode 100644 index 0000000..2d54acd --- /dev/null +++ b/cert_db/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# +# Copyright 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +if [[ -z "${K8S_CRON}" ]]; then + crond -b + sleep infinity +else + /usr/bin/rat-manage-api cert_id sweep --country US + /usr/bin/rat-manage-api cert_id sweep --country CA +fi diff --git a/cert_db/requirements.txt b/cert_db/requirements.txt new file mode 100644 index 0000000..563f15b --- /dev/null +++ b/cert_db/requirements.txt @@ -0,0 +1,23 @@ +alembic==1.8.1 +Flask==2.3.2 +Flask-JSONRPC==1.0.2 +Flask-Login==0.6.2 +Flask-SQLAlchemy==2.5.1 +Flask-User==1.0.2.1 +Flask-WTF==1.1.1 +Flask-Migrate==2.6.0 +Flask-Script==2.0.5 +Jinja2==3.1.2 +json5==0.9.10 +prettytable==3.5.0 +python_dateutil==2.8.2 +pyxdg==0.28 +email-validator==1.3.0 +jwt==1.3.1 +WsgiDAV==4.1.0 +typeguard==2.13.3 +celery==5.2.7 +Werkzeug==2.3.3 +gevent==23.9.1 +pika==1.3.2 + diff --git a/cert_db/sweep.sh b/cert_db/sweep.sh new file mode 100755 index 0000000..cf7ee02 --- /dev/null +++ b/cert_db/sweep.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# +# Copyright 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +/usr/bin/rat-manage-api cert_id sweep --country US +/usr/bin/rat-manage-api cert_id sweep --country CA diff --git a/cmake/CheckCoverage.cmake b/cmake/CheckCoverage.cmake new file mode 100644 index 0000000..1a56a6e --- /dev/null +++ b/cmake/CheckCoverage.cmake @@ -0,0 +1,41 @@ +# Add a target to allow test coverage analysis (all tests together). +# This option fails on non-unix systems +option(BUILD_WITH_COVERAGE "Run unit tests with code coverage analysis" OFF) + +if(BUILD_WITH_COVERAGE) + if(NOT UNIX) + message(FATAL_ERROR "Unable to coverage-check non-unix") + endif(NOT UNIX) + + FIND_PROGRAM(LCOV_PATH lcov) + IF(NOT LCOV_PATH) + MESSAGE(FATAL_ERROR "lcov not found") + ENDIF(NOT LCOV_PATH) + + FIND_PROGRAM(GENHTML_PATH genhtml) + IF(NOT GENHTML_PATH) + MESSAGE(FATAL_ERROR "genhtml not found!") + ENDIF(NOT GENHTML_PATH) + + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --coverage") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage") + + # Target to check with pre- and post-steps to capture coverage results + SET(OUT_RAW "${CMAKE_BINARY_DIR}/coverage-raw.lcov") + SET(OUT_CLEAN "${CMAKE_BINARY_DIR}/coverage-clean.lcov") + add_custom_target(check-coverage + # Before any tests + COMMAND ${LCOV_PATH} --directory . --zerocounters + + # Actually run the tests, ignoring exit code + COMMAND ${CMAKE_CTEST_COMMAND} --verbose || : + + # Pull together results, ignoring system files and auto-built files + COMMAND ${LCOV_PATH} --directory . --capture --output-file ${OUT_RAW} + COMMAND ${LCOV_PATH} --remove ${OUT_RAW} '*/test/*' '${CMAKE_BINARY_DIR}/*' '/usr/*' --output-file ${OUT_CLEAN} + COMMAND ${GENHTML_PATH} -o coverage ${OUT_CLEAN} + COMMAND ${CMAKE_COMMAND} -E remove ${OUT_RAW} ${OUT_CLEAN} + + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) +endif(BUILD_WITH_COVERAGE) diff --git a/cmake/Findminizip.cmake b/cmake/Findminizip.cmake new file mode 100644 index 0000000..b00b376 --- /dev/null +++ b/cmake/Findminizip.cmake @@ -0,0 +1,36 @@ + +IF(UNIX) + find_package(PkgConfig) + pkg_search_module(MINIZIP REQUIRED minizip) + if(NOT ${MINIZIP_FOUND} EQUAL 1) + message(FATAL_ERROR "minizip is missing") + endif() +ENDIF(UNIX) +IF(WIN32) + # Verify headers present + find_path(MINIZIP_MAIN_INCLUDE minizip/zip.h PATHS ${MINIZIP_INCLUDE_DIRS} ${CONAN_INCLUDE_DIRS}) + + # Verify link and dynamic library present + find_library(MINIZIP_MAIN_LIB NAMES minizip minizipd PATHS ${MINIZIP_LIBDIR} ${CONAN_LIB_DIRS}) + find_file(MINIZIP_MAIN_DLL NAMES minizip.dll minizipd.dll PATHS ${MINIZIP_BINDIR} ${CONAN_BIN_DIRS}) + message("-- Found minizip at ${MINIZIP_MAIN_LIB} ${MINIZIP_MAIN_DLL}") + + add_library(minizip SHARED IMPORTED) + set_target_properties(minizip PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${MINIZIP_MAIN_INCLUDE} + ) + set_target_properties(minizip PROPERTIES + IMPORTED_LOCATION "${MINIZIP_MAIN_DLL}" + IMPORTED_IMPLIB "${MINIZIP_MAIN_LIB}" + ) + + # handle the QUIETLY and REQUIRED arguments and set JPEG_FOUND to TRUE if + # all listed variables are TRUE + include(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(MINIZIP DEFAULT_MSG MINIZIP_MAIN_LIB MINIZIP_MAIN_INCLUDE) + + if(MINIZIP_FOUND) + set(MINIZIP_INCLUDE_DIRS ${MINIZIP_MAIN_INCLUDE}) + set(MINIZIP_LIBRARIES minizip) + endif(MINIZIP_FOUND) +ENDIF(WIN32) diff --git a/cmake/srcfunctions.cmake b/cmake/srcfunctions.cmake new file mode 100644 index 0000000..c79e32f --- /dev/null +++ b/cmake/srcfunctions.cmake @@ -0,0 +1,497 @@ +# Redirect add_... functions to accumulate target names + +# +# Define a library from sources with its headers. +# This relies on the pre-existing values: +# - "VERSION" to set the library PROJECT_VERSION property +# - "SOVERSION" to set the library SOVERSION property +# +function(add_dist_library) + set(PARSE_OPTS ) + set(PARSE_ARGS_SINGLE TARGET EXPORTNAME) + set(PARSE_ARGS_MULTI SOURCES HEADERS) + cmake_parse_arguments(ADD_DIST_LIB "${PARSE_OPTS}" "${PARSE_ARGS_SINGLE}" "${PARSE_ARGS_MULTI}" ${ARGN}) + if("${ADD_DIST_LIB_TARGET}" STREQUAL "") + message(FATAL_ERROR "add_dist_library missing TARGET parameter") + endif() + if("${ADD_DIST_LIB_EXPORTNAME}" STREQUAL "") + message(FATAL_ERROR "add_dist_library missing EXPORTNAME parameter") + endif() + if("${ADD_DIST_LIB_SOURCES}" STREQUAL "") + message(FATAL_ERROR "add_dist_library missing SOURCES parameter") + endif() + + if(WIN32 AND BUILD_SHARED_LIBS) + # Give the DLL version markings + set(WINRES_COMPANY_NAME_STR "OpenAFC") + set(WINRES_PRODUCT_NAME_STR ${PROJECT_NAME}) + set(WINRES_PRODUCT_VERSION_RES "${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0") + set(WINRES_PRODUCT_VERSION_STR "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}-${SVN_LAST_REVISION}") + set(WINRES_INTERNAL_NAME_STR ${ADD_DIST_LIB_TARGET}) + set(WINRES_ORIG_FILENAME "${CMAKE_SHARED_LIBRARY_PREFIX}${ADD_DIST_LIB_TARGET}${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(WINRES_FILE_DESCRIPTION_STR "Runtime for ${ADD_DIST_LIB_TARGET}") + set(WINRES_FILE_VERSION_RES ${WINRES_PRODUCT_VERSION_RES}) + set(WINRES_FILE_VERSION_STR ${WINRES_PRODUCT_VERSION_STR}) + set(WINRES_COMMENTS_STR "") + configure_file("${CMAKE_SOURCE_DIR}/src/libinfo.rc.in" "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_LIB_TARGET}-libinfo.rc" @ONLY) + list(APPEND ADD_DIST_LIB_SOURCES "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_LIB_TARGET}-libinfo.rc") + endif(WIN32 AND BUILD_SHARED_LIBS) + + add_library(${ADD_DIST_LIB_TARGET} ${ADD_DIST_LIB_SOURCES}) + set_target_properties( + ${ADD_DIST_LIB_TARGET} PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${SOVERSION} + ) + + include(GenerateExportHeader) + generate_export_header(${ADD_DIST_LIB_TARGET}) + target_include_directories(${ADD_DIST_LIB_TARGET} PUBLIC + $ + $/${PKG_INSTALL_INCLUDEDIR}> + ) + list(APPEND ADD_DIST_LIB_HEADERS "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_LIB_TARGET}_export.h") + + # Source-directory relative path + get_filename_component(SOURCE_DIRNAME ${CMAKE_CURRENT_SOURCE_DIR} NAME) + + # Include headers, with original directory name + install( + FILES ${ADD_DIST_LIB_HEADERS} + DESTINATION ${PKG_INSTALL_INCLUDEDIR}/${SOURCE_DIRNAME} + COMPONENT development + ) + + if(WIN32) + # PDB for symbol mapping + install( + FILES "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${ADD_DIST_LIB_TARGET}.pdb" + DESTINATION ${PKG_INSTALL_DEBUGDIR} + COMPONENT debuginfo + ) + # Sources for debugger (directory name is target name) + install( + FILES ${ADD_DIST_LIB_HEADERS} ${ADD_DIST_LIB_SOURCES} + DESTINATION ${PKG_INSTALL_DEBUGDIR}/${SOURCE_DIRNAME} + COMPONENT debuginfo + ) + endif(WIN32) + + install( + TARGETS ${ADD_DIST_LIB_TARGET} + EXPORT ${ADD_DIST_LIB_EXPORTNAME} + # For Win32 + RUNTIME + DESTINATION ${PKG_INSTALL_BINDIR} + COMPONENT runtime + ARCHIVE + DESTINATION ${PKG_INSTALL_LIBDIR} + COMPONENT development + # For unix + LIBRARY + DESTINATION ${PKG_INSTALL_LIBDIR} + COMPONENT runtime + ) +endfunction(add_dist_library) + +function(add_dist_module) + set(PARSE_OPTS ) + set(PARSE_ARGS_SINGLE TARGET EXPORTNAME COMPONENT) + set(PARSE_ARGS_MULTI SOURCES HEADERS) + cmake_parse_arguments(ADD_DIST_LIB "${PARSE_OPTS}" "${PARSE_ARGS_SINGLE}" "${PARSE_ARGS_MULTI}" ${ARGN}) + if("${ADD_DIST_LIB_TARGET}" STREQUAL "") + message(FATAL_ERROR "add_dist_library missing TARGET parameter") + endif() + if("${ADD_DIST_LIB_COMPONENT}" STREQUAL "") + set(ADD_DIST_LIB_COMPONENT runtime) + endif() + if("${ADD_DIST_LIB_SOURCES}" STREQUAL "") + message(FATAL_ERROR "add_dist_library missing SOURCES parameter") + endif() + if(NOT PKG_MODULE_LIBDIR) + message(FATAL_ERROR "Must define PKG_MODULE_LIBDIR for installation") + endif() + + if(WIN32 AND BUILD_SHARED_LIBS) + # Give the DLL version markings + set(WINRES_COMPANY_NAME_STR "OpenAFC") + set(WINRES_PRODUCT_NAME_STR ${PROJECT_NAME}) + set(WINRES_PRODUCT_VERSION_RES "${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0") + set(WINRES_PRODUCT_VERSION_STR "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}-${SVN_LAST_REVISION}") + set(WINRES_INTERNAL_NAME_STR ${ADD_DIST_LIB_TARGET}) + set(WINRES_ORIG_FILENAME "${CMAKE_SHARED_LIBRARY_PREFIX}${ADD_DIST_LIB_TARGET}${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(WINRES_FILE_DESCRIPTION_STR "Runtime for ${ADD_DIST_LIB_TARGET}") + set(WINRES_FILE_VERSION_RES ${WINRES_PRODUCT_VERSION_RES}) + set(WINRES_FILE_VERSION_STR ${WINRES_PRODUCT_VERSION_STR}) + set(WINRES_COMMENTS_STR "") + configure_file("${CMAKE_SOURCE_DIR}/src/libinfo.rc.in" "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_LIB_TARGET}-libinfo.rc" @ONLY) + list(APPEND ADD_DIST_LIB_SOURCES "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_LIB_TARGET}-libinfo.rc") + endif(WIN32 AND BUILD_SHARED_LIBS) + + add_library(${ADD_DIST_LIB_TARGET} MODULE ${ADD_DIST_LIB_SOURCES}) + set_target_properties( + ${ADD_DIST_LIB_TARGET} PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${SOVERSION} + # no "lib" prefix on unix + PREFIX "" + ) + + include(GenerateExportHeader) + generate_export_header(${ADD_DIST_LIB_TARGET}) + list(APPEND ADD_DIST_LIB_HEADERS "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_LIB_TARGET}_export.h") + + # Source-directory relative path + get_filename_component(SOURCE_DIRNAME ${CMAKE_CURRENT_SOURCE_DIR} NAME) + + if(WIN32) + # PDB for symbol mapping + install( + FILES "${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}/${ADD_DIST_LIB_TARGET}.pdb" + DESTINATION ${PKG_INSTALL_DEBUGDIR} + COMPONENT debuginfo + ) + # Sources for debugger (directory name is target name) + install( + FILES ${ADD_DIST_LIB_HEADERS} ${ADD_DIST_LIB_SOURCES} + DESTINATION ${PKG_INSTALL_DEBUGDIR}/${SOURCE_DIRNAME} + COMPONENT debuginfo + ) + endif(WIN32) + + install( + TARGETS ${ADD_DIST_LIB_TARGET} + EXPORT ${ADD_DIST_LIB_EXPORTNAME} + # For Win32 + RUNTIME + DESTINATION ${PKG_MODULE_LIBDIR} + COMPONENT ${ADD_DIST_LIB_COMPONENT} + ARCHIVE + DESTINATION ${PKG_INSTALL_LIBDIR} + COMPONENT development + # For unix + LIBRARY + DESTINATION ${PKG_MODULE_LIBDIR} + COMPONENT ${ADD_DIST_LIB_COMPONENT} + ) +endfunction(add_dist_module) + +function(add_dist_executable) + set(PARSE_OPTS SYSTEMEXEC) + set(PARSE_ARGS_SINGLE TARGET EXPORTNAME) + set(PARSE_ARGS_MULTI SOURCES HEADERS) + cmake_parse_arguments(ADD_DIST_BIN "${PARSE_OPTS}" "${PARSE_ARGS_SINGLE}" "${PARSE_ARGS_MULTI}" ${ARGN}) + if("${ADD_DIST_BIN_TARGET}" STREQUAL "") + message(FATAL_ERROR "add_dist_executable missing TARGET parameter") + endif() + if("${ADD_DIST_BIN_SOURCES}" STREQUAL "") + message(FATAL_ERROR "add_dist_executable missing SOURCES parameter") + endif() + + if(WIN32) + # Give the DLL version markings + set(WINRES_COMPANY_NAME_STR "OpenAFC") + set(WINRES_PRODUCT_NAME_STR ${PROJECT_NAME}) + set(WINRES_PRODUCT_VERSION_RES "${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0") + set(WINRES_PRODUCT_VERSION_STR "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}-${SVN_LAST_REVISION}") + set(WINRES_INTERNAL_NAME_STR ${ADD_DIST_BIN_TARGET}) + set(WINRES_ORIG_FILENAME "${ADD_DIST_BIN_TARGET}${CMAKE_EXECUTABLE_SUFFIX}") + set(WINRES_FILE_DESCRIPTION_STR "Runtime for ${ADD_DIST_BIN_TARGET}") + set(WINRES_FILE_VERSION_RES ${WINRES_PRODUCT_VERSION_RES}) + set(WINRES_FILE_VERSION_STR ${WINRES_PRODUCT_VERSION_STR}) + set(WINRES_COMMENTS_STR "") + configure_file("${CMAKE_SOURCE_DIR}/src/libinfo.rc.in" "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_BIN_TARGET}-libinfo.rc" @ONLY) + list(APPEND ADD_DIST_BIN_SOURCES "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_BIN_TARGET}-libinfo.rc") + endif(WIN32) + + add_executable(${ADD_DIST_BIN_TARGET} ${ADD_DIST_BIN_SOURCES}) + + if(TARGET Threads::Threads) + target_link_libraries(${ADD_DIST_BIN_TARGET} PRIVATE Threads::Threads) + endif() + + if(${ADD_DIST_BIN_SYSTEMEXEC}) + set(ADD_DIST_BIN_DEST ${PKG_INSTALL_SBINDIR}) + else() + set(ADD_DIST_BIN_DEST ${PKG_INSTALL_BINDIR}) + endif() + install( + TARGETS ${ADD_DIST_BIN_TARGET} + EXPORT ${ADD_DIST_BIN_EXPORTNAME} + DESTINATION ${ADD_DIST_BIN_DEST} + COMPONENT runtime + ) + if(WIN32) + get_filename_component(SOURCE_DIRNAME ${CMAKE_CURRENT_SOURCE_DIR} NAME) + # PDB for symbol mapping + install( + FILES "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${ADD_DIST_BIN_TARGET}.pdb" + DESTINATION ${PKG_INSTALL_DEBUGDIR} + COMPONENT debuginfo + ) + # Sources for debugger (directory name is target name) + install( + FILES ${ADD_DIST_BIN_HEADERS} ${ADD_DIST_BIN_SOURCES} + DESTINATION ${PKG_INSTALL_DEBUGDIR}/${SOURCE_DIRNAME} + COMPONENT debuginfo + ) + endif(WIN32) +endfunction(add_dist_executable) + +# +# Define a python library from sources. +# The named function arguments are: +# TARGET: The cmake target name to create. +# SETUP_TEMPLATE: A file to be used as template for setup.py. +# COMPONENT: The cmake "install" component to install the library as. +# SOURCEDIR: The root directory of all sources for the target, python files or otherwise. +# +# When processing the setup template, a variable is created for a windows-safe +# escaped file path to the source directory named +# DIST_LIB_PACKAGE_DIR_ESCAPED. +# +function(add_dist_pythonlibrary) + set(PARSE_OPTS ) + set(PARSE_ARGS_SINGLE TARGET SETUP_TEMPLATE SOURCEDIR COMPONENT) + set(PARSE_ARGS_MULTI ) + cmake_parse_arguments(ADD_DIST_LIB "${PARSE_OPTS}" "${PARSE_ARGS_SINGLE}" "${PARSE_ARGS_MULTI}" ${ARGN}) + if("${ADD_DIST_LIB_TARGET}" STREQUAL "") + message(FATAL_ERROR "add_dist_pythonlibrary missing TARGET parameter") + endif() + if("${ADD_DIST_LIB_SETUP_TEMPLATE}" STREQUAL "") + message(FATAL_ERROR "add_dist_pythonlibrary missing SETUP_TEMPLATE parameter") + endif() + if("${ADD_DIST_LIB_SOURCEDIR}" STREQUAL "") + message(FATAL_ERROR "add_dist_pythonlibrary missing SOURCEDIR parameter") + endif() + + find_program(PYTHON_BIN "python") + if(NOT PYTHON_BIN) + message(FATAL_ERROR "Missing executable for 'python'") + endif() + + # Setuptools runs on copy of source in the build path + set(ADD_DIST_LIB_SOURCECOPY "${CMAKE_CURRENT_BINARY_DIR}/pkg") + # Need to escape the path for windows + if(WIN32) + string(REPLACE "/" "\\\\" DIST_LIB_PACKAGE_DIR_ESCAPED ${ADD_DIST_LIB_SOURCECOPY}) + else(WIN32) + set(DIST_LIB_PACKAGE_DIR_ESCAPED ${ADD_DIST_LIB_SOURCECOPY}) + endif(WIN32) + + # Assemble the actual setup.py input + configure_file(${ADD_DIST_LIB_SETUP_TEMPLATE} setup.py @ONLY) + + # Command depends on all source files, package-included or not + # Record an explicit sentinel file for the build + file(GLOB_RECURSE ALL_PACKAGE_FILES "${ADD_DIST_LIB_SOURCEDIR}/*") + add_custom_command( + DEPENDS ${ALL_PACKAGE_FILES} + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/timestamp" + COMMAND ${CMAKE_COMMAND} -E remove_directory ${ADD_DIST_LIB_SOURCECOPY} + COMMAND ${CMAKE_COMMAND} -E copy_directory ${ADD_DIST_LIB_SOURCEDIR} ${ADD_DIST_LIB_SOURCECOPY} + COMMAND ${PYTHON_BIN} "${CMAKE_CURRENT_BINARY_DIR}/setup.py" build --quiet + COMMAND ${CMAKE_COMMAND} -E touch "${CMAKE_CURRENT_BINARY_DIR}/timestamp" + ) + add_custom_target(${ADD_DIST_LIB_TARGET} ALL + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/timestamp" + ) + + # Use DESTDIR from actual install environment + set(ADD_DIST_LIB_INSTALL_CMD "${PYTHON_BIN} \"${CMAKE_CURRENT_BINARY_DIR}/setup.py\" install --root=\$DESTDIR/${CMAKE_INSTALL_PREFIX} --prefix=") + if(PKG_INSTALL_PYTHONSITEDIR) + set(ADD_DIST_LIB_INSTALL_CMD "${ADD_DIST_LIB_INSTALL_CMD} --install-lib=${PKG_INSTALL_PYTHONSITEDIR}") + endif() + install( + CODE "execute_process(COMMAND ${ADD_DIST_LIB_INSTALL_CMD})" + COMPONENT ${ADD_DIST_LIB_COMPONENT} + ) + +endfunction(add_dist_pythonlibrary) + +# Use qt "lrelease" to generate a translation binary from a source file. +# The named function arguments are: +# TARGET: The output QM file to create. +# SOURCE: The input TS file to read. +function(add_qt_translation) + set(PARSE_OPTS ) + set(PARSE_ARGS_SINGLE TARGET SOURCE) + set(PARSE_ARGS_MULTI ) + cmake_parse_arguments(ADD_TRANSLATION "${PARSE_OPTS}" "${PARSE_ARGS_SINGLE}" "${PARSE_ARGS_MULTI}" ${ARGN}) + if(NOT ADD_TRANSLATION_TARGET) + message(FATAL_ERROR "add_qt_translation missing TARGET parameter") + endif() + if(NOT ADD_TRANSLATION_SOURCE) + message(FATAL_ERROR "add_qt_translation missing SOURCE parameter") + endif() + + find_package(Qt5LinguistTools) + add_custom_command( + OUTPUT ${ADD_TRANSLATION_TARGET} + DEPENDS ${ADD_TRANSLATION_SOURCE} + COMMAND Qt5::lrelease -qm "${ADD_TRANSLATION_TARGET}" "${ADD_TRANSLATION_SOURCE}" + ) +endfunction(add_qt_translation) + +# Common run-time test behavior +set(GTEST_RUN_ARGS "--gtest_output=xml:test-detail.junit.xml") +function(add_gtest_executable TARGET_NAME ...) + add_executable(${ARGV}) + set_target_properties(${TARGET_NAME} PROPERTIES + COMPILE_FLAGS "-DGTEST_LINKED_AS_SHARED_LIBRARY=1" + ) + target_include_directories(${TARGET_NAME} PRIVATE ${GTEST_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} PRIVATE ${GTEST_BOTH_LIBRARIES}) + + find_package(Threads QUIET) + if(TARGET Threads::Threads) + target_link_libraries(${TARGET_NAME} PRIVATE Threads::Threads) + endif() + + add_test( + NAME ${TARGET_NAME} + COMMAND ${TARGET_NAME} ${GTEST_RUN_ARGS} + ) + set_property( + TEST ${TARGET_NAME} + APPEND PROPERTY + ENVIRONMENT + "TEST_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}" + "TEST_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}" + ) + if(UNIX) + set_property( + TEST ${TARGET_NAME} + APPEND PROPERTY + ENVIRONMENT + "XDG_DATA_DIRS=${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}:/usr/share" + ) + elseif(WIN32) + set_property( + TEST ${TARGET_NAME} + APPEND PROPERTY + ENVIRONMENT + "LOCALAPPDATA=${CMAKE_INSTALL_PREFIX}\\${CMAKE_INSTALL_DATADIR}" + ) + + set(PATH_WIN "${CMAKE_INSTALL_PREFIX}\\bin\;${GTEST_PATH}\;$ENV{PATH}") + # escape for ctest string processing + string(REPLACE ";" "\\;" PATH_WIN "${PATH_WIN}") + string(REPLACE "/" "\\" PATH_WIN "${PATH_WIN}") + set_property( + TEST ${TARGET_NAME} + APPEND PROPERTY + ENVIRONMENT + "PATH=${PATH_WIN}" + "QT_PLUGIN_PATH=${CMAKE_INSTALL_PREFIX}\\bin" + ) + endif() +endfunction(add_gtest_executable) + +function(add_nosetest_run TEST_NAME) + find_program(NOSETEST_BIN "nosetests") + if(NOT NOSETEST_BIN) + message(FATAL_ERROR "Missing executable for 'nosetests'") + endif() + set(NOSETEST_RUN_ARGS "-v" "--with-xunit" "--xunit-file=test-detail.xunit.xml") + add_test( + NAME ${TEST_NAME} + COMMAND ${NOSETEST_BIN} ${CMAKE_CURRENT_SOURCE_DIR} ${NOSETEST_RUN_ARGS} + ) + + set_property( + TEST ${TEST_NAME} + APPEND PROPERTY + ENVIRONMENT + "TEST_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}" + "TEST_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}" + ) + if(UNIX) + set_property( + TEST ${TEST_NAME} + APPEND PROPERTY + ENVIRONMENT + "XDG_DATA_DIRS=${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}:/usr/share" + ) + elseif(WIN32) + set_property( + TEST ${TEST_NAME} + APPEND PROPERTY + ENVIRONMENT + "LOCALAPPDATA=${CMAKE_INSTALL_PREFIX}\\${CMAKE_INSTALL_DATADIR}" + ) + endif() +endfunction(add_nosetest_run) + +# +# Define a web site library from scources. +# Yarn build should output to /www directory relative to build directory. +# Function Arguments: +# TARGET: the cmake target name to create +# SETUP_TEMPLATES files to be used as templates for webpack.*.js +# COMPONENT: The cmake "install" component to install the library as. +# SOURCES: All of the dependencies of the target +# +function(add_dist_yarnlibrary) + + set(PARSE_OPTS) + set(PARSE_ARGS_SINGLE TARGET COMPONENT) + set(PARSE_ARGS_MULTI SOURCES SETUP_TEMPLATES) + cmake_parse_arguments(ADD_DIST_LIB "${PARSE_OPTS}" "${PARSE_ARGS_SINGLE}" "${PARSE_ARGS_MULTI}" ${ARGN}) + if("${ADD_DIST_LIB_TARGET}" STREQUAL "") + message(FATAL_ERROR "add_dist_yarnlibrary missing TARGET parameter") + endif() + + if("${ADD_DIST_LIB_SETUP_TEMPLATES}" STREQUAL "") + message(FATAL_ERROR "add_dist_yarnlibrary missing SETUP_TEMPLATES parameter") + endif() + + if("${ADD_DIST_LIB_SOURCES}" STREQUAL "") + message(FATAL_ERROR "add_dist_yarnlibrary missing SOURCES parameter") + endif() + + # TODO: only build working is build-dev + #if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + set(YARN_BUILD_TYPE "build-dev") + message(STATUS "will build yarn in DEV mode.") + #else() + # set(YARN_BUILD_TYPE "build") + #endif() + + find_program(YARN "yarn") + + # Setuptools runs on copy of source in the build path + set(ADD_DIST_LIB_SOURCECOPY "${CMAKE_CURRENT_BINARY_DIR}/pkg") + + foreach(SETUP_TEMPLATE ${ADD_DIST_LIB_SETUP_TEMPLATES}) + message(STATUS "${ADD_DIST_LIB_SOURCECOPY}/${SETUP_TEMPLATE}") + string(REPLACE ".in" ".js" CONFIG_NAME ${SETUP_TEMPLATE}) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/${SETUP_TEMPLATE}" "${ADD_DIST_LIB_SOURCECOPY}/${CONFIG_NAME}" @ONLY) + endforeach(SETUP_TEMPLATE) + + + # Record an explicit sentinel file for the build + add_custom_command( + DEPENDS ${SOURCES} + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/timestamp" + message(STATUS "Building YARN") + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/*" "${ADD_DIST_LIB_SOURCECOPY}" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/src" "${ADD_DIST_LIB_SOURCECOPY}/src" + COMMAND ${YARN} --cwd ${ADD_DIST_LIB_SOURCECOPY} + COMMAND ${YARN} --cwd ${ADD_DIST_LIB_SOURCECOPY} version --no-git-tag-version --new-version "${PROJECT_VERSION}-${SVN_LAST_REVISION}" + COMMAND ${YARN} --cwd ${ADD_DIST_LIB_SOURCECOPY} ${YARN_BUILD_TYPE} + COMMAND ${CMAKE_COMMAND} -E touch "${CMAKE_CURRENT_BINARY_DIR}/timestamp" + ) + + add_custom_target(${ADD_DIST_LIB_TARGET} ALL + # COMMAND ${CMAKE_COMMAND} -E r ${CMAKE_CURRENT_BINARY_DIR}/timestamp + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/timestamp" + ) + + install( + DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/www" + DESTINATION "${PKG_INSTALL_DATADIR}" + COMPONENT ${ADD_DIST_LIB_COMPONENT} + ) + +endfunction(add_dist_yarnlibrary) diff --git a/config/ratapi.conf b/config/ratapi.conf new file mode 100644 index 0000000..c409045 --- /dev/null +++ b/config/ratapi.conf @@ -0,0 +1,17 @@ +# Flask settings +PROPAGATE_EXCEPTIONS = False +SECRET_KEY = '9XXc5Lw+DZwXINyOmKcY5c41AMhLabqn4jFLXJntsVutrZCauB5W/AOv7tDbp63ge2SS2Ujz/OnfeQboJOrbsQ' + +# Flask-SQLAlchemy settings +SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:N3SF0LVKJx1RAhFGx4fcw@ratdb/fbrat' +SQLALCHEMY_POOL_SIZE = 30 + +# Flask-User settings +USER_EMAIL_SENDER_EMAIL = 'fbrat@a9556f3227ba.ihl.broadcom.net' +SESSION_COOKIE_SECURE = True +SESSION_COOKIE_SAMESITE = 'Lax' + +# RAT settings +GOOGLE_APIKEY = 'AIzaSyAjcMamfS5LhIRzQ6Qapi0uKX151himkmQ' +HISTORY_DIR = '/mnt/nfs/rat_transfer/history' +DEFAULT_ULS_DIR = '/mnt/nfs/rat_transfer/ULS_Database' diff --git a/database_readme.md b/database_readme.md new file mode 100644 index 0000000..befd9c4 --- /dev/null +++ b/database_readme.md @@ -0,0 +1,400 @@ +# Database Readme + +## **Database Description** + +### **Details of Databases** +#### **FS_Database:** +contains parameters defining the FS links for interference analysis. +For the ULS databaes (for the US) These are: +* FS Tx/Rx CallSign +* FS Tx/Rx Lat/Long and Height above ground level, +* FS Diversity Height above ground level, +* FS Primary and Diversity Rx Gain, Antenna Model, Antenna Diameter, +* FS Start/End Frequencies and Bandwidth, +* FS passive repeater Lat/Long, Height above ground level, dimensions, antenna model, antenna diameter and gain +* FS Rx near-field adjustment factor parameters + +contains parameters defining exclusion zone(s) around each RAS antenna that needs to be protected. + +contains parameters defining FS Rx actual antenna pattern (angle-off-boresight vs. discrimination gain). + +#### **proc_lidar_2019:** +contains json files that allow showing boundaries of RAS exclusion zones and LiDAR in GUI. +* RAS_ExclusionZone.json +* LiDAR_Bounds.json.gz + +This also contains all lidar tiles where each city has a subdirectory with tiles with a .csv that isn’t under the city subdirectory. The lidar zip file contains all of this. + +#### **Multiband-BDesign3D:** +contains building database over Manhattan. +#### **globe:** +contains NOAA GLOBE (1km resolution) terrain database. +#### **srtm3arcsecondv003:** +contains 3arcsec (=90m) SRTM terrain database files. These are used in the regions where 1arcsec 3DEP is used in the event a 3DEP tile is missing. +#### **srtm1arcsecond:** +contains 1arcsec (=30m) SRTM terrain database files. This is used in regions where 1arcsec 3DEP is not available. +#### **3dep:** +The 1_arcsec subdirectory (one currently used) contains 1arcsec (=30m) 3DEP terrain database files over US, Canada and Mexico. +#### **cdsm:** +contains the Natural Resources Canada Canadian Digital Surface Model (CDSM), 2000 at the highest available resolution. + +#### **nlcd:** +contains nlcd_2019_land_cover_I48_20210604_resample.zip (referred to as "Production NLCD" in AFC Config UI) and federated_nlcd.zip (referred to as "WFA Test NLCD" in AFC Config UI) files. This is used to determine RLAN/FS morphology (i.e. Urban, Suburban or Rural) to pick the appropriate path/clutter loss model. In addition, it is used to determine the appropriate P.452 Rural clutter category. +#### **landcover-2020-classification:** +The 2020 Land Cover of Canada produced by Natural Resources Canada. +#### **clc:** +Corine Land Cover is land categorization over the EU used to determine RLAN/FS morphology. +#### **population:** +contains the Gridded Population of the World (GPW), v4.11, population density database. Use of GPW for determination of RLAN morphology is only used in the absence of a land cover database. + +#### **US.kml:** +specifies United States' country boundary where AP access is allowed for that region. +#### **CA.kml:** +specifies Canada's country boundary where AP access is allowed for that region. +#### **GB.kml:** +specifies the Great Britain country boundary where AP access is allowed for that region. +#### **BRA.kml:** +specifies Brazil's country boundary where AP access is allowed for that region. + +#### **itudata:** +contains two ITU maps that are used by the ITM path loss model. 1) Radio Climate map (TropoClim.txt) and 2) Surface Refractivity map (N050.txt) + +#### **winnforum databases:** +these are WinnForum databases used by the FS Parser (antenna_model_diameter_gain.csv, billboard_reflector.csv, category_b1_antennas.csv, high_performance_antennas.csv, fcc_fixed_service_channelization.csv, transmit_radio_unit_architecture.csv). They provide the data to validate/fix/fill-in the corresponding ULS parameters. Two other WinnForum databases (nfa_table_data.csv and rat_transfer/pr/WINNF-TS-1014-V1.2.0-App02.csv) are used by the AFC Engine for near-field adjustment factor calculation for primary/diversity receivers and passive sites respectively. Note that the nfa_table_data.csv is generated manually as a simplied version of WINNF-TS-1014-V1.2.0-App01.csv. The use of these databases is described in WINNF-TS-1014 and WINNF-TS-5008 documents. + +### **Location or procedure to download/acquire these databases** +* **FS_Database:** Created using FS Script Parser from ULS raw data on FCC website (see details in the ULS Script documentation), RAS database from FCC 47CFR Part 15.407, and ISED's 6GHz DataExtract database on https://ised-isde.canada.ca/site/spectrum-management-system/en/spectrum-management-system-data + +* **proc_lidar_2019:** raw data obtained from https://rockyweb.usgs.gov/vdelivery/Datasets/Staged/Elevation/Non_Standard_Contributed/NGA_US_Cities/ +* **Multiband-BDesign3D:** this was purchased https://www.b-design3d.com/ + +* **globe:** https://ngdc.noaa.gov/mgg/topo/globe.html +* **srtm3arcsecondv003:** https://www2.jpl.nasa.gov/srtm/ +* **srtm1arcsecond:** https://search.earthdata.nasa.gov/search/granules?p=C1000000240-LPDAAC_ECS&pg[0][v]=f&pg[0][gsk]=-start_date&q=srtm&tl=1702926101.019!3!! +* **3dep:** https://data.globalchange.gov/dataset/usgs-national-elevation-dataset-ned-1-arc-second +* **cdsm:"** https://open.canada.ca/data/en/dataset/768570f8-5761-498a-bd6a-315eb6cc023d + +* **nlcd:** original file nlcd_2019_land_cover_I48_20210604 was downloaded from [link](https://www.mrlc.gov/data?f%5B0%5D=category%3Aland%20cover) (download NLCD 2019 Land Cover (CONUS)). Using gdal utilties this file was translated to nlcd_2019_land_cover_I48_20210604_resample.zip so that the 1-arcsec tiles matchup with 1-arcsec 3DEP tiles. The federated_nlcd.zip file was obtained by using other gdal utilities to convert federated's many files to one file covering CONUS. +* **landcover-2020-classification:** original file was downloaded from [link](https://open.canada.ca/data/en/dataset/ee1580ab-a23d-4f86-a09b-79763677eb47). Using gdal utilies this file was translated to landcover-2020-classification_resampled.tif so that the 1-arcsec tiles matchup with 1-arcsec 3DEP tiles and the canada landcover classifications are mapped to the equivalent NLCD codes. +* **clc:** original file was downloaded from the [Copernicus](https://land.copernicus.eu/pan-european/corine-land-cover/clc2018) website (download the GeoTIFF data). Login is required. Using gdal utilies this file was translated to landcover-2020-classification_resampled.tif so that the 1-arcsec tiles matchup with 1-arcsec 3DEP tiles and the canada landcover classifications are mapped to the equivalent NLCD codes. +* **population:** https://sedac.ciesin.columbia.edu/data/set/gpw-v4-population-density-rev11 + +* **US.kml:** https://public.opendatasoft.com/explore/dataset/world-administrative-boundaries/export/?flg=en-us +* **CA.kml:** https://www12.statcan.gc.ca/census-recensement/2021/geo/sip-pis/boundary-limites/index2021-eng.cfm?year=21 (Catrographic Boundary files, selecting 'Provinces/territories' of Administrative boundaries) +* **GB.kml:** https://public.opendatasoft.com/explore/dataset/world-administrative-boundaries/export/?flg=en-us +* **BRA.kml:** https://public.opendatasoft.com/explore/dataset/world-administrative-boundaries/export/?flg=en-us + +* **itudata:** Radio Climate map from ITU-R Rec, P.617-3 (https://www.itu.int/rec/R-REC-P.617-3-201309-S/en) and Surface Refractivity map from ITU-R Rec, P.452-17 (https://www.itu.int/rec/R-REC-P.452-17-202109-I/en) + +* **winnforum databases:** The Winnforum databases used by FS parser can be downloaded from here: Use https://github.com/Wireless-Innovation-Forum/6-GHz-AFC/tree/main/data/common_data to open in browser. The scripts use: https://raw.githubusercontent.com/Wireless-Innovation-Forum/6-GHz-AFC/main/data/common_data/ for downloading. The near-field adjustment factor databases can be downloaded from: https://6ghz.wirelessinnovation.org/baseline-standards. + +### **Licenses and Source Citations** + +#### **proc_lidar_2019** +Available for public use with no restrictions + +Disclaimer and quality information is at https://rockyweb.usgs.gov/vdelivery/Datasets/Staged/Elevation/Non_Standard_Contributed/NGA_US_Cities/00_NGA%20133%20US%20Cities%20Data%20Disclaimer%20and%20Explanation%20Readme.pdf + +#### **Globe** +Public domain + +NOAA National Geophysical Data Center. 1999: Global Land One-kilometer Base Elevation (GLOBE) v.1. NOAA National Centers for Environmental Information. https://doi.org/10.7289/V52R3PMS. Accessed TBD + +#### **3DEP** +Public domain + +Data available from U.S. Geological Survey, National Geospatial Program. + +#### **srtm1arcsecond** +Public domain + +NASA JPL (2013). NASA Shuttle Radar Topography Mission Global 1 arc second [Data set]. NASA EOSDIS Land Processes Distributed Active Archive Center. + +#### **CDSM** +Natural Resource of Canada. (2015). Canada Digital Surface Model [Data set]. https://open.canada.ca/data/en/dataset/768570f8-5761-498a-bd6a-315eb6cc023d. Contains information licensed under the Open Government Licence – Canada. + +#### **NLCD** +Public domain + +References: + +Dewitz, J., and U.S. Geological Survey, 2021, National Land Cover Database (NLCD) 2019 Products (ver. 2.0, June 2021): U.S. Geological Survey data release, https://doi.org/10.5066/P9KZCM54 + +Wickham, J., Stehman, S.V., Sorenson, D.G., Gass, L., and Dewitz, J.A., 2021, Thematic accuracy assessment of the NLCD 2016 land cover for the conterminous United States: Remote Sensing of Environment, v. 257, art. no. 112357, at https://doi.org/10.1016/j.rse.2021.112357 + +Homer, Collin G., Dewitz, Jon A., Jin, Suming, Xian, George, Costello, C., Danielson, Patrick, Gass, L., Funk, M., Wickham, J., Stehman, S., Auch, Roger F., Riitters, K. H., Conterminous United States land cover change patterns 2001–2016 from the 2016 National Land Cover Database: ISPRS Journal of Photogrammetry and Remote Sensing, v. 162, p. 184–199, at https://doi.org/10.1016/j.isprsjprs.2020.02.019 + +Jin, Suming, Homer, Collin, Yang, Limin, Danielson, Patrick, Dewitz, Jon, Li, Congcong, Zhu, Z., Xian, George, Howard, Danny, Overall methodology design for the United States National Land Cover Database 2016 products: Remote Sensing, v. 11, no. 24, at https://doi.org/10.3390/rs11242971 + +Yang, L., Jin, S., Danielson, P., Homer, C., Gass, L., Case, A., Costello, C., Dewitz, J., Fry, J., Funk, M., Grannemann, B., Rigge, M. and G. Xian. 2018. A New Generation of the United States National Land Cover Database: Requirements, Research Priorities, Design, and Implementation Strategies, ISPRS Journal of Photogrammetry and Remote Sensing, 146, pp.108-123. + +#### **2020 Canada Land Cover** +Natural Resource of Canada. (2022). 2020 Land Cover of Canada [Data set]. https://open.canada.ca/data/en/dataset/ee1580ab-a23d-4f86-a09b-79763677eb47. Contains information licensed under the Open Government Licence – Canada. + +#### **Corine Land Cover** + +Access to data is based on a principle of full, open and free access as established by the Copernicus data and information policy Regulation (EU) No 1159/2013 of 12 July 2013. This regulation establishes registration and licensing conditions for GMES/Copernicus users and can be found [here](http://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32013R1159) + +Free, full and open access to this data set is made on the conditions that: + +1. When distributing or communicating Copernicus dedicated data and Copernicus service information to the public, users shall inform the public of the source of that data and information. +2. Users shall make sure not to convey the impression to the public that the user's activities are officially endorsed by the Union. +3. Where that data or information has been adapted or modified, the user shall clearly state this. +4. The data remain the sole property of the European Union. Any information and data produced in the framework of the action shall be the sole property of the European Union. Any communication and publication by the beneficiary shall acknowledge that the data were produced “with funding by the European Union”. + +Reference: + +©European Union, Copernicus Land Monitoring Service 2018, European Environment Agency (EEA) + +#### **population** +Creative Commons Attribution 4.0 International (CC BY) License (https://creativecommons.org/licenses/by/4.0) + +Center for International Earth Science Information Network - CIESIN - Columbia University. 2018. Gridded Population of the World, Version 4 (GPWv4): Population Density, Revision 11. Palisades, New York: NASA Socioeconomic Data and Applications Center (SEDAC). https://doi.org/10.7927/H49C6VHW + +#### **Canada country boundary** +Statistics Canada (2022). Boundary Files, Census Year 2021 [Data set]. https://www12.statcan.gc.ca/census-recensement/2021/geo/sip-pis/boundary-limites/index2021-eng.cfm?year=21. Reproduced and distributed on an "as is" basis with the permission of Statistics Canada. + + +#### **winnforum databases** +Available for public use under the copyright of The Software Defined Radio Forum, Inc. doing business as the Wireless Innovation Forum. + +THIS DOCUMENT (OR WORK PRODUCT) IS BEING OFFERED WITHOUT ANY WARRANTY WHATSOEVER, AND IN PARTICULAR, ANY WARRANTY OF NON-INFRINGEMENT IS EXPRESSLY DISCLAIMED. ANY USE OF THIS SPECIFICATION (OR WORK PRODUCT) SHALL BE MADE ENTIRELY AT THE IMPLEMENTER'S OWN RISK, AND NEITHER THE FORUM, NOR ANY OF ITS MEMBERS OR SUBMITTERS, SHALL HAVE ANY LIABILITY WHATSOEVER TO ANY IMPLEMENTER OR THIRD PARTY FOR ANY DAMAGES OF ANY NATURE WHATSOEVER, DIRECTLY OR INDIRECTLY, ARISING FROM THE USE OF THIS DOCUMENT (OR WORK PRODUCT). + +## **Database Update** + +### **Expected update frequency of each database file** +* **FS_Database:** daily (per FCC and ISED requirements). Note that the RAS database portion is expected to be updated rarely. +* **proc_lidar_2019:** every few years (whenever a new database is available) +* **Multiband-BDesign3D:** no change (unless a newer building database for Manhattan is needed) +* **globe:** every few years (whenever a new database is available) +* **srtm3arcsecondv003:** every few years (whenever a new database is available) +* **srtm1arcsecond:** every few years (whenever a new database is available) +* **3dep:** every few years (whenever a new database is available) +* **cdsm:** every few years (whenever a new database is available) +* **nlcd:** every few years (whenever a new database is available) +* **2020canadalandcover:** every few years (whenever a new database is available) +* **clc:** every few years (whenever a new database is available) +* **population:** every few years (whenever a new database is available) +* **US.kml:** every few years (whenever an updated country boundary is available) +* **CA.kml:** every few years (whenever an updated country boundary is available) +* **GB.kml:** every few years (whenever an updated country boundary is available) +* **BRA.kml:** every few years (whenever an updated country boundary is available) +* **itudata:** these haven't been updated for a long time but can be updated if new maps are generated. +* **winnforum databases:** might be as early as every 6 months (at the discretion of WinnForum) + +### **Database update procedure** +* **FS_Database:** FS Script Parser automatically updates this daily (see next section) + +* **proc_lidar_2019:** download lidar database and post-process + +* **globe:** download database. Convert to WGS84 using open-afc/tools/geo_converters/to_wgs84.py script. +* **srtm3arcsecondv003:** download database. Convert to WGS84 using open-afc/tools/geo_converters/to_wgs84.py script. +* **srtm1arcsecond:** download database. Convert to WGS84 using open-afc/tools/geo_converters/to_wgs84.py script. +* **3dep:** download database. Convert to WGS84 using open-afc/tools/geo_converters/to_wgs84.py script. +* **cdsm:** download database. Follow the procedures on open-afc/tools/geo_converters/Canada CDSM surface model +* **nlcd:** download database, run proper gdal utilties to orient the tiles matching 3DEP 1-arcsec tiles and put in the proper directory +* **clc:** download database, run proper gdal scripts (open-afc/tools/geo_converters) to convert the data categorization mapping and coordinate system and put in the proper directory +* **2020canadalandocover:** download database, run proper gdal scripts (open-afc/tools/geo_converters) to convert the data categorization mapping and coordinate system and put in the proper directory +* **population:** download database and put in the proper directory + + +## **Database Processing/Format** +### **Processing done (in any) on the original database to convert it to a format usable by afc-engine** +* **FS_Database:** generated by the FS Script Parser. +* **LiDAR_Database:** generated from significant post-processing: + For each city: + * (1) Identify bare earth and building files available. + * (2) Identify pairs of files where a pair consists of a bare earth and building polygon file that cover the same region. + * (3) Convert bare earth into tif raster file, and convert building polygon file into tif raster file where both files are on same lon/lat grid. + * (4) Combine bare earth and building tif files into a single tif file with bare earth on Band 1 and building height on band 2. Both Band 1 and Band 2 values are AMSL. + * (5) Under target directory create directory for each city, under each city create dir structure containing combined tif files. + * (6) For each city create info file listing tif files and min/max lon/lat for each file. +* **srtm3arcsecondv003:** the two SRTM tiles over Manhattan are removed since they erroneously contain building height +* **country.KML:** there is some post-processing done that is not documented here as we are moving to using a different processing. + +* **Near Field Adjustment Factor File:** "nfa_table_data.csv" is created as follows. + +1. Download "WINNF-TS-1014-V1.2.0-App01 6GHz Functional Requirements - Appendix A.xlsx" from [https://6ghz.wirelessinnovation.org/baseline-standards](https://6ghz.wirelessinnovation.org/baseline-standards) + + +2. Note that this .xlsx file has 18 tabs labeled "xdB = 0 dB", "xdB = -1 dB", "xdB = -2 dB", ..., "xdB = -17 dB". Also note that in each of these 18 tabs, there is data for efficiency values ($\eta$) ranging from 0.4 to 0.7 in steps of 0.05. Further note the following: + - For each xdB and efficiency, there is a two column dataset with columns labeled u and dB + - The dB value is the adjustment factor. + - For each of these 2 column datasets, the last value of adjustment factor is 0 + - For each of these 2 column datasets, u begins at 0 and increases monotonically to a max value for which adjustment value is 0. + - For each xdB value, the max value of u for efficiency = 0.4 is >= the max value of u for any of the other efficiency values shown. + +3. Interpolate the data. For each 2 column dataset, use linear interpolation to compute the adjustment factor for 0.05 increments in u. + +4. Pad the data. For each 2 column dataset, append values of u in 0.05 increments up to the max u value for efficiency = 0.4. For these appended values append 0 for the adjustment factor value. + +5. Assemble all this data into a single 3-column CSV file named as nfa_table_data.csv. The file header is "xdB,u,efficiency,NFA". Subsequent data lines list values for xdB, u, efficiency, and NFA for each of the interpolated/padded 2-column datasets. + +* **Near Field Adjustment Factor for Passive Repeaters File:** "WINNF-TS-1014-V1.2.0-App02.csv" is created as follows. + +1. Download "WINNF-TS-1014-V1.2.0-App02 6GHz Functional Requirements - Appendix B.xlsx" from [https://6ghz.wirelessinnovation.org/baseline-standards](https://6ghz.wirelessinnovation.org/baseline-standards) + +2. In the "Data" tab of this .xlsx file, note that this tab contains tabular gain data where each column corresponds to a different Q and each row corresponds to a different value of 1/KS. + +3. The algorithm implemented in afc-engine only uses this table for values of KS > 0.4. This means 1/KS <= 2.5. The values of 1/KS in the .xlsx file go up to 7.5. For the purpose of interpolation, keep the first row in the file with 1/KS > 2.5 (2.512241), and delete all rows after this row with larger values of 1/KS. + +4. Count then number of Q values in the table (NQ). + +5. Count the number of 1/KS values in the file (NK). + +6. Replace the upper left cell in the table, that contains "1/KS" with "NQ:NK" where NQ and NK are the count values from steps 3 and 4 above. + +7. Save the Data tab in .csv format. Save the file named as WINNF-TS-1014-V1.2.0-App02.csv. + +### **Scripts to be used and procedure to invoke these scripts** +##### FS_Database: +FS Script Parser. The AFC Administrator can run the parser manually or set the time for the daily update. The parser fetches the raw daily and weekly ULS data from the FCC website. + +##### NLCD creation: +###### Step 1. +Ensure that gdal utilities are installed on your machine(currently gdal ver 3.3.3 used): +``` +dnf install gdal +``` + +###### Step 2. +Get extents of the original file by executing command below: +``` +gdalinfo -norat -nomd -noct nlcd_2019_land_cover_l48_20210604.img +``` +###### Corner Coordinates: +``` +Upper Left (-2493045.000, 3310005.000) (130d13'58.18"W, 48d42'26.63"N) +Lower Left (-2493045.000, 177285.000) (119d47' 9.98"W, 21d44'32.31"N) +Upper Right ( 2342655.000, 3310005.000) ( 63d40'19.89"W, 49d10'37.43"N) +Lower Right ( 2342655.000, 177285.000) ( 73d35'40.55"W, 22d 4'36.23"N) +Center ( -75195.000, 1743645.000) ( 96d52'22.83"W, 38d43' 4.71"N) +``` +###### Step 3. +Define the minimum/maximum Latitude and Longitude coordinates that contain the entire region covered by the file. In order to line up with 3DEP database, we want to make sure that each of these values are integer multiple of 1-arcsec. From the above extents, we see that the min longitude is 130d13'58.18"W. This can be rounded down to integer multiple of 1-arcsec as -(130 + 15/60). Similarly, maximum values are rounded up to integer multiple of 1-arcsec. Finally, the resolution is defined as 1-arcsec which equals 1/3600 degrees. Below commands can be typed directly into a bash shell. +``` +minLon=`bc <<< 'scale = 6; -(130 + 15/60)'` +maxLon=`bc <<< 'scale = 6; -(63 + 37.5/60)'` +minLat=`bc <<< 'scale = 6; (21 + 41.25/60)'` +maxLat=`bc <<< 'scale = 6; (49 + 11.25/60)'` +lonlatRes=`bc <<< 'scale = 20; (1/3600)'` + +echo minLon = $minLon +echo maxLon = $maxLon +echo minLat = $minLat +echo maxLat = $maxLat +``` +###### Step 4. +Define the input and output files for the conversion using commands below: +fin=nlcd_2019_land_cover_l48_20210604.img +fout=nlcd_2019_land_cover_l48_20210604_resample.tif + +###### Step 5. +Use gdal utility gdalwarp to convert the file to desired output +``` +gdalwarp -t_srs '+proj=longlat +datum=WGS84' -tr $lonlatRes $lonlatRes -te $minLon $minLat $maxLon $maxLat $fin $fout +``` + +###### Step 6: +Combine 900+ federated .int files into a single gdal .vrt file. +``` +gdalbuildvrt federated_nlcd.vrt output/*.int +``` + +###### Step 7: +Define the input and output files for the Federated file conversion +``` +fin=federated_nlcd.vrt +fout=federated_nlcd.tif +``` + +###### Step 8: +Run gdal utility gdalwarp to convert the federated file using the exact same file extents as for the nlcd_2019_land_cover_l48_20210604_resample.tif file: +``` +gdalwarp -te $minLon $minLat $maxLon $maxLat $fin $fout +``` + + +## **Database Usage ** + +### **Expected location of post-processed database on the AFC server** +There are three category of databases: Dynamic, Static and ULS. +1. **Dynamic:** +* These are assets that are subject to change more frequenty than Static, either by the user interacting with the GUI (AFC Config) uploading files (AntennaPatterns), or another asset that may change in the future +* These are stored via the Object Storage component by default + +2. **Static:** +* These are the assets that are not expected to change for at least a year (and some for many years) +* Appear in the containers under /mnt/nfs/rat_transfer +* Examples are: Terrain (3DEP, SRTM, Globe), Building (LiDAR, Multiband-BDesign3D), NLCD, Population Density +* Below are the database directories under /mnt/nfs/rat_transfer + * **ULS_Database:** Fallback (static) ULS_Database in case an active ULS_Database under fbrat is missing + * **srtm3arcsecondv003** + * **RAS_Database** + * **proc_lidar_2019** + * **population** + * **Multiband-BDesign3D** + * **globe** + * **3dep** + * **nlcd** + * **itudata** + +3. **ULS (note: WIP):** +* These are the supporting files for the ULS Script Parser that download, process, and create the new ULS files +* Live under /mnt/nfs/rat_transfer/daily_uls_parse/data_files. + * **WIP:** Functionality built into API + * Data for yesterdaysDB (used to retain FSID from day to day) and highest known FSID (to avoid collision, FSIDs are not reused currently) are stored here. + +## Mappings for use in OpenAFC +OpenAFC containers needs several mappings to work properly. Assuming that you are using /var/databases on your host to store the databases, you can select either option 1 here (which is assumed in the docker compose shown in the main README) or set mappings individually as shown in 2-6. + +1) All databases in one folder - map to /mnt/nfs/rat_transfer + ``` + /var/databases:/mnt/nfs/rat_transfer + ``` + Those databases are: + - 3dep + - daily_uls_parse + - databases + - globe + - itudata + - nlcd + - population + - proc_gdal + - proc_lidar_2019 + - RAS_Database + - srtm3arcsecondv003 + - ULS_Database + - nfa + - pr + + +2) LiDAR Databases to /mnt/nfs/rat_transfer/proc_lidar_2019 + ``` + /var/databases/proc_lidar_2019:/mnt/nfs/rat_transfer/proc_lidar_2019 + ``` +3) RAS database to /mnt/nfs/rat_transfer/RAS_Database + ``` + /var/databases/RAS_Database:/mnt/nfs/rat_transfer/RAS_Database + ``` +4) Actual ULS Databases to /mnt/nfs/rat_transfer/ULS_Database + ``` + /var/databases/ULS_Database:/mnt/nfs/rat_transfer/ULS_Database + ``` +5) Folder with daily ULS Parse data /mnt/nfs/rat_transfer/daily_uls_parse + ``` + /var/databases/daily_uls_parse:/mnt/nfs/rat_transfer/daily_uls_parse + ``` +6) Folder with AFC Config data /mnt/nfs/afc_config (now can be moved to Object Storage by default) + ``` + /var/afc_config:/mnt/nfs/afc_config + ``` +**NB: All or almost all files and folders should be owned by user and group 1003 (currently - fbrat)** + +This can be applied via following command (mind the real location of these folders on your host system): + +``` +chown -R 1003:1003 /var/databases /var/afc_config +``` + + diff --git a/dispatcher/Dockerfile b/dispatcher/Dockerfile new file mode 100644 index 0000000..24ba929 --- /dev/null +++ b/dispatcher/Dockerfile @@ -0,0 +1,62 @@ +# +# Copyright (C) 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +FROM nginx:1.24.0-alpine +ENV PYTHONUNBUFFERED=1 +RUN apk add --update --no-cache python3 \ + && ln -sf python3 /usr/bin/python \ + && python3 -m ensurepip +RUN pip3 install --no-cache --upgrade \ + --root-user-action=ignore pip setuptools +# +# Install reqiored python external and internal packages +# +WORKDIR /wd +# copy list of external packages +COPY dispatcher/requirements.txt /wd +# copy internal packages +COPY src/afc-packages ./afc-packages +# install +RUN set -x \ + && pip3 install --no-cache --root-user-action=ignore \ + -r /wd/requirements.txt \ + && pip3 install --use-pep517 --root-user-action=ignore \ + -r /wd/afc-packages/pkgs.dispatcher \ + && rm -rf /wd/afc-packages \ + && pip3 uninstall -y setuptools pip \ +# create certificate directories + && mkdir -p \ + /certificates/servers \ + /etc/nginx/templates + +# Server side certificates +COPY dispatcher/certs/servers/server.cert.pem \ +dispatcher/certs/servers/server.key.pem /certificates/servers/ +# Default client side CA certificates as a placeholder +COPY dispatcher/certs/clients/client.bundle.pem \ +/etc/nginx/certs/ + +# Copy nginx configuration files +COPY dispatcher/nginx.conf /etc/nginx/ +COPY dispatcher/nginx.conf.template /etc/nginx/templates/ + +COPY dispatcher/acceptor.py /wd/ + +ENV AFC_MSGHND_NAME=${AFC_MSGHND_NAME:-msghnd} +ENV AFC_MSGHND_PORT=${AFC_MSGHND_PORT:-8000} +ENV AFC_WEBUI_NAME=${AFC_WEBUI_NAME:-rat_server} +ENV AFC_WEBUI_PORT=${AFC_WEBUI_PORT:-80} +ENV AFC_ENFORCE_HTTPS=${AFC_ENFORCE_HTTPS:-TRUE} +ENV AFC_SERVER_NAME=${AFC_SERVER_NAME:-"_"} +ENV AFC_ENFORCE_MTLS=${AFC_ENFORCE_MTLS:-false} +ENV AFC_PROXY_CONN_TOUT=${AFC_PROXY_CONN_TOUT:-720} +# +ENV AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +COPY dispatcher/entrypoint.sh / +RUN chmod +x /entrypoint.sh +CMD ["/entrypoint.sh"] +HEALTHCHECK --start-period=60s --interval=30s --retries=1 \ + CMD curl --fail http://localhost/fbrat/ap-afc/healthy || exit 1 diff --git a/dispatcher/acceptor.py b/dispatcher/acceptor.py new file mode 100755 index 0000000..390990f --- /dev/null +++ b/dispatcher/acceptor.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +""" +Description + +The acceptor client (aka consumer) registeres own queue within broker +application (aka rabbitmq). Such queue used as a channel for control commands. +""" + +from appcfg import BrokerConfigurator, ObjstConfig +import os +import sys +from sys import stdout +import logging +from logging.config import dictConfig +import argparse +import inspect +import gevent +import subprocess +import shutil +from ncli import MsgAcceptor +from hchecks import MsghndHealthcheck, ObjstHealthcheck +from fst import DataIf + +dictConfig({ + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'standard': { + 'format': '%(asctime)s - [%(levelname)s] %(name)s [%(module)s.%(funcName)s:%(lineno)d]: %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S', + } + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'standard', + } + }, + 'root': { + 'level': 'INFO', + 'handlers': ['console'] + }, +}) +app_log = logging.getLogger() + + +class Configurator(dict): + __instance = None + + def __new__(cls): + if cls.__instance is None: + cls.__instance = dict.__new__(cls) + return cls.__instance + + def __init__(self): + dict.__init__(self) + self.update(BrokerConfigurator().__dict__.items()) + self.update(ObjstConfig().__dict__.items()) + self['OBJST_CERT_CLI_BUNDLE'] = \ + 'certificate/client.bundle.pem' + self['DISPAT_CERT_CLI_BUNDLE'] = \ + '/etc/nginx/certs/client.bundle.pem' + self['DISPAT_CERT_CLI_BUNDLE_DFLT'] = \ + '/etc/nginx/certs/client.bundle.pem_dflt' + + +log_level_map = { + 'debug': logging.DEBUG, # 10 + 'info': logging.INFO, # 20 + 'warn': logging.WARNING, # 30 + 'err': logging.ERROR, # 40 + 'crit': logging.CRITICAL, # 50 +} + + +def set_log_level(opt) -> int: + app_log.info(f"({os.getpid()}) {inspect.stack()[0][3]}() " + f"{app_log.getEffectiveLevel()}") + app_log.setLevel(log_level_map[opt]) + return log_level_map[opt] + + +def readiness_check(cfg): + """Provide readiness check by calling for response preconfigured + list of subjects (containers) + """ + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + objst_chk = ObjstHealthcheck(cfg) + msghnd_chk = MsghndHealthcheck.from_hcheck_if() + checks = [gevent.spawn(objst_chk.healthcheck), + gevent.spawn(msghnd_chk.healthcheck)] + gevent.joinall(checks) + for i in checks: + if i.value != 0: + return i.value + return 0 + + +def run_restart(cfg): + """Get messages""" + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + with DataIf().open(cfg['OBJST_CERT_CLI_BUNDLE']) as hfile: + if hfile.head(): + app_log.debug(f"Found cert bundle file.") + with open(cfg['DISPAT_CERT_CLI_BUNDLE'], 'w') as ofile: + ofile.write(hfile.read().decode('utf-8')) + app_log.info(f"{os.path.getctime(cfg['DISPAT_CERT_CLI_BUNDLE'])}, " + f"{os.path.getsize(cfg['DISPAT_CERT_CLI_BUNDLE'])}") + else: + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + # use default certificate (placeholder) + # in any case of missing file, no more certificates included + app_log.info(f"Misssing certificate file " + f"{cfg['OBJST_CERT_CLI_BUNDLE']}, back to default " + f"{cfg['DISPAT_CERT_CLI_BUNDLE_DFLT']}") + shutil.copy2(cfg['DISPAT_CERT_CLI_BUNDLE_DFLT'], + cfg['DISPAT_CERT_CLI_BUNDLE']) + p = subprocess.Popen("nginx -s reload", + stdout=subprocess.PIPE, shell=True) + app_log.info(f"{p.communicate()}") + + +def run_remove(cfg): + """Get messages""" + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}() " + f"{cfg['DISPAT_CERT_CLI_BUNDLE']}") + os.unlink(cfg['DISPAT_CERT_CLI_BUNDLE']) + # restore builtin certifiicate from backup + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}() " + f"restore default certificate " + f"{cfg['DISPAT_CERT_CLI_BUNDLE_DFLT']}") + shutil.copy2(cfg['DISPAT_CERT_CLI_BUNDLE_DFLT'], + cfg['DISPAT_CERT_CLI_BUNDLE']) + p = subprocess.Popen("nginx -s reload", + stdout=subprocess.PIPE, shell=True) + app_log.info(f"{p.communicate()}") + + +commands_map = { + 'cmd_restart': run_restart, + 'cmd_remove': run_remove, +} + + +def get_commands(cfg, msg): + """Get messages""" + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + commands_map[msg](cfg) + + +def run_it(cfg): + """Execute command line run command""" + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + + # backup builtin certifiicate as a default one + shutil.copy2(cfg['DISPAT_CERT_CLI_BUNDLE'], + cfg['DISPAT_CERT_CLI_BUNDLE_DFLT']) + # check if lucky to find new certificate bundle already + run_restart(cfg) + + maker = MsgAcceptor(cfg['BROKER_URL'], cfg['BROKER_EXCH_DISPAT'], + msg_handler=get_commands, handler_params=cfg) + app_log.info(f"({os.getpid()}) Connected to {cfg['BROKER_URL']}") + maker.run() + + +# available commands to execute in alphabetical order +execution_map = { + 'run': run_it, + 'check': readiness_check, +} + + +def make_arg_parser(): + """Define command line options""" + args_parser = argparse.ArgumentParser( + epilog=__doc__.strip(), + formatter_class=argparse.RawTextHelpFormatter) + args_parser.add_argument('--log', type=set_log_level, + default='info', dest='log_level', + help=" - set " + "logging level (default=info).\n") + args_parser.add_argument('--cmd', choices=execution_map.keys(), + nargs='?', + help="run - start accepting commands.\n" + "check - run readiness check.\n") + + return args_parser + + +def prepare_args(parser, cfg): + """Prepare required parameters""" + app_log.debug(f"{inspect.stack()[0][3]}() {parser.parse_args()}") + cfg.update(vars(parser.parse_args())) + + +def main(): + """Main function of the utility""" + res = 0 + parser = make_arg_parser() + config = Configurator() + + if prepare_args(parser, config) == 1: + # error in preparing arguments + res = 1 + else: + if isinstance(config['cmd'], type(None)): + parser.print_help() + + if res == 0: + app_log.debug(f"{inspect.stack()[0][3]}() {config}") + res = execution_map[config['cmd']](config) + sys.exit(res) + + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + sys.exit(1) + + +# Local Variables: +# mode: Python +# indent-tabs-mode: nil +# python-indent: 4 +# End: +# +# vim: sw=4:et:tw=80:cc=+1 diff --git a/dispatcher/certs/clients/client.bundle.pem b/dispatcher/certs/clients/client.bundle.pem new file mode 100644 index 0000000..8dfe171 --- /dev/null +++ b/dispatcher/certs/clients/client.bundle.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFhzCCA2+gAwIBAgIUY5uGP2XsWuXYbKzpAgKjjBRQJU0wDQYJKoZIhvcNAQEL +BQAwWjELMAkGA1UEBhMCSUwxDzANBgNVBAgMBklzcmFlbDERMA8GA1UEBwwIVGVs +IEF2aXYxETAPBgNVBAoMCEJyb2FkY29tMRQwEgYDVQQDDAtBRkMgVGVzdGluZzAe +Fw0yMzA1MzEwOTUzMjFaFw0zMzA1MjgwOTUzMjFaMFoxCzAJBgNVBAYTAklMMQ8w +DQYDVQQIDAZJc3JhZWwxETAPBgNVBAcMCFRlbCBBdml2MREwDwYDVQQKDAhCcm9h +ZGNvbTEUMBIGA1UEAwwLQUZDIFRlc3RpbmcwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCegQz88G0AH1xQuRMaITEltVinWHEnxODjE9+gaT4qoCfz7keJ +qq0ZRyu1y8oWlV8AGU1w/eWR7MXc+qI1+BzRgecJCStDr/NJhgrlPgGMj8TBO3AM +8M9TJk+1/pBAVZwwOIw8eWBNQVe4Ws+xhh88V8j/mG0beFQMwj5qzlQnmhkqWHIP +0btmZ2KxRUFIzVS7daf8dOk9fKAqFFtmDIYPnq7vAsVYSl+xQVcTsn8wDcE7Nv9O +4Ctd/xy7Jo51zJKC93kspiobP3ca/CNCVMpq5FpVRKopMe/4Zb39t+owL5L2O20j +++TE1CeugoGImR1VhIjnI98Qo9r3M2SkaYD+R5A0oFfiJOB9MWJb2JYKXcSbDE+S +EuqiXB5J4rKINAG6EeXcLhtztQ+StN275mOKnaFqH6Sj/v15AUT2HFYNITTgEXkX +LirR2tKhmiXSY3j9LH7EM8M6COeGjDensCw+zUbAUWWZb/AFRlWwzoq+fkvZ+BBO +yW57wOLV3TSHwz2H92vC6V5jjSdLhmUTO6cWhoJfl7A7/zUZAaudPexRstrYvVJQ +IYgt/qm5u3NmbZMCuCMdTWByK79tekUgS579+SjHh4sCCefQURCT8pFwthovhgEZ +6A3orWhgfHiXjSsZ9DZSydKMWFt86OlO4Kq3p7jdZuuFgh+PmzP4cP6yXQIDAQAB +o0UwQzASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBRjAdBgNVHQ4E +FgQURn6ApnN3kwblVs7owTr3PQqfBnwwDQYJKoZIhvcNAQELBQADggIBAIpDebfL +VO1CgXMvmEnMv+96pQd2GfWQj6+G/FFZm9l0dN3Hek328+4zKfxRLSHW1PF3XEPK +WbUvukZYp+748L8X//TMXYH5xEQR3igWyjEDVpUXSJEqKkktgJ3SUeQjvq20nVA8 +jiVQ8UjMGCsIiWFmz45kH3CkaN4CX/HYnEk2XGzob8sGc7nss0oLGVEKtcN4lFOb +tD1R4taiYuoz71sCuOVx5m7c0ja/D0/FhXleZ3CR8qXPKTr6FiYPbwvlKAAg2pC2 +ZtOb6UQ8rwad17HTCIv3/mEfRANVcod8GzZaiJJo7XJAwbdcB7xkJ6rRWe2PPpcK +MCmvUErENGdDmSLULAHhbylGRDOa/BFQCBI7F+rRzrNdBbC2X5EQJP4HfMrjrR3X ++5RAk+eyambTRvetRP8TNbUkjRJshUx/DVeFnHsyA3jvLsVZmZQF8ynFEEMsK1Ba +wnBbBXoeZlK6bo8R/YSRhzewv+XegS62vDGb6rUe7aj6BRUR8BTnc3PF+opyUlQz ++WfHJyFaAljE675GV0xBo3dAAMcF+0IESAcd68UHhVfebAPfLQ0D/9ksVXzm5A6J +51tt1dMntf4YAl+qGkAPJ5WaZmYPILrfwtuA3jA4LmrQD23wUlQqOyFYA3n/s9wo +N1ek3w2xwY3/v24M2si/8OSEWpgtZzr5iw7q +-----END CERTIFICATE----- diff --git a/dispatcher/certs/clients/test_ca_crt.pem b/dispatcher/certs/clients/test_ca_crt.pem new file mode 100644 index 0000000..8dfe171 --- /dev/null +++ b/dispatcher/certs/clients/test_ca_crt.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFhzCCA2+gAwIBAgIUY5uGP2XsWuXYbKzpAgKjjBRQJU0wDQYJKoZIhvcNAQEL +BQAwWjELMAkGA1UEBhMCSUwxDzANBgNVBAgMBklzcmFlbDERMA8GA1UEBwwIVGVs +IEF2aXYxETAPBgNVBAoMCEJyb2FkY29tMRQwEgYDVQQDDAtBRkMgVGVzdGluZzAe +Fw0yMzA1MzEwOTUzMjFaFw0zMzA1MjgwOTUzMjFaMFoxCzAJBgNVBAYTAklMMQ8w +DQYDVQQIDAZJc3JhZWwxETAPBgNVBAcMCFRlbCBBdml2MREwDwYDVQQKDAhCcm9h +ZGNvbTEUMBIGA1UEAwwLQUZDIFRlc3RpbmcwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCegQz88G0AH1xQuRMaITEltVinWHEnxODjE9+gaT4qoCfz7keJ +qq0ZRyu1y8oWlV8AGU1w/eWR7MXc+qI1+BzRgecJCStDr/NJhgrlPgGMj8TBO3AM +8M9TJk+1/pBAVZwwOIw8eWBNQVe4Ws+xhh88V8j/mG0beFQMwj5qzlQnmhkqWHIP +0btmZ2KxRUFIzVS7daf8dOk9fKAqFFtmDIYPnq7vAsVYSl+xQVcTsn8wDcE7Nv9O +4Ctd/xy7Jo51zJKC93kspiobP3ca/CNCVMpq5FpVRKopMe/4Zb39t+owL5L2O20j +++TE1CeugoGImR1VhIjnI98Qo9r3M2SkaYD+R5A0oFfiJOB9MWJb2JYKXcSbDE+S +EuqiXB5J4rKINAG6EeXcLhtztQ+StN275mOKnaFqH6Sj/v15AUT2HFYNITTgEXkX +LirR2tKhmiXSY3j9LH7EM8M6COeGjDensCw+zUbAUWWZb/AFRlWwzoq+fkvZ+BBO +yW57wOLV3TSHwz2H92vC6V5jjSdLhmUTO6cWhoJfl7A7/zUZAaudPexRstrYvVJQ +IYgt/qm5u3NmbZMCuCMdTWByK79tekUgS579+SjHh4sCCefQURCT8pFwthovhgEZ +6A3orWhgfHiXjSsZ9DZSydKMWFt86OlO4Kq3p7jdZuuFgh+PmzP4cP6yXQIDAQAB +o0UwQzASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBRjAdBgNVHQ4E +FgQURn6ApnN3kwblVs7owTr3PQqfBnwwDQYJKoZIhvcNAQELBQADggIBAIpDebfL +VO1CgXMvmEnMv+96pQd2GfWQj6+G/FFZm9l0dN3Hek328+4zKfxRLSHW1PF3XEPK +WbUvukZYp+748L8X//TMXYH5xEQR3igWyjEDVpUXSJEqKkktgJ3SUeQjvq20nVA8 +jiVQ8UjMGCsIiWFmz45kH3CkaN4CX/HYnEk2XGzob8sGc7nss0oLGVEKtcN4lFOb +tD1R4taiYuoz71sCuOVx5m7c0ja/D0/FhXleZ3CR8qXPKTr6FiYPbwvlKAAg2pC2 +ZtOb6UQ8rwad17HTCIv3/mEfRANVcod8GzZaiJJo7XJAwbdcB7xkJ6rRWe2PPpcK +MCmvUErENGdDmSLULAHhbylGRDOa/BFQCBI7F+rRzrNdBbC2X5EQJP4HfMrjrR3X ++5RAk+eyambTRvetRP8TNbUkjRJshUx/DVeFnHsyA3jvLsVZmZQF8ynFEEMsK1Ba +wnBbBXoeZlK6bo8R/YSRhzewv+XegS62vDGb6rUe7aj6BRUR8BTnc3PF+opyUlQz ++WfHJyFaAljE675GV0xBo3dAAMcF+0IESAcd68UHhVfebAPfLQ0D/9ksVXzm5A6J +51tt1dMntf4YAl+qGkAPJ5WaZmYPILrfwtuA3jA4LmrQD23wUlQqOyFYA3n/s9wo +N1ek3w2xwY3/v24M2si/8OSEWpgtZzr5iw7q +-----END CERTIFICATE----- diff --git a/dispatcher/certs/clients/test_ca_key.pem b/dispatcher/certs/clients/test_ca_key.pem new file mode 100644 index 0000000..ceb82c6 --- /dev/null +++ b/dispatcher/certs/clients/test_ca_key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAnoEM/PBtAB9cULkTGiExJbVYp1hxJ8Tg4xPfoGk+KqAn8+5H +iaqtGUcrtcvKFpVfABlNcP3lkezF3PqiNfgc0YHnCQkrQ6/zSYYK5T4BjI/EwTtw +DPDPUyZPtf6QQFWcMDiMPHlgTUFXuFrPsYYfPFfI/5htG3hUDMI+as5UJ5oZKlhy +D9G7ZmdisUVBSM1Uu3Wn/HTpPXygKhRbZgyGD56u7wLFWEpfsUFXE7J/MA3BOzb/ +TuArXf8cuyaOdcySgvd5LKYqGz93GvwjQlTKauRaVUSqKTHv+GW9/bfqMC+S9jtt +I/vkxNQnroKBiJkdVYSI5yPfEKPa9zNkpGmA/keQNKBX4iTgfTFiW9iWCl3EmwxP +khLqolweSeKyiDQBuhHl3C4bc7UPkrTdu+Zjip2hah+ko/79eQFE9hxWDSE04BF5 +Fy4q0drSoZol0mN4/Sx+xDPDOgjnhow3p7AsPs1GwFFlmW/wBUZVsM6Kvn5L2fgQ +Tslue8Di1d00h8M9h/drwuleY40nS4ZlEzunFoaCX5ewO/81GQGrnT3sUbLa2L1S +UCGILf6pubtzZm2TArgjHU1gciu/bXpFIEue/fkox4eLAgnn0FEQk/KRcLYaL4YB +GegN6K1oYHx4l40rGfQ2UsnSjFhbfOjpTuCqt6e43WbrhYIfj5sz+HD+sl0CAwEA +AQKCAgEAlduZdgOyaq070K5KmyfKmcZNHVsHEPyZSthdVAJs3kwxufUM+eG+aunZ +L7aPSK7M9QD3MN88D612nSjx+Giuhn50+xwKJaLTOC5dWsQ3HrhG5BLYK8P5oLnW +H1Gg/NJ0Kzsri3mOTTx8PTbOqx8NpTWyOcXZUmF/xdhYvw54jkCpjlm07bPzpCwX +KVc7FCPd+qaQvqWiZ5nOrDo299LbZSU4a42JG6Kluqb2Nw9KJRq8GMo0tFRJbENo +3KDljAZwdxaXIFmx8bUdXQwKIgqcnldr+LZE01H9ejJnYNxjtE8meVtSIvVjI50a +L2oAIi/xhgsajL5jhg4FVjbm7nM5mrMSp6Sf/YNCsvVB/LtyD3XsdtTgNbUPI9P3 +9ZI16X/4XlyMJsnVieQQ27gZiJmyz36r+cMF7yvZ2e14DHX+k2aRT7sNFapWGzuC +YheFmPbApya1ZZHpLlUC3H7HRcyBufRLbJ3aseFhw/Jq5Ha0rv5fNUIVDGudiPR9 +qbRYV2xodpElqtCYRynsiIqNSglRClw0iFQlwuS+LLFxU/wNyuGFI9jlDHWwgYd3 +iz43oOi2hG53N+QkAZAzuQeAcQlE5q0L/UW0+4tnblxrIU/euuOc6XEb2di5kTMo +0RJxJ5Vm8KMRwOMUKDRpg8VMug+lAU7/E8inbakKD7GI69Z1DcECggEBAMnuVLSI ++as1ucXXTY1izeqkskZ3Hs54fwzeEM0yoCt4l+RcMI6wPczr3gmMP/5hxglgiI6v +AD6FTKv/17zNJ6yPREFImD6ylSXGlCV6QagynQYim+AHWkHnds5iwmXRHBtbhNrl +nZdCMq5gvE7NDFGkHt2v8Ut6HVNeB8+9ZGUIsZuOQJbDZUK+zRR0Urx23h2KukNK +vRsdUleWLShPn5kXuYpipxnRtAbZWaX6rbb3S4OZGpqI2HYw3r2WoA7tMl03r7F4 +nlOhK9QudpZnGz7dYFM9vOglBym4A4KnTk45njfJ+Z9+ye8PqOdEsDDr94+N8Fd8 +/yMbt2zoX9gjkRkCggEBAMjx9T4zj4rUJOq7zu82MufmRVSz017eKpnxMkIIIi50 +qhPpZKrULaD3Tw/TaRpB5r/Rn+MDfwGvot7XyhsJkn9UBGxGzacC4LDLDFUU3mDz +xPzJMlYrHIKzVyLulVZbJFKfbkrZQnzRuowQEBECpRCThVI9/yyFiHyt1rqWBLeK +IBWHTuqq05PkuEpvY+XP3YboxR14mIEtxloa1gVcBC5ez2Kgk5evgW4DetdDXKLH +dLdN+Bugmi3pi8pRaALDEMWRlApieHenZ50te9pgUFEBPo92OzWW4t3W7zx95P7L +YcPVagbmGjaFOznfIB5eE3pGMwq1XyTmzgbxmL+W/+UCggEAKd0qXH9lW5R43kZj +wqMwU6wvdaJulZmvpWhIjXIoeLq4qtReEMcDGP/xeYNFJST6HKmRxhsL7upN2f7h +qDfUONc+7FXzklQNzdYckqSFBizwFvyH2mtL0Av/uowJB3jR3e4cXhFqmZhUz4go +oiGqoyZma8l1OhOoDseY0P3P2Y5y2/Ai/d6mmK7b75iqKn5uUCuZsCfHit6KWrQ8 +ynWvfdrIUaNgR18NdroK9vlAmIUud6r/M/iY/+/jzeRzbITKgz7vQtjh4i6w2n2D +gmz/3gmhVcCf8HT0xjZrK+QpvNf/MEvEX8e2b8SMXN4FtS7GlVF0+X5lms69OWv3 +quS8yQKCAQEAs8RyR/FMq0BavnzCBhMQu5Gv2LT/ldFff1TeR2CLY9APD70ux/0F +zQkye9AAKPNoAZcBWNAZ4WJFbOLT9hg8LRj20/8LoCvc44zjPYJr54RtaFB+bcQn +v229uIyEoTrsZdYnj2KKLqxaHU+JcA5TqV6wWQEJtcTIc/H5NfdbxO8XAOuJ2Dp3 +CcoGbOD2F/Q8FKNNJK5skLRozNdRPH4zZ2B+W5eYMo1aVxdZ4BZtW1rgudRQ8DZf +eE+FNbxaNo5YBMfWDuxFJZZoBZ9ZO+YKNE3Or/1vvuN7lkbgw9dE9ATzM4VLU1yr +erb6Yy2PyFsVRcBjjWLw+UxaaK/enRfzWQKCAQAeS1VQnrGGwzl59LndYkuNas5V +7DQvTu7kXFsNKgujaF+NE3Ks0zDLx6C2zbL2JGf2LbjF+baXUbKatVU8qpvplO3f +15uLPq6BEI/pCKc8JThyWaE/0pnVv2l92fjNvN2EzDGsX8uj9TYaFnkE0mV7YQPh +eliJAlT4ou2PIDrgwxqd6fiw/bkA9NSDy/tVdbAIz3p4gyf+4KvvNbUL9Q5sLtEO +LPIurhavsWrpZIOvVxVnTxOoIe4iVE+iSy4KBgqaBMqhULJZqQnDoHBoFDzE06e9 +jSKYCCCeYi6k4mkMHD3KvKjcLseiEmHE1w8EEEQgh5NmDnpCSMOaUEG65oqY +-----END RSA PRIVATE KEY----- diff --git a/dispatcher/certs/servers/server.bundle.pem b/dispatcher/certs/servers/server.bundle.pem new file mode 100644 index 0000000..429ab2c --- /dev/null +++ b/dispatcher/certs/servers/server.bundle.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFhzCCA2+gAwIBAgIUHZCstmrQ1m9wz/EjyUzmQxFpgQ0wDQYJKoZIhvcNAQEL +BQAwWjELMAkGA1UEBhMCSUwxDzANBgNVBAgMBklzcmFlbDERMA8GA1UEBwwIVGVs +IEF2aXYxETAPBgNVBAoMCEJyb2FkY29tMRQwEgYDVQQDDAtBRkMgVGVzdGluZzAe +Fw0yMzA1MjkxMjQyMjFaFw0zMzA1MjYxMjQyMjFaMFoxCzAJBgNVBAYTAklMMQ8w +DQYDVQQIDAZJc3JhZWwxETAPBgNVBAcMCFRlbCBBdml2MREwDwYDVQQKDAhCcm9h +ZGNvbTEUMBIGA1UEAwwLQUZDIFRlc3RpbmcwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCuvs3GQi4x0wzi6uN7MiuwMl0rboqMzIn7B6bsD3IBhIqKVAxs +D2n/j72EcezYZkG1dMNIZU1GWRdBl8dBhB2gdkrR6ODiTV4TPAgjTfmZ+J6IZvOY +IwQImTBmaBhIuyC/56lxMBFQxOkfmFtTRsOgtN+rWT5Dgibkc2pUup/V+i8tveFX +954+QhCnFPxSQlkE6l94zgKlB5kkPlW2hvMiZu34tgnqbBuu8Zhk0a/kdMjmmWNT +jZt4v8i8cZkgH+D5Qx6Ai8ndsIj9a7C80sOUZ68jRmDBnLh6bpX4Af7opnWux8Pc +32KXzzBls9cenFevv9Ue1z0M9YfYLixGeg1Detk5Qmh4y/KPEweMXw+9ZRckBpAw +BA5czH6PlF+jnX4tjpuiddfKsx4ALqHr4Qw5rH87cepa2ia/VTWBe7eCpxdqy1yj +IoZYjWsfn1VSpZkIXF3TzRZdJzq9ggZ9A4UjmYYr8O02DbdpFdsgryMcCCRVdgJ2 +qsithswC7V2A7fHGOoA+Xr3/gnmBdqC2Zwc5nq8DHqVX6xl5aFCdbNtPsUtVq8CO +wwQYh4EdqBMYos6xhRKoG/pil1e/FNNsnPg7ibZW1XWdUbUo21B33UpI41gRBPtc +xxd1XTCx+jspXPnBrfgvJswBMPVzF+8RsZQy7VIRhUW1vYmvPlxxzXQLXwIDAQAB +o0UwQzASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBRjAdBgNVHQ4E +FgQUmqnURyaeNvKe5/Z5hEdHx6Rk6bMwDQYJKoZIhvcNAQELBQADggIBABQdPhYf +otoudC/BYOlBXYmxDCpLYyKp5JBnP6+Uc4gNlndmHMy7Q5qPfRQNfUs6USbxpUyD +e725AtBjkfa65fZ00C9bnzbXiiew5dKJz9HrfoRXZ4wS+X89eTsbw4LYGG/QgJRa +aFDRt5BEM9Jh/k2AWeAOaRHz1dcQRyo2n8ZFNHdltey9z5AStGccwBSx3kSgefB/ +8+zxJ5Z2+C4I/6DevPD+2i0vCA7wPvq1xUNXNLvriZihjbe+BfW9yWNYt04jKjAl +r7xUyA4s+EgrAsre2Xn+8BfItFN4BiRgfQ4k1Uhz4gHfFdOkV07voFKFAV1fHjRO +1u+LXQjuU9cNv+6cAO5KNAA5tuBQNBivf3vqiVvc5bb2+7ZOwLgj8/P0I+MkgwC7 +CNJCwDFFwfsUlABT+6jfcivdrw6LGd1yewI/zLZ6o//ZXG1aky2Fr2fTbCRgEONY +fuXDCP4tTY/n227IZ7ZTpwmICJsldmOCpc+zucczZEs7nOpbZ4DrDimRYmm7Ffgs +7TPsypkg9ATcOp7LAlKXn1oIPZwRPxiuaDCSPI+9h7j8U0bhk7oDE2IlmIyTZ/Za +vUTQoKMXONpfs12PupmxZz622FRRgKLGtS1M7vEV7dWzAG64Fd37MwN2Mh5zfUBY +8M2AJ6MhJSLffEWNT2h1EOHDRgYbwbhjkCKU +-----END CERTIFICATE----- diff --git a/dispatcher/certs/servers/server.cert.pem b/dispatcher/certs/servers/server.cert.pem new file mode 100644 index 0000000..6b0ccc8 --- /dev/null +++ b/dispatcher/certs/servers/server.cert.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF6DCCA9CgAwIBAgIUTldfidz38+Lnukf4EC6xQ4AvLQYwDQYJKoZIhvcNAQEL +BQAwWjELMAkGA1UEBhMCSUwxDzANBgNVBAgMBklzcmFlbDERMA8GA1UEBwwIVGVs +IEF2aXYxETAPBgNVBAoMCEJyb2FkY29tMRQwEgYDVQQDDAtBRkMgVGVzdGluZzAe +Fw0yMzA1MzEwODQ1NTZaFw0yNTA2MTkwODQ1NTZaMFcxCzAJBgNVBAYTAklMMQ8w +DQYDVQQIDAZJc3JhZWwxETAPBgNVBAcMCFRlbCBBdml2MREwDwYDVQQKDAhCcm9h +ZGNvbTERMA8GA1UEAwwIYmV0YV9zZXIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDg4U+tOhn4ZgSyo826i/bpbXzXnKI7gQgmaPygF8bA/mhuXeNBHHbU +sTJUVNjvPFINyEeE23mGGPZ1i2JQTXsuSTYj1MLOC1ScKR/hE8+L9TxOoJPtfY1d +hoXzgofjJI6aR9cmr6HT8Hg8gJDoSRMrLLTTEWRh36d/KgQlrPYQmtHKdstvyJop +5uPIy/3mfEDVy0EoXsq3spLMTLxdD8gUrBuYT56FS0q9XwNCI/+vGn9RYMOZbzA9 +s8f8vES3AFwxuBAu1H+zoyPgFa+dGeHWoW9feu/LpSQOtK5hjlqeHWGNdHNdcc6s +GQEVdGHqt+BGj6nd7jUCNqbNet3jWjKQCn0WnmxdnM/2gR+djhvo7B9puwHphsSx +8r8VghfHyNL3RytYDezONUunFG/mU2P7RzFTc2PcqxtAfz4K4REIRQwqvW4nBv2K +40Thgcc2SBPCoihoNdHUxlZPO5lwJdv2k07ztc35ujUMtBiqmQWQnoVkpd9OXYHV +rI6F7TK8DP1MP1w+L7QNuzyFEtxPl2YjWNqeuWf33L74/IEL0NWhHW1yIJE9+0L3 +1LLrhKnbk0+C0pNFaLP5UDeqDQejvAKn5NVMhQL3i4rtdn5lHnpSd803EiBTs6ZW +TRG351AY+tWonY4eNMgfKLoA4tc9e9iENBJI/efkytLp3K6zNdISDQIDAQABo4Go +MIGlMB8GA1UdIwQYMBaAFJqp1Ecmnjbynuf2eYRHR8ekZOmzMAwGA1UdEwEB/wQC +MAAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDgYDVR0PAQH/BAQDAgWgMDAGA1UdEQQp +MCeCH3dlYi1paGwtd2NjLTAxLmlobC5icm9hZGNvbS5uZXSHBAq7gCgwHQYDVR0O +BBYEFMh+ORROXNcfoSAuTn92epuRvtNfMA0GCSqGSIb3DQEBCwUAA4ICAQClwzvh +gNyPG2lisJ/EooDDX22gxUInb7cmi8gKX+pNrT4UJqwgo7olgWVfJwyBnI+u7fs1 +dXOyu4zaoSmnh2TJLtLukWhPcxFgVdfo5D0CdmV9c02qgWt+TXbApATK7F7857P6 +sdPTbqbMAtn4Z48uGt+rluOyTO3CJ9ZzIJHuegzZEFjX24HtXXVdRLMRA1Cauxhb +2b+ty+k7JB5jIwJ9t+PZzb2ktKy06yGqjYyazy3RpVAxWi8aAJuBQGxHmy0ZBNLx +0JaqDj+D+zc+U0jezhlm3II+o0cq7THCKhZPGbZUIszTN9CFtByKoIzO4jBdnYkw +0d+Kws/A6dfPv8janfxTUlS50P1+/5OeZCMc7g83KMzWzIBjye16FMENJhPxhuDD +y1ylCTnEC5YZMCfikBo9McVft6MN/z60sQgFF2TNYqEFVYpr3Z/qw8EoBmKbl/8i +HU9Ac8GdsQFTmrFaFtlSSh/Cfq41iVlLTKjr54YJ2QvjLN+XD6geTBWTfkYIryv/ +9IkOcg3bLsfXp9LD5RVe0t4FdgfutYYOzNI0FMa5Q6H2C0yX+6NW0pYQT4Yny/pT +xl7rTSy9qSd1ChkxGNHzzwrFQaPA1E+Aq4Df5J1p+sVaL17vsEV7ClIWSJXMbIP5 +auYOE6NvyXIli3UoafQ0TIzUfB9ab+coVN/Txw== +-----END CERTIFICATE----- diff --git a/dispatcher/certs/servers/server.key.pem b/dispatcher/certs/servers/server.key.pem new file mode 100644 index 0000000..c710874 --- /dev/null +++ b/dispatcher/certs/servers/server.key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKgIBAAKCAgEA4OFPrToZ+GYEsqPNuov26W1815yiO4EIJmj8oBfGwP5obl3j +QRx21LEyVFTY7zxSDchHhNt5hhj2dYtiUE17Lkk2I9TCzgtUnCkf4RPPi/U8TqCT +7X2NXYaF84KH4ySOmkfXJq+h0/B4PICQ6EkTKyy00xFkYd+nfyoEJaz2EJrRynbL +b8iaKebjyMv95nxA1ctBKF7Kt7KSzEy8XQ/IFKwbmE+ehUtKvV8DQiP/rxp/UWDD +mW8wPbPH/LxEtwBcMbgQLtR/s6Mj4BWvnRnh1qFvX3rvy6UkDrSuYY5anh1hjXRz +XXHOrBkBFXRh6rfgRo+p3e41AjamzXrd41oykAp9Fp5sXZzP9oEfnY4b6OwfabsB +6YbEsfK/FYIXx8jS90crWA3szjVLpxRv5lNj+0cxU3Nj3KsbQH8+CuERCEUMKr1u +Jwb9iuNE4YHHNkgTwqIoaDXR1MZWTzuZcCXb9pNO87XN+bo1DLQYqpkFkJ6FZKXf +Tl2B1ayOhe0yvAz9TD9cPi+0Dbs8hRLcT5dmI1janrln99y++PyBC9DVoR1tciCR +PftC99Sy64Sp25NPgtKTRWiz+VA3qg0Ho7wCp+TVTIUC94uK7XZ+ZR56UnfNNxIg +U7OmVk0Rt+dQGPrVqJ2OHjTIHyi6AOLXPXvYhDQSSP3n5MrS6dyuszXSEg0CAwEA +AQKCAgEAhgZiqThOkBelRx6PF1Yhoz9ov0wP+GzPgazimztwbkdx0A1Oyth/DgZJ +m68x12tY7/PkhA8WH1CzWpzmzDtRZeWmSbadH5XrKGLuKAPZl21iMu5LG6jPXuU0 +4ktyV3LLNrIITXsxdJIF5vEs6/PZY8ryPjVIYXidaBGPhTDPOlg7HnKsjoO9Nanx +KhRBz2NQdNr9i2TrZo4cJXy6arBkK8XjcGRLct/LvI9q7rlrwl2Fcee8y65TzwJd +94fxYCvrxooPwwlMzrA1SnFCR9xMF9IBAaPQVMuocMdIgsYHxeJ26Ip100Rny3Pf +jHzferd6CDPJJoa4uwf9Y8uNgNmZ9dbqiJR+tgdR8WuG2Bn3NzOOeN8tipPzDYuf +2jHO117IsgEPugbW0IQcpee3gZf/7iqaJVIIT6c0Bq2tSYcpNSRCYdOx9rR5KVH7 +Qv2KWKl/rHHVw38jX9HxmwFjZhF6Lc4hQVHc9ZOqY0gwbQCLtqQKHOU8RcgbrjhX +lEq7le5God2ukNctHU/CSvSF1LXRmrX+xQSdawwtpaRUtgx7YgG2cwo48rox/d3d +Knmf8sArMkvpNCAIj7oRI8FS34NbvKUYiMqqIEUinwmemA9s7QK/8DfTFOzDS9Z4 +hXNrU38SfQGCGSQcvwbDCjrCgQqpxoGhMYRUqPuwVo9PyuhPmxUCggEBAPT5XY3H +vnXL6Oust2ypPvzOZ0qjDCQGWjkFIGyWuZ7RFzjPJYKBeyCH/HqQuEeWsN29gj+B +UY9HyN9PDhEaGGRahgYmRKAOLs8BHJ7yzNTLZef5HiDFFkZ26n/QmQQGVxdvXpV5 +rLYR4XtIvCKkooJURqkrIATTENilin09iVpYbozKKFvSYmv+HN0+t03XxqtxnGVj +aS+lM0UeV8dWypce9Ipu1gSPLy67uSJ8p0oa99zo2OPgPjf2r9Rj8+oKLTf89aK4 +Ev//fukbeMwtRLl6hy97gUCvyoNdgXzEIjI+rdMC13LM26BvPxgtT2mqZs7ocU8Q +qptTEmKfVFlnNzMCggEBAOsAa2yGOE3MEaNDaOT9cOo2frpxevOjrU/rZ0Ds5ZHl +tpiM5qm/Grjd8tbDmQo2Xcarqnq37G9ce7x1JNRMdD42UbudTx0QSg3gi6FFxZVj +ccoDACUkIuT7NbUQV/LwCNV3/ZsWX75yanWhZUAtaAu8ZN/7dtpXVXcmZbYgs0xm +zAMlHlSDqMYeol2uyPRX0jdcDSc3kh6pGAssBpelenALrBjdQn++4S57iWeiKUfc +qvMtq9gHKcIRL3o/zwDln26hrZ7qgd6+hUYqG2MREs4vOPlpPwN+m2bJKrRKE0dO ++zylLJ5GaBn3Bv34wiuDZZSQt1ChMvXmKf8OKBZEkb8CggEAbXGGwVPOnFPoEHpO +TCZktI8GCItFXkbUQgsvTDQeY3yockHhUSfFuWfnfV5wZdNF2xrSOMruhCOe810f +PLa61QK4Q8EPAa60bNjjT4PLzPm94mAifKNwazSvWUD5S5oFiLvBtufwKDte0DRT +kOqai71ZADT7Dgy5xwBWGdPHLGy7nvymATfBrtuNS668N/PBl1CffZBnKtkUSbnf +n3f/9Hno6HvR86GAg9FsSaMFHg9kUvZYB55kTZ5ROYMaMqIvR4ckunigTGx552zV +j+pdfLvn72eu/BZNVFkPA42gdXAZOl9Xn7s0F737ozKC+wMdAS1Jifg5MEFxwkvK +ZFK/jwKCAQEAiCUmFylrVSb00PEsw/1QfWA06y7zXFNnBPYMS8Dy/yNmNdrrh0v/ +3zo2hdWrxA7bJU4u5gnIIHwj83qqa5QfhCtUDq2EOAJH5OJCApy5a2LBeZdjbiER +VjdzVgKx8Ty+4W0yr7a2oU8H/j4SuquTq7jpeBnnMXeHPBAyvOEU/x5O80N93tin +3p/A0SWBpo16bDgYJrA7JygvlclbyF9GH8OjYIRPElMzggpwAGoiIE/nehrrg6wi +tRvftaNh+dMOGrnwLDEQLEuUSqH6W9p4WpthFp2ytAOVZGcHJowDvzwysV/ACbIg +fWpv0pNbanolT3zHtx6st2kwy2MYNk5jYQKCAQEA6hsqOztjsDs3Q9GQMreKgzrp +FrBG6zSCJYsskWofl87kiAeztGmDCMtW0SiKAGwr1QxZA2qENUS/+Pdf1tmx7nIJ +Y+7nULd1BWKpiFxOIzY8lQtqItEpq4sJtp4q6grvHu1N6wfIyVTl75H4P/YlQo+Q +nOP8O0RiSE63sEgyWxIzi4BeiTIHfpUDw4LGjIqZAdDbGbsaLx4CLSMoxhxHKPzu +Yy+17mzAZAE5otdKzqCxjXjxKQtBOUA9n8Ye6e2ekoFXMmJI7DiuaVacuWQgOhCO +oqmuTnrGXxHrWwS7j0Bt9SZhHXu0b89faPegGk9pp5wrCxZrXlLq4TvT/BuFsA== +-----END RSA PRIVATE KEY----- diff --git a/dispatcher/entrypoint.sh b/dispatcher/entrypoint.sh new file mode 100644 index 0000000..73a3954 --- /dev/null +++ b/dispatcher/entrypoint.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# +# Copyright (C) 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +case "$AFC_DEVEL_ENV" in + "devel") + echo "Running debug profile" + ACCEPTOR_LOG_LEVEL="--log debug" + BIN=nginx-debug + apk add --update --no-cache bash + ;; + "production") + echo "Running production profile" + ACCEPTOR_LOG_LEVEL= + BIN=nginx + ;; + *) + echo "Uknown profile" + ACCEPTOR_LOG_LEVEL= + BIN=nginx + ;; +esac + +/docker-entrypoint.sh $BIN -g "daemon off;" & + +/wd/acceptor.py $ACCEPTOR_LOG_LEVEL --cmd run + +exit $? diff --git a/dispatcher/html/50x.html b/dispatcher/html/50x.html new file mode 100644 index 0000000..a57c2f9 --- /dev/null +++ b/dispatcher/html/50x.html @@ -0,0 +1,19 @@ + + + +Error + + + +

An error occurred.

+

Sorry, the page you are looking for is currently unavailable.
+Please try again later.

+

If you are the system administrator of this resource then you should check +the error log for details.

+

Faithfully yours, nginx.

+ + diff --git a/dispatcher/html/index.html b/dispatcher/html/index.html new file mode 100644 index 0000000..e8f5622 --- /dev/null +++ b/dispatcher/html/index.html @@ -0,0 +1,23 @@ + + + +Welcome to nginx! + + + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and +working. Further configuration is required.

+ +

For online documentation and support please refer to +nginx.org.
+Commercial support is available at +nginx.com.

+ +

Thank you for using nginx.

+ + diff --git a/dispatcher/nginx.conf b/dispatcher/nginx.conf new file mode 100644 index 0000000..18ba064 --- /dev/null +++ b/dispatcher/nginx.conf @@ -0,0 +1,18 @@ +# +# Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +user nginx; +worker_processes auto; + +error_log /dev/stdout crit; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + +include /etc/nginx/conf.d/nginx.conf; diff --git a/dispatcher/nginx.conf.template b/dispatcher/nginx.conf.template new file mode 100644 index 0000000..faa8dbc --- /dev/null +++ b/dispatcher/nginx.conf.template @@ -0,0 +1,219 @@ +# +# Copyright (C) 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +http { + log_format short_fmt '[$time_local] $request_time $upstream_response_time'; + log_format error_fmt '[$time_local] $remote_addr - $ssl_client_s_dn - $remote_user - $request_uri - $uri'; + access_log /dev/stdout error_fmt; + error_log /dev/stdout debug; + + + upstream msghnd { + # use hash algo to capture complete client address + hash $binary_remote_addr consistent; + server ${AFC_MSGHND_NAME}:${AFC_MSGHND_PORT}; + # idle connections preserved in the cache of each worker + keepalive 32; + } + + upstream webui { + # use hash algo to capture complete client address + hash $binary_remote_addr consistent; + server ${AFC_WEBUI_NAME}:${AFC_WEBUI_PORT}; + # idle connections preserved in the cache of each worker + keepalive 32; + } + + map $scheme:$afc_https_enforce $should_redirect { + http:TRUE 1; + default 0; + } + + server { + listen 80; + listen [::]:80 ipv6only=on; + listen 443 ssl; + listen [::]:443 ipv6only=on ssl; + + server_name ${AFC_SERVER_NAME}; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_certificate /certificates/servers/server.cert.pem; + ssl_certificate_key /certificates/servers/server.key.pem; + + ssl_client_certificate /etc/nginx/certs/client.bundle.pem; + ssl_verify_client optional; + ssl_verify_depth 10; + + #ssl_stapling on; + #ssl_stapling_verify on; + + # ignoring attempts to establish a session with a client that requests a wrong host name + set $reject_request 0; + set $afc_server_name ${AFC_SERVER_NAME}; + + if ($host != $server_name) { + set $reject_request 1; + } + # ... but not in case of a wildcard + if ($afc_server_name = "_") { + set $reject_request 0; + } + # we won't return any response to the client in case of rejection, just close the connection + if ($reject_request) { + return 444; + } + + # To enforce check HTTPS set AFC_ENFORCE_HTTPS to the value "true" + set $afc_https_enforce ${AFC_ENFORCE_HTTPS}; + # To enforce check mTLS set AFC_ENFORCE_MTLS to the value "true" + # otherwise it is optional + set $afc_mtls_status ${AFC_ENFORCE_MTLS}; + + if ($should_redirect = 1) { + return 301 https://$host$request_uri; + } + + set $afc_mtls_enforce ${AFC_ENFORCE_MTLS}; + + location /fbrat/ap-afc/availableSpectrumInquirySec { + #if ($ssl_client_verify != SUCCESS) { + # return 403; + #} + # disable buffering for latency + proxy_buffering off; + # response to a request + proxy_read_timeout ${AFC_PROXY_CONN_TOUT}; + # establish a connection with a proxied server + proxy_connect_timeout 720; + # transmit a request to a proxied server + proxy_send_timeout 720; + sendfile on; + proxy_bind $server_addr; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass http://webui; + proxy_redirect http:// $scheme://; + # keep connections open by changing it's header + proxy_http_version 1.1; + proxy_set_header "Connection" ""; + } + + location /fbrat/ap-afc/availableSpectrumInquiry { + if ($ssl_client_verify != SUCCESS) { + set $afc_mtls_status "${afc_mtls_status}_false"; + } + # check if mtls is enforced and ssl_client_verify is not success + if ($afc_mtls_status = true_false) { + return 403; + } + # disable buffering for latency + proxy_buffering off; + # response to a request + proxy_read_timeout ${AFC_PROXY_CONN_TOUT}; + # establish a connection with a proxied server + proxy_connect_timeout 720; + # transmit a request to a proxied server + proxy_send_timeout 720; + proxy_pass http://msghnd$uri$is_args$args; + # keep connections open by changing it's header + proxy_http_version 1.1; + proxy_set_header "Connection" ""; + access_log off; + log_not_found off; + } + + # forbid internal tests + location /fbrat/ap-afc/availableSpectrumInquiryInternal { + return 403; + } + + # forbid webdav methods other than GET + location /fbrat/ratapi/v1/files { + limit_except GET { deny all; } + sendfile on; + proxy_bind $server_addr; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass http://webui; + proxy_redirect http:// $scheme://; + } + + # forbid internal request + location /fbrat/ratapi/v1/GetAfcConfigByRulesetID { + return 403; + } + + # forbid internal request + location /fbrat/ratapi/v1/GetRulesetIDs { + return 403; + } + + location / { + if ($request_uri = "/") { + return 301 $scheme://$http_host/fbrat; + } + sendfile on; + proxy_bind $server_addr; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass http://webui; + proxy_redirect http:// $scheme://; + } + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504; + #error_page 500 502 503 504 /50x.html; + #location = /50x.html { + # root /usr/share/nginx/html; + #} + + error_page 403 /403.html; + location /403.html { + access_log /dev/stdout error_fmt; + } + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options "SAMEORIGIN"; + add_header Content-Security-Policy "script-src 'self' 'unsafe-eval' https://maps.googleapis.com https://code.jquery.com https://netdna.bootstrapcdn.com/bootstrap https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; style-src 'self' https://fonts.googleapis.com https://netdna.bootstrapcdn.com https://www.gstatic.com/recaptcha/ 'unsafe-inline'"; + add_header X-XSS-Protection "1; mode=block"; + add_header Referrer-Policy "strict-origin-when-cross-origin"; + add_header Permissions-Policy "geolocation=(self), microphone=(), camera=(), speaker=(), vibrate=(), payment=(), fullscreen=(self), sync-xhr=(), magnetometer=(), gyroscope=(), accelerometer=(), usb=(), autoplay=(), midi=(), encrypted-media=(), vr=(), xr-spatial-tracking=()"; + add_header Feature-Policy "geolocation 'self'; microphone 'none'; camera 'none'; speaker 'none'; vibrate 'none'; payment 'none'; fullscreen 'self'; sync-xhr 'none'; magnetometer 'none'; gyroscope 'none'; accelerometer 'none'; usb 'none'; autoplay 'none'; midi 'none'; encrypted-media 'self'; vr 'none'; xr-spatial-tracking 'none';"; + } + + # only for healthcheck + server { + listen 127.0.0.1:80; + + location /fbrat/ap-afc/healthy { + return 200 "OK"; + } + + } + + # Source data for NginxExporter (generator of Nginx Prometheus metrics) + server { + listen 8080; + location /stub_status { + stub_status; + } + } + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + tcp_nopush on; + + keepalive_timeout 180; + server_tokens off; +} diff --git a/dispatcher/nginx_mtls.conf b/dispatcher/nginx_mtls.conf new file mode 100644 index 0000000..7d1f053 --- /dev/null +++ b/dispatcher/nginx_mtls.conf @@ -0,0 +1,77 @@ + +user nginx; +worker_processes auto; + +error_log /dev/stdout crit; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + log_format short_fmt '[$time_local] $request_time $upstream_response_time'; + log_format error_fmt '[$time_local] $remote_addr - $ssl_client_s_dn - $remote_user - $request_uri - $uri'; + access_log /dev/stdout error_fmt; + error_log /dev/stdout debug; + + + upstream backend { + ip_hash; + server rat_server; + } +# server { +# listen 80; +# listen [::]:80; +# +# location /fbrat/ap-afc/1.1/availableSpectrumInquiry { +# proxy_pass http://backend/fbrat/ap-afc/1.1/availableSpectrumInquiry$is_args$args; +# } +# location / { +# proxy_pass http://backend$is_args$args; +# } +# } + server { + listen 443 ssl; + listen [::]:443 ssl; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_certificate /certificates/servers/server.cert.pem; + ssl_certificate_key /certificates/servers/server.key.pem; + + ssl_client_certificate /certificates/clients/client.bundle.pem; + ssl_verify_client on; + ssl_verify_depth 10; + + location / { + root /wd/nginx/html; + index index.html index.htm; + if ($ssl_client_verify != SUCCESS) { + return 403; + } + } + + location /fbrat/ap-afc/1.1/availableSpectrumInquiry { + if ($ssl_client_verify != SUCCESS) { + return 403; + } + proxy_pass http://backend$uri$is_args$args; + } + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + + } + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + tcp_nopush on; + + keepalive_timeout 180; +} diff --git a/dispatcher/requirements.txt b/dispatcher/requirements.txt new file mode 100644 index 0000000..b0e0549 --- /dev/null +++ b/dispatcher/requirements.txt @@ -0,0 +1,6 @@ +amqp==5.1.1 +gevent==23.9.1 +greenlet==2.0.2 +kombu==5.2.4 +requests==2.31.0 +vine==5.0.0 diff --git a/fbrat.rpmlintrc b/fbrat.rpmlintrc new file mode 100644 index 0000000..8daa6f1 --- /dev/null +++ b/fbrat.rpmlintrc @@ -0,0 +1,21 @@ +# Nonstandard license term +addFilter(r'.*: W: invalid-license Commercial') +addFilter(r'.*: W: invalid-url URL: .*') +addFilter(r'.*: W: invalid-url Source0: .*') + +# This is used for Doxygen files only +addFilter(r'.*: W: rpm-buildroot-usage %build -DAPIDOC_INSTALL_PATH=%{buildroot}%{apidocdir} \\') + +# Allow unnecessary cmake-generated linking +addFilter(r'.*: W: unused-direct-shlib-dependency .*') +# dbus configuration is package-driven +addFilter(r'.*: W: conffile-without-noreplace-flag /etc/dbus-1/system\.d/.*\.conf') +# Libary debug info is special case +addFilter(r'.*-debuginfo\..*: W: only-non-binary-in-usr-lib') + +# daemon users +addFilter(r'fbrat\..*: W: non-standard-uid /var/lib/fbrat fbrat') +addFilter(r'fbrat\..*: W: non-standard-gid /var/lib/fbrat fbrat') + +# The statically-linked library calls exit +addFilter(r'fbrat\..*: W: shared-lib-calls-exit /usr/lib64/libafccrashdump.so.0.0.0 .*') diff --git a/gunicorn/config.py b/gunicorn/config.py new file mode 100644 index 0000000..909f797 --- /dev/null +++ b/gunicorn/config.py @@ -0,0 +1,12 @@ +# +# Copyright 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +import prometheus_client.multiprocess + + +def child_exit(server, worker): + prometheus_client.multiprocess.mark_process_dead(worker.pid) diff --git a/gunicorn/gunicorn.conf.py b/gunicorn/gunicorn.conf.py new file mode 100644 index 0000000..bf085ac --- /dev/null +++ b/gunicorn/gunicorn.conf.py @@ -0,0 +1,11 @@ +# +# Copyright 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +bind = '0.0.0.0:8000' +workers = 20 +deamon = True +pidfile = '/run/gunicorn/openafc_app.pid' +timeout = 120 diff --git a/gunicorn/gunicorn.logs.conf b/gunicorn/gunicorn.logs.conf new file mode 100644 index 0000000..8e9ba26 --- /dev/null +++ b/gunicorn/gunicorn.logs.conf @@ -0,0 +1,28 @@ +[loggers] +keys=root, gunicorn.error + +[handlers] +keys=console + +[formatters] +keys=generic + +[logger_root] +level=DEBUG +handlers=console + +[logger_gunicorn.error] +level=DEBUG +handlers=console +propagate=0 +qualname=gunicorn.error + +[handler_console] +class=StreamHandler +formatter=generic +args=(sys.stdout, ) + +[formatter_generic] +format=%(asctime)s [%(process)d] [%(levelname)s] %(message)s +datefmt=%Y-%m-%d %H:%M:%S +class=logging.Formatter diff --git a/gunicorn/wsgi.py b/gunicorn/wsgi.py new file mode 100644 index 0000000..c69d173 --- /dev/null +++ b/gunicorn/wsgi.py @@ -0,0 +1,13 @@ +# +# Copyright 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +import ratapi + +app = ratapi.create_app( + config_override={ + 'APPLICATION_ROOT': '/fbrat', + } +) diff --git a/http-checkout/rathttpcheckout/__init__.py b/http-checkout/rathttpcheckout/__init__.py new file mode 100644 index 0000000..b3532e7 --- /dev/null +++ b/http-checkout/rathttpcheckout/__init__.py @@ -0,0 +1,22 @@ +''' This package is a pure collection of unittest cases. + +The test configuration can be controlled by environment variables: + +`HTTPCHECKOUT_BASEURL` + as the base URL to access the host-under-test. Make sure this has a trailing slash +`HTTPCHECKOUT_READONLY` + any non-empty value will skip all tests which modify the CPO Archive state. + +An example of running this checkout is: + +HTTPCHECKOUT_BASEURL=http://localhost:5000/ \ +XDG_DATA_DIRS=$PWD/testroot/share \ +PYTHONPATH=$PWD/http-checkout \ +nosetests -v rathttpcheckout + +''' + +from .aaa import * +from .paws import * +from .ratapi import * +from .www import * diff --git a/http-checkout/rathttpcheckout/aaa.py b/http-checkout/rathttpcheckout/aaa.py new file mode 100644 index 0000000..c62c2f5 --- /dev/null +++ b/http-checkout/rathttpcheckout/aaa.py @@ -0,0 +1,43 @@ +''' Test cases related to AAA functions (not the APIs that relate to them). +''' +import logging +from .base import (ValidateHtmlResponse, BaseTestCase) +import os + +#: Logger for this module +LOGGER = logging.getLogger(__name__) + + +class TestUserLogin(BaseTestCase): + + def setUp(self): + BaseTestCase.setUp(self) + + def tearDown(self): + BaseTestCase.tearDown(self) + + def test_login_options(self): + self._test_options_allow( + self._resolve_url('user/sign-in'), + {'GET', 'POST'} + ) + + def test_login_request(self): + resp = self.httpsession.get(self._resolve_url('user/sign-in')) + # now a location, managed by flask_login + self.assertEqual(200, resp.status_code) + self.assertTrue("csrf_token" in resp.content) + + def test_login_success(self): + resp = self.httpsession.post( + self._resolve_url('user/sign-in'), + ) + self.assertEqual(200, resp.status_code) + encoding = resp.headers.get("Content-Type") + LOGGER.debug("Mah: %s", encoding) + self.assertEqual("text/html; charset=utf-8", encoding) + self.assertTrue('form' in resp.content) + try: + ValidateHtmlResponse()(resp) + except Exception as err: + self.fail('body is not valid html: {0}'.format(err)) diff --git a/http-checkout/rathttpcheckout/afc.py b/http-checkout/rathttpcheckout/afc.py new file mode 100644 index 0000000..6955c08 --- /dev/null +++ b/http-checkout/rathttpcheckout/afc.py @@ -0,0 +1,143 @@ +from .base import (UserLoginBaseTestCase) + + +class TestAfcEngine(UserLoginBaseTestCase): + ''' Class for testing the results of the AFC Engine + ''' + + def setUp(self): + UserLoginBaseTestCase.setUp(self) + + def tearDown(self): + UserLoginBaseTestCase.tearDown(self) + + def _set_afc_config(self, afc_config): + ''' Uploads the _afc_config variable to the server to be used for AFC Engine tests + ''' + self._test_modify_request(self._resolve_url( + 'ratapi/v1/afcconfig/afc_config.json'), afc_config) + + def _generate_params( + self, + lat, + lng, + height, + semi_maj=0, + semi_min=0, + orientation=0, + height_type="AGL", + height_cert=0, + in_out_door="INDOOR", + ruleset_ids=None): + ''' Uses parameters to generate a well formed JSON object to be used for analysis. + + :param lat: latitude + :type lat: number + + :param lng: longitude + :type lng: number + + :param height: height + :type height: number + + :param semi_maj: ellipse semi-major axis (default 0) + :type semi_maj: number + + :param semi_min: ellipse semi-minor axis (default 0) + :type semi_min: number + + :param orientation: ellipse orientation. degrees clockwise from north (defualt 0) + :type orientation: number + + :param height_type: "AMSL" (above mean sea level)(default) | "AGL" (above ground level) + + :param height_cert: height uncertainty (default 0) + :type height_cert: number + + :param in_out_door: "INDOOR" | "OUTDOOR" | "ANY" + + :param ruleset_ids: list of ruleset IDs (default ['AFC-6GHZ-DEMO-1.0']) + + :returns: PawsRequest + ''' + + if ruleset_ids is None: + ruleset_ids = ['AFC-6GHZ-DEMO-1.0'] + + return { + 'deviceDesc': { + 'rulesetIds': ruleset_ids, + }, + 'location': { + 'point': { + 'center': { + 'latitude': lat, + 'longitude': lng, + }, + 'semiMajorAxis': semi_maj, + 'semiMinorAxis': semi_min, + 'orientation': orientation, + }, + }, + 'antenna': { + 'height': height, + 'heightType': height_type, + 'heightUncertainty': height_cert, + }, + 'capabilities': { + 'indoorOutdoor': in_out_door, + } + } + + def _test_geojson_result_valid(self, result): + ''' + ''' + + def _test_channel_result_valid(self, result): + ''' + ''' + + def _test_paws_result_valid(self, result, req_devicedesc): + ''' Tests that the structure of a returned paws object is correct + ''' + + # check for same device description + self.assertEqual(req_devicedesc, result.get('deviceDesc')) + + for spec in result.get('spectrumSpecs'): + + # check matching ruleset + self.assertEqual( + { + 'authority': 'US', + 'rulesetId': 'AFC-6GHZ-DEMO-1.0', + }, + spec.get('rulesetInfo') + ) + + for schedule in spec.get('spectrumSchedules'): + + # check properly formatted time + self._test_iso_time(schedule.get('eventTime').get('startTime')) + self._test_iso_time(schedule.get('eventTime').get('stopTime')) + + # must have four groups of channels + self.assertEqual(len(schedule.get('spectra')), 4) + + # validate spectra contents + self._test_present_bandwidths( + schedule.get('spectra'), + [20000000, 40000000, 80000000, 160000000]) + + def _test_iso_time(self, time): + ''' Tests that the time is a properly formatted ISO time string + ''' + self.assertRegexpMatches( + time, r'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z') + + def _test_present_bandwidths(self, spectra, bandwidths): + ''' Tests to make sure each bandwidth is present in the spectra profiles + ''' + present_bands = [s.get('resolutionBwHz') for s in spectra] + for band_width in bandwidths: + self.assertIn(band_width, present_bands) diff --git a/http-checkout/rathttpcheckout/base.py b/http-checkout/rathttpcheckout/base.py new file mode 100644 index 0000000..0fe9917 --- /dev/null +++ b/http-checkout/rathttpcheckout/base.py @@ -0,0 +1,967 @@ +''' Non-test support objects and classes used by the actual test cases. +''' + +import datetime +from io import BytesIO +import logging +import lxml.etree as etree +import os +import re +import requests +import shutil +import tempfile +import unittest +from urlparse import urljoin +import werkzeug.datastructures +import werkzeug.http +import werkzeug.urls +from nose import SkipTest + +#: Logger for this module +LOGGER = logging.getLogger(__name__) + +#: Regex to match application/xml and applicaiton/sub+xml +XML_CONTENT_RE = re.compile(r'^application/(.+\+)?xml$') + +#: Time format for ISO 8601 "basic" time used by XML Schema. +#: This string is usable by datetime.strftime and datetime.strptime +TIME_FORMAT_BASIC = '%Y%m%dT%H%M%SZ' +#: Time format for ISO 8601 "extended" time used by XML Schema. +#: This string is usable by datetime.strftime and datetime.strptime +TIME_FORMAT_EXTENDED = '%Y-%m-%dT%H:%M:%SZ' + +#: Absolute path to this package directory +PACKAGE_PATH = os.path.abspath(os.path.dirname(__file__)) + + +def get_xml_parser(schema): + ''' Generate a function to extract an XML DOM tree from an encoded document. + + :param schema: Iff not None, the document will be validated against + this schema object. + :type use_schema: bool + :return: The parser function which takes a file-like parameter and + returns a tree object of type :py:cls:`lxml.etree.ElementTree`. + ''' + xmlparser = etree.XMLParser(schema=schema) + + def func(infile): + try: + return etree.parse(infile, parser=xmlparser) + except etree.XMLSyntaxError as err: + infile.seek(0) + with tempfile.NamedTemporaryFile(delete=False) as outfile: + shutil.copyfileobj(infile, outfile) + raise ValueError( + 'Failed to parse XML with error {0} in file {1}'.format( + err, outfile.name)) + + return func + + +def extract_metadict(doc): + ''' Extract a server metadata dictionary from its parsed XML document. + + :param doc: The document to read from. + :return: The metadata URL map. + ''' + metadict = {} + for el_a in doc.findall('//{http://www.w3.org/1999/xhtml}a'): + m_id = el_a.attrib.get('id') + m_href = el_a.attrib.get('href') + if m_id is None or m_href is None: + continue + metadict[m_id] = m_href + return metadict + + +def merged(base, delta): + ''' Return a merged dictionary contents. + + :param base: The initial contents to merge. + :param delta: The modifications to apply. + :return: A dictionary containing the :py:obj:`base` updated + by the :py:obj:`delta`. + ''' + mod = dict(base) + mod.update(delta) + return mod + + +def limit_count(iterable, limit): + ''' Wrap an iterable/generator with a count limit to only yield the first + :py:obj:`count` number of items. + + :param iterable: The source iterable object. + :param limit: The maximum number of items available from the generator. + :return A generator with a count limit. + ''' + count = 0 + for item in iterable: + yield item + count += 1 + if count >= limit: + return + + +def modify_etag(orig): + ''' Given a base ETag value, generate a modified ETag which is + guaranteed to not match the original. + + :param str orig: The original ETag. + :return: A different ETag + ''' + # Inject characters near the end + mod = orig[:-1] + '-eh' + orig[-1:] + return mod + + +class ValidateJsonResponse(object): + ''' Validate an expected JSON file response. + ''' + + def __call__(self, resp): + import json + json.loads(resp.content) + + +class ValidateHtmlResponse(object): + ''' Validate an HTML response with a loose parser. + ''' + + def __call__(self, resp): + import bs4 + kwargs = dict( + markup=resp.content, + features='lxml', + ) + bs4.BeautifulSoup(**kwargs) + + +class ValidateXmlResponse(object): + ''' Validate an expected XML file response. + + :param parser: A function to take a file-like input and output an + XML element tree :py:cls:`lxml.etree.ElementTree`. + :param require_root: If not None, the root element must be this value. + ''' + + def __init__(self, parser, require_root=None): + self._parser = parser + if not callable(self._parser): + raise ValueError('ValidateXmlResponse parser invalid') + self._require_root = require_root + + def __call__(self, resp): + xml_tree = self._parser(BytesIO(resp.content)) + if self._require_root is not None: + root_tag = xml_tree.getroot().tag + if self._require_root != root_tag: + raise ValueError( + 'Required root element "{0}" not present, got "{1}"'.format( + self._require_root, root_tag)) + + +class BaseTestCase(unittest.TestCase): + ''' Common access and helper functions which use the :py:mod:`unittest` + framework but this class defines no test functions itself. + + :ivar httpsession: An :py:class:`requests.Session` instance for test use. + :ivar xmlparser: An :py:class:`etree.XMLParser` instance for test use. + ''' + + #: Cached URL to start at and to resolve from + BASE_URL = os.environ.get('HTTPCHECKOUT_BASEURL') + #: True if the editing tests should be skipped + READONLY = bool(os.environ.get('HTTPCHECKOUT_READONLY')) + #: Keep any created resources in the tearDown() method which are left behind + KEEP_TESTITEMS = bool(os.environ.get('HTTPCHECKOUT_KEEP_TESTITEMS')) + + def setUp(self): + unittest.TestCase.setUp(self) + self.maxDiff = 10e3 + + self.assertIsNotNone( + self.BASE_URL, 'Missing environment HTTPCHECKOUT_BASEURL') + self.httpsession = requests.Session() + + ca_roots = os.environ.get('HTTPCHECKOUT_CACERTS') + LOGGER.info('HTTPCHECKOUT_CACERTS is "%s"', ca_roots) + if ca_roots == '': + import warnings + from urllib3.exceptions import InsecureRequestWarning + self.httpsession.verify = False + warnings.filterwarnings("ignore", category=InsecureRequestWarning) + + def tearDown(self): + self.httpsession = None + unittest.TestCase.tearDown(self) + + def _assertWriteTest(self): + ''' Skip the current test if HTTPCHECKOUT_READONLY is set. + ''' + if self.READONLY: + self.skipTest( + 'Not running editing tests because of HTTPCHECKOUT_READONLY') + + def _resolve_url(self, url): + ''' Resolve a URL relative to the original base URL. + + :param url: The URL to resolve. + :type url: str + :return: The resolved absolute URL to request on. + :rtype: str + ''' + return urljoin(self.BASE_URL, url) + + def assertSameUrlPath(self, first, second): + ''' Assert that two URLs are equal except for their query/fragment parts. + ''' + f_url = werkzeug.urls.url_parse(first) + s_url = werkzeug.urls.url_parse(second) + + for attr in ('scheme', 'netloc', 'path'): + self.assertEqual( + getattr(f_url, attr), + getattr(s_url, attr), + 'Mismatched URL {0}'.format(attr) + ) + + def _get_xml_parser(self, use_schema=None): + ''' Generate a function to extract an XML DOM tree from an encoded document. + + :return: The parser function which takes a file-like parameter and + returns a tree object of type :py:cls:`lxml.etree.ElementTree`. + ''' + return get_xml_parser(schema=use_schema) + + def _get_xml_encoder(self): + ''' Generate a function to encode a document from an XML DOM tree. + + :return: The parser function which takes a parameter of a + tree object of type :py:cls:`lxml.etree.ElementTree` and + returns a file-like object. + ''' + + def func(doc, outfile=None): + ''' Encode a document. + + :parm doc: The document to encode. + :type doc: :py:cls:`lxml.etree.ElementTree` + :param outfile: An optional file-like object to encode into. + This must be None if the encoder is used multiple times. + :type outfile: file-like or None + :return: The encoded file-like object. + ''' + if outfile is None: + outfile = BytesIO() + + doc.write(outfile, encoding='UTF-8', xml_declaration=True) + if hasattr(outfile, 'seek'): + try: + outfile.seek(0) + except BaseException: + pass + return outfile + + return func + + def _test_working_links(self, text): + ''' Verify that html has well formed links + + :param text: html doc with href's + ''' + + import bs4 + html = bs4.BeautifulSoup(text, 'html.parser') + for link in [a['href'] for a in html.find_all('a')]: + self._test_working_link(link) + + def _test_working_link(self, url): + ''' Verify that a url returns a 200 response + + :param url: The URL to be checked + ''' + + resolved_url = self._resolve_url(url) + resp = self.httpsession.get(resolved_url) + self.assertEqual(200, resp.status_code) + + def _test_options_allow(self, url, methods): + ''' Verify that the OPTIONS response for a URL matches a specific set. + + :param url: The URL to pass to :py:mod:`requests` + :type url: str + :param methods: The method names which must be identical to the response. + :type methods: iterable + :param params: URL parameter dictionary to pass to :py:mod:`requests`. + :type params: dict or None + ''' + methods = set([str(m).upper() for m in methods]) + methods.add('OPTIONS') + if 'GET' in methods: + methods.add('HEAD') + + resolved_url = self._resolve_url(url) + resp = self.httpsession.options(resolved_url) + self.assertEqual(200, resp.status_code) + got_allow = werkzeug.http.parse_set_header(resp.headers['allow']) + self.assertEqual(methods, set(got_allow)) + + def _test_path_contents( + self, + url, + params=None, + base_headers=None, + validate_response_pre=None, + validate_response_post=None, + must_authorize=True, + valid_status=None, + content_type=None, + valid_encodings=None, + require_length=True, + require_vary=None, + require_etag=True, + require_lastmod=True, + require_cacheable=True, + cache_must_revalidate=False): + ''' Common assertions for static resources. + + :param url: The URL to pass to :py:mod:`requests` + :type url: str + :param params: URL parameter dictionary to pass to :py:mod:`requests`. + :type params: dict or None + :param base_headers: A dictionary of headers to send with every request. + :type base_headers: dict or None + :param validate_response_pre: A callable which takes a single argument of + the response object and performs its own validation of the headers + and/or body for each non-cached response. + This is performed before any of the parametric checks. + :type validate_response_pre: callable or None + :param validate_response_post: A callable which takes a single argument of + the response object and performs its own validation of the headers + and/or body for each non-cached response. + This is performed after any of the parametric checks. + :type validate_response_post: callable or None + :param must_authorize: Access to the resource without an Authorization + header is attempted and compared against this value. + :type must_authorize: bool + :param valid_status: A set of valid status codes to allow. + If not provided, only code 200 is valid. + :type valid_status: set or None + :param content_type: If not None, the required Content-Type header. + :type content_type: bool or None + :param valid_encodings: If not None, a list of content encodings to check for. + The resource must provide each of the non-identity encodings listed. + :type valid_encodings: list or None + :param require_length: If either true or false, assert that the + content-length header is present or not. + :type require_length: bool or None + :param require_vary: A set of Vary reults required to be present + in the response. + If the :py:obj:`valid_encodings` list has more than the identity + encoding present, then 'accept-encoding' will be automatically + added to this vary list. + :type require_vary: list or None + :param require_etag: If not None, whether the ETag is required present + or not present (True or False) or a specific string value. + :type require_etag: str or bool or None + :param require_lastmod: If not None, whether the Last-Modified is + required present or not present (True or False) or a specific value. + :type require_lastmod: str or bool or None + :param require_cacheable: If true, the resource is checked for its cacheability. + Not all resources should be cacheable (even if not explicitly marked no-cache). + :type require_cacheable: bool + :param cache_must_revalidate: If True, the response must have its + 'must-revalidate' cache control header set. + :type cache_must_revalidate: bool + :raises: raises unittest exceptions if an assertion fails + ''' + + if base_headers is None: + base_headers = {} + + if valid_status is None: + valid_status = [200] + valid_status = set(valid_status) + + # Set of valid encodings to require + if valid_encodings is None: + valid_encodings = [] + valid_encodings = set(valid_encodings) + valid_encodings.add('identity') + # Force as ordered list with identity encoding first and gzip always + # attempted + try_encodings = set(valid_encodings) + try_encodings.discard('identity') + try_encodings = sorted(list(try_encodings)) + try_encodings.insert(0, 'identity') + # Cached identity-encoded contents + identity_body = None + + if require_vary is None: + require_vary = [] + require_vary = set(require_vary) + if len(valid_encodings) > 1: + require_vary.add('accept-encoding') + + resolved_url = self._resolve_url(url) + + # Options on the resource itself + resp = self.httpsession.options( + resolved_url, params=params, + allow_redirects=False, + headers=base_headers, + ) + self.assertEqual(200, resp.status_code) + got_allow = werkzeug.http.parse_set_header(resp.headers.get('allow')) + self.assertIn('options', got_allow) + if 404 not in valid_status: + self.assertIn('head', got_allow) + self.assertIn('get', got_allow) + + # Options without authentication + resp = self.httpsession.options( + resolved_url, params=params, + allow_redirects=False, + headers=merged(base_headers, { + 'authorization': None, + }), + ) + if must_authorize: + self.assertEqual(401, resp.status_code) + else: + self.assertEqual(200, resp.status_code) + + for try_encoding in try_encodings: + # initial non-cache response + enc_headers = merged( + base_headers, + { + 'accept-encoding': try_encoding, + } + ) + resp = self.httpsession.get( + resolved_url, params=params, + allow_redirects=False, + headers=enc_headers, + stream=True, + ) + # External validation first + if validate_response_pre: + try: + validate_response_pre(resp) + except Exception as err: + self.fail('Failed pre-validation: {0}'.format(err)) + + # Now parametric validation + self.assertIn(resp.status_code, valid_status) + + got_content_type = werkzeug.http.parse_options_header( + resp.headers['content-type']) + if content_type is not None: + self.assertEqual(content_type.lower(), + got_content_type[0].lower()) + + # Encoding comparison compared to valid + got_encoding = resp.headers.get('content-encoding', 'identity') + if try_encoding in valid_encodings: + self.assertEqual(try_encoding, got_encoding) + else: + self.assertEqual( + 'identity', got_encoding, + msg='"{0}" was supposed to be a disallowed content-encoding but it was accepted'.format( + try_encoding) + ) + + got_length = resp.headers.get('content-length') + if require_length is True: + self.assertIsNotNone(got_length, msg='Content-Length missing') + elif require_length is False: + self.assertIsNone( + got_length, msg='Content-Length should not be present') + + # Guarantee type is correct also + if got_length is not None: + try: + got_length = int(got_length) + except ValueError: + self.fail( + 'Got a non-integer Content-Length: {0}'.format(got_length)) + + got_vary = werkzeug.http.parse_set_header(resp.headers.get('vary')) + for item in require_vary: + LOGGER.debug("headers: %s", resp.headers) + self.assertIn( + item, + got_vary, + msg='Vary header missing item "{0}" got {1}'.format( + item, + got_vary)) + + got_etag = resp.headers.get('etag') + got_lastmod = resp.headers.get('last-modified') + if resp.status_code != 204: + if require_etag is True: + self.assertIsNotNone(got_etag, msg='ETag header missing') + elif require_etag is False: + self.assertIsNone( + got_etag, msg='ETag header should not be present') + elif require_etag is not None: + self.assertEqual(require_etag, got_etag) + + if require_lastmod is True: + self.assertIsNotNone( + got_lastmod, msg='Last-Modified header missing') + elif require_lastmod is False: + self.assertIsNone( + got_lastmod, msg='Last-Modified header should not be present') + elif require_lastmod is not None: + self.assertEqual(require_lastmod, got_lastmod) + + # Caching headers + cache_control = werkzeug.http.parse_cache_control_header( + resp.headers.get('cache-control'), + cls=werkzeug.datastructures.ResponseCacheControl, + ) + # The resource must define its domain + if False: + self.assertTrue( + cache_control.no_cache + or cache_control.public # pylint: disable=no-member + or cache_control.private, # pylint: disable=no-member + msg='Missing cache public/private assertion for {0}'.format( + resolved_url) + ) + if require_cacheable is not False and cache_must_revalidate is True: + self.assertTrue( + cache_control.must_revalidate) # pylint: disable=no-member + if require_cacheable is True: + self.assertFalse(cache_control.no_cache) + self.assertFalse(cache_control.no_store) +# self.assertLessEqual(0, cache_control.max_age) + elif require_cacheable is False: + # FIXME not always true + self.assertTrue(cache_control.no_cache) + + # Actual body content itself + got_body = str(resp.content) + if resp.status_code == 204: + self.assertEqual('', got_body) + else: + # Ensure decoded body is identical + if got_encoding == 'identity': + identity_body = got_body + self.assertIsNotNone(identity_body) + if got_length is not None: + self.assertEqual(len(identity_body), got_length) + else: + self.assertEqual(identity_body, got_body) + + # XML specific decoding + if XML_CONTENT_RE.match( + got_content_type[0]) is not None and validate_response_post is None: + validate_response_post = ValidateXmlResponse( + self._get_xml_parser(use_schema=True)) + + # After all parametric tests on this response + if validate_response_post: + try: + validate_response_post(resp) + except Exception as err: + self.fail('Failed post-validation: {0}'.format(err)) + + # Check the unauthorized view of same URL + for method in ('GET', 'HEAD'): + resp = self.httpsession.request( + method, + resolved_url, params=params, + allow_redirects=False, + headers=merged(enc_headers, { + 'authorization': None, + }), + ) + if must_authorize: + self.assertEqual( + 401, + resp.status_code, + msg='For {0} on {1}: Expected 401 status got {2}'.format( + method, + resolved_url, + resp.status_code)) + else: + self.assertIn( + resp.status_code, + valid_status, + msg='For {0} on {1}: Expected valid status got {2}'.format( + method, + resolved_url, + resp.status_code)) + + # Any resource with cache control header + resp = self.httpsession.head( + resolved_url, params=params, + allow_redirects=False, + headers=merged(enc_headers, { + 'if-match': '*', + }), + ) + self.assertIn(resp.status_code, valid_status) + # Caching with ETag + if got_etag is not None: + self.assertIn(resp.status_code, valid_status) + # Existing resource + resp = self.httpsession.head( + resolved_url, params=params, + allow_redirects=False, + headers=merged(enc_headers, { + 'if-match': got_etag, + }), + ) + self.assertIn(resp.status_code, valid_status) + # Client cache response + resp = self.httpsession.head( + resolved_url, params=params, + allow_redirects=False, + headers=merged(enc_headers, { + 'if-none-match': got_etag, + }), + ) + self.assertIn(resp.status_code, [ + 304] if require_cacheable else valid_status) + # With adjusted ETag + mod_etag = modify_etag(got_etag) + resp = self.httpsession.head( + resolved_url, params=params, + allow_redirects=False, + headers=merged(enc_headers, { + 'if-none-match': mod_etag, + }), + ) + self.assertIn(resp.status_code, valid_status) + + # Caching with Last-Modified + if got_lastmod is not None: + # No changes here so normal response + resp = self.httpsession.head( + resolved_url, params=params, + allow_redirects=False, + headers=merged(enc_headers, { + 'if-unmodified-since': got_lastmod, + }), + ) +# self.assertIn(resp.status_code, valid_status) + + # An earlier time will give a 412 + new_time = werkzeug.http.parse_date( + got_lastmod) - datetime.timedelta(seconds=5) + resp = self.httpsession.head( + resolved_url, params=params, allow_redirects=False, headers=merged( + enc_headers, { + 'if-unmodified-since': werkzeug.http.http_date(new_time), }), ) + self.assertIn(resp.status_code, [ + 412] if require_cacheable else valid_status) + + # An later time is normal response + new_time = werkzeug.http.parse_date( + got_lastmod) + datetime.timedelta(seconds=5) + resp = self.httpsession.head( + resolved_url, params=params, allow_redirects=False, headers=merged( + enc_headers, { + 'if-unmodified-since': werkzeug.http.http_date(new_time), }), ) + self.assertIn(resp.status_code, valid_status) + + # Client cache response + resp = self.httpsession.head( + resolved_url, params=params, + allow_redirects=False, + headers=merged(enc_headers, { + 'if-modified-since': got_lastmod, + }), + ) +# self.assertIn(resp.status_code, [304] if require_cacheable else valid_status) + + # A later time should also give a 304 response + new_time = werkzeug.http.parse_date( + got_lastmod) + datetime.timedelta(seconds=5) + resp = self.httpsession.head( + resolved_url, params=params, + allow_redirects=False, + headers=merged(enc_headers, { + 'if-modified-since': werkzeug.http.http_date(new_time), + }), + ) + self.assertIn(resp.status_code, [ + 304] if require_cacheable else valid_status) + + # An earlier time will give a 200 response + new_time = werkzeug.http.parse_date( + got_lastmod) - datetime.timedelta(seconds=5) + resp = self.httpsession.head( + resolved_url, params=params, + allow_redirects=False, + headers=merged(enc_headers, { + 'if-modified-since': werkzeug.http.http_date(new_time), + }), + ) + self.assertIn(resp.status_code, valid_status) + + def _test_modify_request( + self, url, up_data, method='POST', params=None, **kwargs): + ''' Common assertions for static resources. + + :param url: The URL to pass to :py:mod:`requests` + :type url: str + :param up_data: The request body data. + :type up_data: str or file-like + :param str method: The method to request the modification. + :param params: URL parameter dictionary to pass to :py:mod:`requests`. + :type params: dict or None + :param base_headers: A dictionary of headers to send with every request. + :type base_headers: dict or None + :param must_authorize: Access to the resource without an Authorization + header is attempted and compared against this value. + :type must_authorize: bool + :param valid_status: A set of valid status codes to allow. + If not provided, only code 201 is valid for new resources and 204 for existing ones. + :type valid_status: set or None + :param empty_is_valid: Whether or not an empty document is a valid modification. + The default is False. + :type empty_is_valid: bool + :param is_idempotent: Whether or not sending the same request should + not change the resource (except for the modify time). + The default is true if the method is PUT. + :type is_idempotent: bool + :param require_etag: If not None, whether the ETag is required present or not present. + :type require_etag: bool or None + ''' + method = method.lower() + # Arguments not passed to _assert_modify_response() + base_headers = kwargs.pop('base_headers', None) + if base_headers is None: + base_headers = {} + must_authorize = kwargs.pop('must_authorize', True) + empty_is_valid = kwargs.pop('empty_is_valid', False) + is_idempotent = kwargs.pop('is_idempotent', method == 'put') + + resolved_url = self._resolve_url(url) + + if hasattr(up_data, 'seek'): + + def reset_up_data(): + up_data.seek(0) + return up_data + + else: + + def reset_up_data(): + return up_data + + # Options on the resource itself + resp = self.httpsession.options( + resolved_url, params=params, + allow_redirects=False, + headers=base_headers, + ) + self.assertEqual(200, resp.status_code) + got_allow = werkzeug.http.parse_set_header(resp.headers['allow']) + self.assertIn('options', got_allow) + self.assertIn(method, got_allow) + # Options without authentication + resp = self.httpsession.options( + resolved_url, params=params, + allow_redirects=False, + headers=merged(base_headers, { + 'authorization': None, + }), + ) + if must_authorize: + self.assertEqual(401, resp.status_code) + else: + self.assertEqual(200, resp.status_code) + + # Initial state for conditions + resp_head = self.httpsession.head( + resolved_url, params=params, + headers={'accept-encoding': 'identity'}, + ) + init_status = resp_head.status_code + self.assertIn(init_status, {200, 404}) + init_etag = resp_head.headers.get('etag') + + if init_status == 200: + # Replacing resource + if kwargs.get('valid_status') is None: + kwargs['valid_status'] = [204] + match_etag = init_etag if init_etag else '*' + add_headers_fail = {'if-none-match': match_etag} + add_headers_good = {'if-match': match_etag} + + elif init_status == 404: + # New resource + if kwargs.get('valid_status') is None: + kwargs['valid_status'] = [201] + add_headers_fail = {'if-match': '*'} + add_headers_good = {'if-none-match': '*'} + + if not empty_is_valid: + # Invalid header content + resp = self.httpsession.request( + method, resolved_url, params=params, + ) + self.assertEqual(415, resp.status_code) + # Invalid (empty) body content + resp = self.httpsession.request( + method, resolved_url, params=params, + headers=base_headers, + ) + self.assertEqual(415, resp.status_code) + + # Check precondition failure + resp = self.httpsession.request( + method, resolved_url, params=params, + headers=merged(base_headers, add_headers_fail), + data=reset_up_data(), + ) + self.assertEqual(412, resp.status_code) + + if must_authorize: + # Unauthorized access with otherwise valid request + resp = self.httpsession.request( + method, resolved_url, params=params, + headers=merged(base_headers, { + 'authorization': None, + }), + data=reset_up_data(), + ) + self.assertEqual(401, resp.status_code) + + # Actual modifying request + resp_mod = self.httpsession.request( + method, resolved_url, params=params, + headers=merged(base_headers, add_headers_good), + data=reset_up_data(), + ) + self._assert_modify_response(resp_mod, **kwargs) + got_modtime = resp_mod.headers.get('last-modified') + got_etag = resp_mod.headers.get('etag') + + # Verify the same info is present in new HEAD reply + resp_head = self.httpsession.head( + resolved_url, params=params, + headers={'accept-encoding': 'identity'}, + ) + self.assertEqual(200, resp_head.status_code) + self.assertEqual(got_modtime, resp_head.headers.get('last-modified')) + self.assertEqual(got_etag, resp_head.headers.get('etag')) + + if is_idempotent: + # Check a duplicate request + add_headers_good = {'if-match': got_etag} + kwargs['valid_status'] = [204] + resp_mod = self.httpsession.request( + method, resolved_url, params=params, + headers=merged(base_headers, add_headers_good), + data=reset_up_data(), + ) + self._assert_modify_response(resp_mod, **kwargs) + self.assertEqual(got_etag, resp_mod.headers.get('etag')) + + # Give back the final valid response + return resp_mod + + def _assert_modify_response(self, resp, valid_status=None, + require_etag=True, require_lastmod=True, + old_etag=None): + ''' Verify the contents of a response to HTTP modification with no body. + + :param resp: The response object to check. + :type resp: :py:cls:`requests.Response` + :param valid_status: A set of valid status codes to allow. + If not provided, only codes (200, 201, 204) are valid. + :type valid_status: set or None + :param require_etag: If not None, whether the ETag is required present + or not present (True or False) or a specific string value. + :type require_etag: str or bool or None + :param require_lastmod: If not None, whether the Last-Modified is + required present or not present (True or False) or a specific value. + :type require_lastmod: str or bool or None + :param old_etag: An optional old ETag value to compare against. + The new response must have a different ETag value than this. + :type old_etag: str or None + ''' + if valid_status is None: + valid_status = [200, 201, 204] + valid_status = set(valid_status) + + self.assertIn(resp.status_code, valid_status) + got_lastmod = resp.headers.get('last-modified') + got_etag = resp.headers.get('etag') + + if require_etag is True: + self.assertIsNotNone(got_etag, msg='ETag header missing') + elif require_etag is False: + self.assertIsNone( + got_etag, msg='ETag header should not be present') + elif require_etag is not None: + self.assertEqual(require_etag, got_etag) + + if require_lastmod is True: + self.assertIsNotNone( + got_lastmod, msg='Last-Modified header missing') + elif require_lastmod is False: + self.assertIsNone( + got_lastmod, msg='Last-Modified header should not be present') + elif require_lastmod is not None: + self.assertEqual(require_lastmod, got_lastmod) + + if old_etag is not None: + self.assertNotEqual(old_etag, got_etag) + + # Empty body + self.assertFalse(bool(resp.content)) + + +class UserLoginBaseTestCase(BaseTestCase): + """Wraps tests in login/logout flow + + Encapsulates login/logout wrapping of tests. + Tests that require authentication will need to use the saved login_token and cookies as headers in their requests + """ + #: User name to test as this assumes a user HTTPCHECKOUT_ACCTNAME already exists in the User DB + VALID_ACCTNAME = os.environ.get( + 'HTTPCHECKOUT_ACCTNAME', 'admin').decode('utf8') + VALID_PASSPHRASE = os.environ.get( + 'HTTPCHECKOUT_PASSPHRASE', 'admin').decode('utf8') + + def __init__(self, *args, **kwargs): + BaseTestCase.__init__(self, *args, **kwargs) + self.login_token = None + self.cookies = None + + def setUp(self): + BaseTestCase.setUp(self) + resp = self.httpsession.post( + self._resolve_url('auth/login'), + json={ + 'email': self.VALID_ACCTNAME, + 'password': self.VALID_PASSPHRASE}) + LOGGER.debug('code: %s, login headers: %s', + resp.status_code, resp.content) + self.cookies = resp.cookies + if resp.status_code == 404: + self.fail(msg="{} not found on this server.".format( + self._resolve_url('auth/login'))) + try: + self.login_token = resp.json()["token"] + except ValueError: + raise SkipTest("Could not login as {}".format(self.VALID_ACCTNAME)) + + def tearDown(self): + resp = self.httpsession.post(self._resolve_url( + 'auth/logout'), params={'Authorization': self.login_token}) + LOGGER.debug('response code: %d\nbody: %s', + resp.status_code, resp.content) + self.login_token = None + self.cookies = None + BaseTestCase.tearDown(self) diff --git a/http-checkout/rathttpcheckout/paws.py b/http-checkout/rathttpcheckout/paws.py new file mode 100644 index 0000000..35e5474 --- /dev/null +++ b/http-checkout/rathttpcheckout/paws.py @@ -0,0 +1,267 @@ +''' Test cases related to PAWS API. +''' + +import logging +from urlparse import urljoin +from random import randint +from .base import (BaseTestCase, UserLoginBaseTestCase) +from afc import (TestAfcEngine) +from nose import SkipTest + +#: Logger for this module +LOGGER = logging.getLogger(__name__) + + +class TestPawsApi(TestAfcEngine): + ''' Test case to verify the PAWS JSON-RPC endpoint. + ''' + + def setUp(self): + UserLoginBaseTestCase.setUp(self) + + # Get the actual endpoint URL + resp = self.httpsession.head( + self._resolve_url(''), + allow_redirects=True + ) + self.assertIn(resp.status_code, (200,)) + index_url = resp.url + config_url = urljoin(index_url, '../ratapi/v1/guiconfig') + resp = self.httpsession.get(config_url) + self.guiconfig = resp.json() + self.paws_url = self._resolve_url(self.guiconfig['paws_url']) + + def tearDown(self): + UserLoginBaseTestCase.tearDown(self) + + def _call_jsonrpc(self, url, method, params, expect_status=200, + expect_error=None, expect_result=None): + if not params: + params = {} + + req_id = randint(1, 10**9) + req_body = { + 'jsonrpc': '2.0', + 'id': req_id, + 'method': method, + 'params': params, + } + LOGGER.debug("request:\n%s", req_body) + resp = self.httpsession.post( + url, + headers={ + 'accept-encoding': 'gzip', + }, + json=req_body, + ) + + LOGGER.debug("request code: %d body:\n%s", + resp.status_code, resp.content) + self.assertEqual(expect_status, resp.status_code) + self.assertEqual('application/json', resp.headers.get('content-type')) + resp_body = resp.json() + self.assertEqual('2.0', resp_body.get('jsonrpc')) + self.assertEqual(req_id, resp_body.get('id')) + + if expect_error is not None: + err_obj = resp_body.get('error') + self.assertIsNotNone(err_obj) + for (key, val) in expect_error.iteritems(): + LOGGER.debug("%s ==? %s", val, err_obj.get(key)) + self.assertEqual(val, err_obj.get(key)) + elif expect_result is not None: + result_obj = resp_body.get('result') + self.assertIsNotNone( + result_obj, msg="In body {}".format(resp_body)) + for (key, val) in expect_result.iteritems(): + self.assertEqual(val, result_obj.get(key)) + + return resp_body + + def test_browser_redirect(self): + resp = self.httpsession.head( + self.paws_url + ) + self.assertEqual(302, resp.status_code) + self.assertEqual(urljoin(self.paws_url, 'paws/browse/'), + resp.headers.get('location')) + + def test_jsonrpc_empty(self): + resp = self.httpsession.post( + self.paws_url, + json={}, + ) + self.assertEqual(200, resp.status_code) + self.assertEqual('application/json', resp.headers.get('content-type')) + + got_body = resp.json() + self.assertEqual(u'2.0', got_body.get('jsonrpc')) + self.assertEqual(None, got_body.get('id')) + got_error = got_body.get('error') + self.assertIsNotNone(got_error) + self.assertEqual(-32602, got_error['code']) + self.assertEqual(u'InvalidParamsError', got_error['name']) + + def test_jsonrpc_badmethod(self): + self._call_jsonrpc( + self.paws_url, + method='hi', + params={}, + expect_error={ + u'code': -32601, + u'name': 'MethodNotFoundError', + } + ) + + def test_jsonrpc_badargs(self): + self._call_jsonrpc( + self.paws_url, + method='spectrum.paws.getSpectrum', + params={}, + expect_error={ + u'code': -32602, + u'name': u'InvalidParamsError', + u'message': u'InvalidParamsError: Required parameter names: deviceDesc location antenna capabilities type version', + }) + + def test_jsonrpc_no_rulesets(self): + req_devicedesc = { + "serialNumber": "sn-test", + } + self._call_jsonrpc( + self.paws_url, + method='spectrum.paws.getSpectrum', + params={ + "antenna": { + "height": 25, + "heightType": "AMSL", + "heightUncertainty": 5 + }, + "capabilities": { + "indoorOutdoor": "INDOOR" + }, + "deviceDesc": req_devicedesc, + "location": { + "point": { + "center": { + "latitude": 40.75, + "longitude": -74 + }, + "orientation": 48, + "semiMajorAxis": 100, + "semiMinorAxis": 75 + } + }, + "type": "AVAIL_SPECTRUM_REQ", + "version": "1.0" + }, + expect_status=401, + expect_error={ + u'code': 401, + u'name': u'InvalidCredentialsError', + u'message': u'InvalidCredentialsError: Invalid rulesetIds: [\"AFC-6GHZ-DEMO-1.1\"] expected', + } + ) + + def test_paws_valid(self): + afc_loc = self.guiconfig["afcconfig_defaults"] + LOGGER.debug("cookies: %s, token: %s", self.cookies, self.login_token) + resp = self.httpsession.head( + self._resolve_url(afc_loc), + headers={ + 'Authorization': self.login_token}, + cookies=self.cookies) + code = resp.status_code + LOGGER.debug("status: %d, url: %s", code, self._resolve_url(afc_loc)) + if code == 404: + raise SkipTest("AFC Config does not exist.") + req_devicedesc = { + "serialNumber": "sn-test", + "rulesetIds": ["AFC-6GHZ-DEMO-1.1"] + } + + self._call_jsonrpc( + self.paws_url, + method='spectrum.paws.getSpectrum', + params={ + "antenna": { + "height": 25, + "heightType": "AMSL", + "heightUncertainty": 5 + }, + "capabilities": { + "indoorOutdoor": "INDOOR" + }, + "deviceDesc": req_devicedesc, + "location": { + "point": { + "center": { + "latitude": 40.75, + "longitude": -74 + }, + "orientation": 48, + "semiMajorAxis": 100, + "semiMinorAxis": 75 + } + }, + "type": "AVAIL_SPECTRUM_REQ", + "version": "1.0" + }, + expect_result={ + 'version': '1.0', + 'type': 'AVAIL_SPECTRUM_RESP', + }, + ) + + def test_paws_resp_structure(self): + afc_loc = self.guiconfig["afcconfig_defaults"] + LOGGER.debug("cookies: %s, token: %s", self.cookies, self.login_token) + resp = self.httpsession.get( + self._resolve_url(afc_loc), + headers={ + 'Authorization': self.login_token}, + cookies=self.cookies) + code = resp.status_code + LOGGER.debug("status: %d, url: %s", code, self._resolve_url(afc_loc)) + if code == 404: + raise SkipTest("AFC Config does not exist.") + req_devicedesc = { + "serialNumber": "sn-test", + "rulesetIds": ["AFC-6GHZ-DEMO-1.1"] + } + + response = self._call_jsonrpc( + self.paws_url, + method='spectrum.paws.getSpectrum', + params={ + "antenna": { + "height": 25, + "heightType": "AMSL", + "heightUncertainty": 5 + }, + "capabilities": { + "indoorOutdoor": "INDOOR" + }, + "deviceDesc": req_devicedesc, + "location": { + "point": { + "center": { + "latitude": 40.75, + "longitude": -74 + }, + "orientation": 80, + "semiMajorAxis": 500, + "semiMinorAxis": 400 + } + }, + "type": "AVAIL_SPECTRUM_REQ", + "version": "1.0" + }, + expect_result={ + 'version': '1.0', + 'type': 'AVAIL_SPECTRUM_RESP', + }, + ) + result = response['result'] + + self._test_paws_result_valid(result, req_devicedesc) diff --git a/http-checkout/rathttpcheckout/ratapi.py b/http-checkout/rathttpcheckout/ratapi.py new file mode 100644 index 0000000..aaf6362 --- /dev/null +++ b/http-checkout/rathttpcheckout/ratapi.py @@ -0,0 +1,161 @@ +''' Test cases related to PAWS API. +''' + +from urlparse import urljoin +from .base import (ValidateJsonResponse, ValidateHtmlResponse, BaseTestCase) + + +class TestRatApi(BaseTestCase): + ''' Test case to verify the RAT RESTful API. + ''' + + def setUp(self): + BaseTestCase.setUp(self) + + # Get the actual endpoint URL + resp = self.httpsession.head( + self._resolve_url(''), + allow_redirects=True + ) + self.assertIn(resp.status_code, (200,)) + index_url = resp.url + + self.guiconfig_url = urljoin(index_url, '../ratapi/v1/guiconfig') + resp = self.httpsession.get(self.guiconfig_url) + self.guiconfig = resp.json() + + def tearDown(self): + BaseTestCase.tearDown(self) + + def test_guiconfig_cache(self): + self._test_path_contents( + self.guiconfig_url, + must_authorize=False, + valid_encodings=None, + require_etag=None, + require_lastmod=None, + validate_response_post=ValidateJsonResponse(), + ) + + +class TestUlsDb(BaseTestCase): + ''' Test case to verify the ULS DB. + ''' + + def setUp(self): + BaseTestCase.setUp(self) + + # Get the actual endpoint URL + resp = self.httpsession.head( + self._resolve_url(''), + allow_redirects=True + ) + self.assertIn(resp.status_code, (200,)) + index_url = resp.url + + self.uls_db_url = urljoin(index_url, '../ratapi/v1/files/uls_db') + uls_resp = self.httpsession.get(self.uls_db_url) + self.uls_db = uls_resp + + self.uls_csv_to_sql = urljoin( + index_url, '../ratapi/v1/convert/uls/csv/sql/') + + def tearDown(self): + BaseTestCase.tearDown(self) + + def test_webdav(self): + self._test_path_contents( + self.uls_db_url, + must_authorize=False, + require_etag=False, + valid_encodings=None, + require_lastmod=False, + validate_response_post=ValidateHtmlResponse() + ) + + def test_links(self): + self._test_working_links(self.uls_db.text) + + def test_bad_file(self): + uls_db_url = self.uls_db_url + "/" + self._test_path_contents( + urljoin(uls_db_url, 'bad_file_name.csv'), + must_authorize=False, + require_etag=False, + valid_encodings=None, + require_lastmod=False, + valid_status=[404] + ) + + +class TestAntennaPattern(BaseTestCase): + ''' Test case to verify the Antenna Pattern. + ''' + + def setUp(self): + BaseTestCase.setUp(self) + + # Get the actual endpoint URL + resp = self.httpsession.head( + self._resolve_url(''), + allow_redirects=True + ) + self.assertIn(resp.status_code, (200,)) + index_url = resp.url + + self.antenna_url = urljoin( + index_url, '../ratapi/v1/files/antenna_pattern') + antenna_pattern = self.httpsession.get(self.antenna_url) + self.antenna_pattern = antenna_pattern + + def tearDown(self): + BaseTestCase.tearDown(self) + + def test_webdav(self): + self._test_path_contents( + self.antenna_url, + must_authorize=False, + require_etag=False, + valid_encodings=None, + require_lastmod=False, + validate_response_post=ValidateHtmlResponse() + ) + + def test_links(self): + self._test_working_links(self.antenna_pattern.text) + + +class TestHistory(BaseTestCase): + ''' Test case to verify the Histories. + ''' + + def setUp(self): + BaseTestCase.setUp(self) + + # Get the actual endpoint URL + resp = self.httpsession.head( + self._resolve_url(''), + allow_redirects=True + ) + self.assertIn(resp.status_code, (200,)) + index_url = resp.url + + self.history_url = urljoin(index_url, '../ratapi/v1/history') + history = self.httpsession.get(self.history_url) + self.history = history + + def tearDown(self): + BaseTestCase.tearDown(self) + + def test_webdav(self): + self._test_path_contents( + self.history_url, + must_authorize=False, + require_etag=False, + valid_encodings=None, + require_lastmod=False, + validate_response_post=ValidateHtmlResponse() + ) + + def test_links(self): + self._test_working_links(self.history.text) diff --git a/http-checkout/rathttpcheckout/www.py b/http-checkout/rathttpcheckout/www.py new file mode 100644 index 0000000..ee28587 --- /dev/null +++ b/http-checkout/rathttpcheckout/www.py @@ -0,0 +1,76 @@ +''' Test cases related to Web pages (not the APIs used by them). +''' + +import logging +from .base import (ValidateHtmlResponse, BaseTestCase) + +#: Logger for this module +LOGGER = logging.getLogger(__name__) + + +class TestWebApp(BaseTestCase): + ''' Test case to verify the PAWS JSON-RPC endpoint. + ''' + + def setUp(self): + BaseTestCase.setUp(self) + + def tearDown(self): + BaseTestCase.tearDown(self) + + def test_root_redirect(self): + resp = self.httpsession.head(self._resolve_url('')) + self.assertEqual(302, resp.status_code) + self.assertEqual(self._resolve_url('www/index.html'), + resp.headers.get('location')) + + def test_html_app(self): + self._test_path_contents( + self._resolve_url('www/index.html'), + valid_encodings=None, + content_type='text/html', + must_authorize=False, + require_etag=True, + require_lastmod=True, + require_cacheable=True, + validate_response_post=ValidateHtmlResponse(), + ) + + def test_guiconfig(self): + required_keys = frozenset([ + 'afcconfig_defaults', + 'uls_convert_url', + 'login_url', + 'paws_url', + 'history_url', + 'admin_url', + 'user_url', + 'ap_deny_admin_url', + 'rat_api_analysis', + 'version', + 'antenna_url', + 'google_apikey', + 'uls_url' + ]) + resp = self.httpsession.get(self._resolve_url('ratapi/v1/guiconfig')) + encoding = resp.headers["Content-Type"] + self.assertEqual("application/json", encoding) + try: + parsed_body = resp.json() + except ValueError: + self.fail("Body is not valid JSON.") + + missing_keys = required_keys - frozenset(parsed_body.keys()) + if missing_keys: + self.fail('Required keys: {0}'.format(' '.join(required_keys))) + + non_200_eps = {} + for value in parsed_body.values(): + resp = self.httpsession.options(self._resolve_url(value)) + LOGGER.debug("Verifying status of %s", self._resolve_url(value)) + if resp.status_code != 200: + non_200_eps[value] = resp.status_code + self.assertEqual( + {}, + non_200_eps, + msg="{}, were defined in GUI config as required endpoint(s) but returned non-200 status on OPTIONS".format(non_200_eps)) diff --git a/infra/afc/Chart.yaml b/infra/afc/Chart.yaml new file mode 100644 index 0000000..6681207 --- /dev/null +++ b/infra/afc/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: afc +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.3.1 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "3.8.15.0" diff --git a/infra/afc/config b/infra/afc/config new file mode 100644 index 0000000..15e50e7 --- /dev/null +++ b/infra/afc/config @@ -0,0 +1,3 @@ +nonMasqueradeCIDRs: +- 240.0.0.0/4 +resyncInterval: 60s \ No newline at end of file diff --git a/infra/afc/templates/NOTES.txt b/infra/afc/templates/NOTES.txt new file mode 100644 index 0000000..a626277 --- /dev/null +++ b/infra/afc/templates/NOTES.txt @@ -0,0 +1,38 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else }} + + {{- if eq .Values.service.msghnd.type "LoadBalancer" }} + echo "Fetching LoadBalancer IP for msghnd..." + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ .Chart.Name }}-{{ .Values.service.msghnd.hostname }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.msghnd.port }} + {{- end }} + + {{- if eq .Values.service.webui.type "LoadBalancer" }} + echo "Fetching LoadBalancer IP for webui..." + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ .Chart.Name }}-{{ .Values.service.webui.hostname }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.webui.port }} + {{- end }} + + {{- if eq .Values.service.objst.type "ClusterIP" }} + echo "Fetching ClusterIP for objst..." + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "afc.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:{{ .Values.service.objst.fileStoragePort }} to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:{{ .Values.service.objst.fileStoragePort }} + {{- end }} + + {{- if eq .Values.service.rmq.type "ClusterIP" }} + echo "Fetching ClusterIP for rmq..." + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "afc.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:{{ .Values.service.rmq.port }} to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:{{ .Values.service.rmq.port }} + {{- end }} + +{{- end }} diff --git a/infra/afc/templates/_helpers.tpl b/infra/afc/templates/_helpers.tpl new file mode 100644 index 0000000..713dbb9 --- /dev/null +++ b/infra/afc/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "afc.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "afc.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "afc.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "afc.labels" -}} +helm.sh/chart: {{ include "afc.chart" . }} +{{ include "afc.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "afc.selectorLabels" -}} +app.kubernetes.io/name: {{ include "afc.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "afc.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "afc.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/infra/afc/templates/deployment-als-kafka.yaml b/infra/afc/templates/deployment-als-kafka.yaml new file mode 100644 index 0000000..871d054 --- /dev/null +++ b/infra/afc/templates/deployment-als-kafka.yaml @@ -0,0 +1,71 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }}-{{ .Values.service.als_kafka.hostname }} + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount.als_kafka }} + {{- end }} + selector: + matchLabels: + {{- include "afc.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "afc.selectorLabels" . | nindent 8 }} + afc: als-kafka + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "afc.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }}-{{ .Values.service.als_kafka.hostname }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.als_kafka.repository }}:{{ .Values.image.als_kafka.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.als_kafka.pullPolicy }} + ports: + - name: als-kafka-port + containerPort: {{ .Values.service.als_kafka.port | int }} + protocol: TCP + # livenessProbe: + # httpGet: + # path: /fbrat/www/index.html + # port: http + # readinessProbe: + # httpGet: + # path: / + # port: http + env: + - name: KAFKA_ADVERTISED_HOST + value: {{ .Values.service.als_kafka.hostname | quote }} + - name: KAFKA_CLIENT_PORT + value: {{ .Values.service.als_kafka.port | quote }} + - name: KAFKA_MAX_REQUEST_SIZE + value: {{ .Values.service.als_kafka.max_request_size | quote | replace ":" "" }} + resources: + {{- toYaml .Values.resources.als_kafka | nindent 12 }} + imagePullSecrets: + - name: container-repo-secret + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/infra/afc/templates/deployment-als-siphon.yaml b/infra/afc/templates/deployment-als-siphon.yaml new file mode 100644 index 0000000..aa37125 --- /dev/null +++ b/infra/afc/templates/deployment-als-siphon.yaml @@ -0,0 +1,94 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }}-{{ .Values.deployments.als_siphon.name }} + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount.als_siphon }} + {{- end }} + selector: + matchLabels: + {{- include "afc.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "afc.selectorLabels" . | nindent 8 }} + afc: als-siphon + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "afc.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }}-{{ .Values.deployments.als_siphon.name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.als_siphon.repository }}:{{ .Values.image.als_siphon.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.als_siphon.pullPolicy }} + env: + - name: KAFKA_SERVERS + value: "{{ .Values.service.als_kafka.hostname }}:{{ .Values.service.als_kafka.port }}" + - name: POSTGRES_HOST + valueFrom: + secretKeyRef: + name: {{ .Chart.Name }}-postgres-secret + key: POSTGRES_HOST + - name: INIT_IF_EXISTS + value: {{ .Values.deployments.als_siphon.init_if_exists | quote }} + - name: KAFKA_MAX_REQUEST_SIZE + value: {{ .Values.service.als_kafka.max_request_size | quote | replace ":" "" }} + - name: POSTGRES_INIT_USER + valueFrom: + secretKeyRef: + name: {{ .Chart.Name }}-postgres-secret + key: POSTGRES_USER + - name: POSTGRES_INIT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Chart.Name }}-postgres-secret + key: POSTGRES_PASSWORD + - name: POSTGRES_ALS_USER + valueFrom: + secretKeyRef: + name: {{ .Chart.Name }}-postgres-secret + key: POSTGRES_USER + - name: POSTGRES_ALS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Chart.Name }}-postgres-secret + key: POSTGRES_PASSWORD + - name: POSTGRES_LOG_USER + valueFrom: + secretKeyRef: + name: {{ .Chart.Name }}-postgres-secret + key: POSTGRES_USER + - name: POSTGRES_LOG_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Chart.Name }}-postgres-secret + key: POSTGRES_PASSWORD + resources: + {{- toYaml .Values.resources.als_siphon | nindent 12 }} + imagePullSecrets: + - name: container-repo-secret + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/infra/afc/templates/deployment-msghnd.yaml b/infra/afc/templates/deployment-msghnd.yaml new file mode 100644 index 0000000..90a62a6 --- /dev/null +++ b/infra/afc/templates/deployment-msghnd.yaml @@ -0,0 +1,110 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }}-{{ .Values.service.msghnd.hostname }} + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount.msghnd }} + {{- end }} + selector: + matchLabels: + {{- include "afc.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "afc.selectorLabels" . | nindent 8 }} + afc: msghnd + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "afc.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }}-{{ .Values.service.msghnd.hostname }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.msghnd.repository }}:{{ .Values.image.msghnd.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.msghnd.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.msghnd.containerPort | int }} + protocol: TCP + # livenessProbe: + # httpGet: + # path: /fbrat/www/index.html + # port: http + # readinessProbe: + # httpGet: + # path: / + # port: http + volumeMounts: + - name: {{ .Chart.Name }}-msghnd-rat-api-secret + mountPath: /etc/xdg/fbrat/ratapi.conf + subPath: ratapi.conf + env: + # RabbitMQ server name: + - name: BROKER_TYPE + value: "external" + - name: BROKER_FQDN + value: {{ .Values.service.rmq.hostname | quote }} + # Filestorage params: + - name: AFC_OBJST_HOST + value: {{ .Values.service.objst.hostname | quote }} + - name: AFC_OBJST_PORT + value: {{ .Values.service.objst.fileStoragePort | quote }} + - name: AFC_OBJST_SCHEME + value: {{ .Values.service.objst.scheme | quote }} + # ALS params + - name: ALS_KAFKA_SERVER_ID + value: {{ .Values.service.msghnd.hostname | quote }} + - name: ALS_KAFKA_CLIENT_BOOTSTRAP_SERVERS + value: "{{ .Values.service.als_kafka.hostname }}:{{ .Values.service.als_kafka.port }}" + - name: ALS_KAFKA_MAX_REQUEST_SIZE + value: {{ .Values.service.als_kafka.max_request_size | quote | replace ":" "" }} + # Rcache parameters + - name: RCACHE_ENABLED + value: {{ .Values.service.rcache.is_enabled | quote }} + - name: RCACHE_POSTGRES_DSN + valueFrom: + secretKeyRef: + name: {{ .Chart.Name }}-postgres-secret + key: RCACHE_POSTGRES_DSN + - name: RCACHE_SERVICE_URL + value: "http://{{ .Values.service.rcache.hostname }}:{{ .Values.service.rcache.port }}" + - name: RCACHE_RMQ_DSN + valueFrom: + secretKeyRef: + name: {{ .Chart.Name }}-rmq-rcache-secret + key: RCACHE_RMQ_DSN + # own msghnd parameters + - name: AFC_MSGHND_WORKERS + value: {{ .Values.service.msghnd.threads_per_pod | quote }} + resources: + {{- toYaml .Values.resources.msghnd | nindent 12 }} + volumes: + - name: {{ .Chart.Name }}-msghnd-rat-api-secret + secret: + secretName: {{ .Chart.Name }}-msghnd-rat-api-secret + imagePullSecrets: + - name: container-repo-secret + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/infra/afc/templates/deployment-objst.yaml b/infra/afc/templates/deployment-objst.yaml new file mode 100644 index 0000000..65f5f73 --- /dev/null +++ b/infra/afc/templates/deployment-objst.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }}-{{ .Values.service.objst.hostname }} + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount.objst }} + {{- end }} + selector: + matchLabels: + {{- include "afc.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "afc.selectorLabels" . | nindent 8 }} + afc: objst + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "afc.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }}-{{ .Values.service.objst.hostname }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.objst.repository }}:{{ .Values.image.objst.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.objst.pullPolicy }} + ports: + - name: afc-objst-port + containerPort: {{ .Values.service.objst.fileStoragePort }} + protocol: TCP + - name: afc-objst-hist + containerPort: {{ .Values.service.objst.historyViewPort }} + protocol: TCP + volumeMounts: + - mountPath: {{ .Values.deployments.global.mountPath | quote }} + name: cont-confs + env: + # Filestorage params: + - name: AFC_OBJST_PORT + value: {{ .Values.service.objst.fileStoragePort | quote }} + - name: AFC_OBJST_HIST_PORT + value: {{ .Values.service.objst.historyViewPort | quote }} + - name: AFC_OBJST_LOCAL_DIR + value: "{{ .Values.deployments.global.mountPath }}/storage" + resources: + {{- toYaml .Values.resources.objst | nindent 12 }} + volumes: + - name: cont-confs + persistentVolumeClaim: + claimName: cont-confs-claim + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/infra/afc/templates/deployment-rcache.yaml b/infra/afc/templates/deployment-rcache.yaml new file mode 100644 index 0000000..64bb8a1 --- /dev/null +++ b/infra/afc/templates/deployment-rcache.yaml @@ -0,0 +1,81 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }}-{{ .Values.service.rcache.hostname }} + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount.rcache }} + {{- end }} + selector: + matchLabels: + {{- include "afc.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "afc.selectorLabels" . | nindent 8 }} + afc: rcache + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "afc.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }}-{{ .Values.service.rcache.hostname }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.rcache.repository }}:{{ .Values.image.rcache.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.rcache.pullPolicy }} + ports: + - name: rcache-port + containerPort: {{ .Values.service.rcache.port | int }} + protocol: TCP + # livenessProbe: + # httpGet: + # path: /fbrat/www/index.html + # port: http + # readinessProbe: + # httpGet: + # path: / + # port: http + env: + # R-Cache params + - name: RCACHE_ENABLED + value: {{ .Values.service.rcache.is_enabled | quote }} + - name: RCACHE_CLIENT_PORT + value: {{ .Values.service.rcache.port | quote }} + - name: RCACHE_POSTGRES_DSN + valueFrom: + secretKeyRef: + name: {{ .Chart.Name }}-postgres-secret + key: RCACHE_POSTGRES_DSN + - name: RCACHE_AFC_REQ_URL + value: "http://{{ .Values.service.msghnd.hostname }}:{{ .Values.service.msghnd.port }}/fbrat/ap-afc/availableSpectrumInquiry?nocache=True" + - name: RCACHE_RULESETS_URL + value: "http://{{ .Values.service.msghnd.hostname }}/fbrat/ratapi/v1/GetRulesetIDs" + - name: RCACHE_CONFIG_RETRIEVAL_URL + value: "http://{{ .Values.service.msghnd.hostname }}/fbrat/ratapi/v1/GetAfcConfigByRulesetID" + resources: + {{- toYaml .Values.resources.rcache | nindent 12 }} + imagePullSecrets: + - name: container-repo-secret + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/infra/afc/templates/deployment-rmq.yaml b/infra/afc/templates/deployment-rmq.yaml new file mode 100644 index 0000000..0e083d5 --- /dev/null +++ b/infra/afc/templates/deployment-rmq.yaml @@ -0,0 +1,62 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }}-{{ .Values.service.rmq.hostname }} + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount.rmq }} + {{- end }} + selector: + matchLabels: + {{- include "afc.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "afc.selectorLabels" . | nindent 8 }} + afc: rmq + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "afc.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }}-{{ .Values.service.rmq.hostname }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.rmq.repository }}:{{ .Values.image.rmq.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.rmq.pullPolicy }} + ports: + - name: rmqp + containerPort: {{ .Values.service.rmq.port }} + protocol: TCP + # livenessProbe: + # httpGet: + # path: / + # port: rmqp + # readinessProbe: + # httpGet: + # path: / + # port: rmqp + resources: + {{- toYaml .Values.resources.rmq | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/infra/afc/templates/deployment-webui.yml b/infra/afc/templates/deployment-webui.yml new file mode 100644 index 0000000..561233a --- /dev/null +++ b/infra/afc/templates/deployment-webui.yml @@ -0,0 +1,112 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }}-{{ .Values.service.webui.hostname }} + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount.webui }} + {{- end }} + selector: + matchLabels: + {{- include "afc.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "afc.selectorLabels" . | nindent 8 }} + afc: webui + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "afc.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }}-{{ .Values.service.webui.hostname }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.webui.repository }}:{{ .Values.image.webui.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.webui.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.webui.port }} + protocol: TCP + # livenessProbe: + # httpGet: + # path: /fbrat/www/index.html + # port: http + # readinessProbe: + # httpGet: + # path: / + # port: http + volumeMounts: + - mountPath: {{ .Values.deployments.global.mountPath | quote }} + name: cont-confs + - name: {{ .Chart.Name }}-webui-rat-api-secret + mountPath: /etc/xdg/fbrat/ratapi.conf + subPath: ratapi.conf + env: + # RabbitMQ server name: + - name: BROKER_TYPE + value: "external" + - name: BROKER_FQDN + value: {{ .Values.service.rmq.hostname | quote }} + # Filestorage params: + - name: AFC_OBJST_HOST + value: {{ .Values.service.objst.hostname | quote }} + - name: AFC_OBJST_PORT + value: {{ .Values.service.objst.fileStoragePort | quote }} + - name: AFC_OBJST_SCHEME + value: {{ .Values.service.objst.scheme | quote }} + # ALS params + - name: ALS_KAFKA_SERVER_ID + value: {{ .Values.service.webui.hostname | quote }} + - name: ALS_KAFKA_CLIENT_BOOTSTRAP_SERVERS + value: "{{ .Values.service.als_kafka.hostname }}:{{ .Values.service.als_kafka.port }}" + - name: ALS_KAFKA_MAX_REQUEST_SIZE + value: {{ .Values.service.als_kafka.max_request_size | quote | replace ":" "" }} + # Rcache parameters + - name: RCACHE_ENABLED + value: {{ .Values.service.rcache.is_enabled | quote }} + - name: RCACHE_POSTGRES_DSN + valueFrom: + secretKeyRef: + name: {{ .Chart.Name }}-postgres-secret + key: RCACHE_POSTGRES_DSN + - name: RCACHE_SERVICE_URL + value: "http://{{ .Values.service.rcache.hostname }}:{{ .Values.service.rcache.port }}" + - name: RCACHE_RMQ_DSN + valueFrom: + secretKeyRef: + name: {{ .Chart.Name }}-rmq-rcache-secret + key: RCACHE_RMQ_DSN + resources: + {{- toYaml .Values.resources.webui | nindent 12 }} + volumes: + - name: cont-confs + persistentVolumeClaim: + claimName: cont-confs-claim + - name: {{ .Chart.Name }}-webui-rat-api-secret + secret: + secretName: {{ .Chart.Name }}-webui-rat-api-secret + imagePullSecrets: + - name: container-repo-secret + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/infra/afc/templates/deployment-worker.yaml b/infra/afc/templates/deployment-worker.yaml new file mode 100644 index 0000000..f151362 --- /dev/null +++ b/infra/afc/templates/deployment-worker.yaml @@ -0,0 +1,111 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }}-{{ .Values.deployments.worker.name }} + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount.worker }} + {{- end }} + selector: + matchLabels: + {{- include "afc.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "afc.selectorLabels" . | nindent 8 }} + afc: worker + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "afc.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }}-{{ .Values.deployments.worker.name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.worker.repository }}:{{ .Values.image.worker.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.worker.pullPolicy }} + # livenessProbe: + # httpGet: + # path: / + # port: rmqp + # readinessProbe: + # httpGet: + # path: / + # port: rmqp + volumeMounts: + - mountPath: {{ .Values.deployments.global.mountPath | quote }} + name: cont-confs + env: + # Filestorage params: + - name: AFC_OBJST_HOST + value: {{ .Values.service.objst.hostname | quote }} + - name: AFC_OBJST_PORT + value: {{ .Values.service.objst.fileStoragePort | quote }} + - name: AFC_OBJST_SCHEME + value: {{ .Values.service.objst.scheme | quote }} + # celery params + - name: AFC_WORKER_CELERY_WORKERS + value: "rat_worker" + - name: AFC_WORKER_CELERY_OPTS + value: "" + - name: AFC_WORKER_CELERY_CONCURRENCY + value: {{ .Values.deployments.worker.celery_concurrency | quote }} + # RabbitMQ server name: + - name: BROKER_TYPE + value: "external" + - name: BROKER_FQDN + value: {{ .Values.service.rmq.hostname | quote }} + # afc-engine preload lib params + - name: AFC_AEP_ENABLE + value: {{ .Values.deployments.worker.afc_aep_enable | quote }} + - name: AFC_AEP_DEBUG + value: {{ .Values.deployments.worker.afc_aep_debug | quote }} + - name: AFC_AEP_REAL_MOUNTPOINT + value: "{{ .Values.deployments.global.mountPath }}/{{ .Values.deployments.worker.afc_aep_real_mountpoint_relative}}" + # Rcache parameters + - name: RCACHE_ENABLED + value: {{ .Values.service.rcache.is_enabled | quote }} + - name: RCACHE_SERVICE_URL + value: "http://{{ .Values.service.rcache.hostname }}:{{ .Values.service.rcache.port }}" + - name: RCACHE_RMQ_DSN + valueFrom: + secretKeyRef: + name: {{ .Chart.Name }}-rmq-rcache-secret + key: RCACHE_RMQ_DSN + # ALS params + - name: ALS_KAFKA_SERVER_ID + value: {{ .Values.deployments.worker.name | quote }} + - name: ALS_KAFKA_CLIENT_BOOTSTRAP_SERVERS + value: "{{ .Values.service.als_kafka.hostname }}:{{ .Values.service.als_kafka.port }}" + - name: ALS_KAFKA_MAX_REQUEST_SIZE + value: {{ .Values.service.als_kafka.max_request_size | quote | replace ":" "" }} + resources: + {{- toYaml .Values.resources.worker | nindent 12 }} + volumes: + - name: cont-confs + persistentVolumeClaim: + claimName: cont-confs-claim + imagePullSecrets: + - name: container-repo-secret + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/infra/afc/templates/hpa.yaml b/infra/afc/templates/hpa.yaml new file mode 100644 index 0000000..8ddcc0f --- /dev/null +++ b/infra/afc/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "afc.fullname" . }} + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "afc.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/infra/afc/templates/ingress-nginx-int.yaml b/infra/afc/templates/ingress-nginx-int.yaml new file mode 100644 index 0000000..ed8771c --- /dev/null +++ b/infra/afc/templates/ingress-nginx-int.yaml @@ -0,0 +1,89 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: afc-ingress + annotations: + # Use annotations to configure specific ingress-nginx behaviors like SSL, timeouts, etc. + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/configuration-snippet: | + if ($request_uri = "/") { + return 301 $scheme://$http_host/fbrat; + } + # Add other necessary annotations based on your specific requirements. +spec: + ingressClassName: nginx + rules: + #- host: {{ .Values.service.ingress_ngnix.hostname | quote }} + - http: + paths: + # should be behind auth or mTLS + - path: /fbrat/ap-afc/availableSpectrumInquirySec + pathType: Prefix + backend: + service: + name: {{ .Values.service.webui.hostname | quote }} + port: + number: {{ .Values.service.webui.port }} + # should be behind auth or mTLS + - path: /fbrat/ap-afc/availableSpectrumInquiry + pathType: Prefix + backend: + service: + name: {{ .Values.service.msghnd.hostname | quote }} + port: + number: {{ .Values.service.msghnd.port }} + # should be accessible only from internal network + # + # - path: /fbrat/ap-afc/availableSpectrumInquiryInternal + # pathType: Prefix + # backend: + # service: + # name: {{ .Values.service.msghnd.hostname | quote }} + # port: + # number: {{ .Values.service.msghnd.port }} + # + # -------------------------------------------------------------------- + # need to forbid webdav methods other than GET + # + # - path: /fbrat/ratapi/v1/files + # pathType: Prefix + # backend: + # service: + # name: {{ .Values.service.webui.hostname | quote }} + # port: + # number: {{ .Values.service.webui.port }} + # + # -------------------------------------------------------------------- + # should be accessible only from internal network + # + # - path: /fbrat/ratapi/v1/GetAfcConfigByRulesetID + # pathType: Prefix + # backend: + # service: + # name: {{ .Values.service.msghnd.hostname | quote }} + # port: + # number: {{ .Values.service.msghnd.port }} + # + # -------------------------------------------------------------------- + # should be accessible only from internal network + # + # - path: /fbrat/ratapi/v1/GetRulesetIDs + # pathType: Prefix + # backend: + # service: + # name: {{ .Values.service.msghnd.hostname | quote }} + # port: + # number: {{ .Values.service.msghnd.port }} + - path: / + pathType: Prefix + backend: + service: + name: {{ .Values.service.webui.hostname | quote }} + port: + number: {{ .Values.service.webui.port }} + # Add other paths as needed. + # Add TLS configuration if you're using HTTPS. +# tls: +# - hosts: +# - {{ .Values.service.ingress_ngnix.hostname | quote }} +# secretName: your-tls-secret diff --git a/infra/afc/templates/ingress.yaml b/infra/afc/templates/ingress.yaml new file mode 100644 index 0000000..1fd7adc --- /dev/null +++ b/infra/afc/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "afc.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "afc.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/infra/afc/templates/msghnd.ratapi.secret.yaml.example b/infra/afc/templates/msghnd.ratapi.secret.yaml.example new file mode 100644 index 0000000..795c621 --- /dev/null +++ b/infra/afc/templates/msghnd.ratapi.secret.yaml.example @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Chart.Name }}-msghnd-rat-api-secret +type: Opaque +stringData: + ratapi.conf: | + # Flask settings + DEBUG = False + PROPAGATE_EXCEPTIONS = False + LOG_LEVEL = 'WARNING' + SECRET_KEY = 'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGH' + + # Flask-SQLAlchemy settings + SQLALCHEMY_DATABASE_URI = 'postgresql://postgres_user:psql_password@psql_hostname/vhost_name' + + # Flask-User settings + USER_EMAIL_SENDER_EMAIL = 'admin@example.com' + + # RAT settings + GOOGLE_APIKEY = 'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLM' + HISTORY_DIR = '/mnt/nfs/rat_transfer/history' + DEFAULT_ULS_DIR = '/mnt/nfs/rat_transfer/ULS_Database' + AFC_APP_TYPE = 'msghnd' \ No newline at end of file diff --git a/infra/afc/templates/postgres-rcache.secret.yaml.example b/infra/afc/templates/postgres-rcache.secret.yaml.example new file mode 100644 index 0000000..148c30b --- /dev/null +++ b/infra/afc/templates/postgres-rcache.secret.yaml.example @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Chart.Name }}-postgres-secret +type: Opaque +data: + # base64 encoded postgresql DSN for rcache user + RCACHE_POSTGRES_DSN: cG9zdGdyZXNxbDovL3JjYWNoZV9wb3N0Z3Jlc191c2VyOnBzcWxfcGFzc3dvcmRAcHNxbF9ob3N0bmFtZS92aG9zdF9uYW1l + # base64 encoded postgresql hostname + POSTGRES_HOST: cHNxbF9ob3N0bmFtZQ== + # base64 encoded postgresql username + POSTGRES_USER: cG9zdGdyZXNfdXNlcg== + # base64 encoded postgresql password + POSTGRES_PASSWORD: cG9zdGdyZXNfdXNlcl9zX3BzcWxfcGFzc3dvcmQ= \ No newline at end of file diff --git a/infra/afc/templates/rmq-rcache.secret.yaml.example b/infra/afc/templates/rmq-rcache.secret.yaml.example new file mode 100644 index 0000000..f6eff32 --- /dev/null +++ b/infra/afc/templates/rmq-rcache.secret.yaml.example @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Chart.Name }}-rmq-rcache-secret +type: Opaque +data: + # base64 encoded amqp connection string for rcache + RCACHE_RMQ_DSN: YW1xcDovL3JjYWNoZV91c2VyOnBhc3N3b3JkQHJhYmJpdF9tcV9ob3N0OjU2NzIvaG9zdA== \ No newline at end of file diff --git a/infra/afc/templates/scaledobject-worker.yaml b/infra/afc/templates/scaledobject-worker.yaml new file mode 100644 index 0000000..f80b557 --- /dev/null +++ b/infra/afc/templates/scaledobject-worker.yaml @@ -0,0 +1,19 @@ +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{ .Chart.Name }}-{{ .Values.deployments.worker.name }}-so + namespace: default +spec: + scaleTargetRef: + name: {{ .Chart.Name }}-{{ .Values.deployments.worker.name }} + minReplicaCount: 2 + pollingInterval: 5 # Optional. Default: 30 seconds + cooldownPeriod: 300 # Optional. Default: 300 seconds + triggers: + - type: rabbitmq + metadata: + queueName: celery + mode: QueueLength + value: {{ .Values.deployments.worker.queue_length | quote }} + authenticationRef: + name: {{ .Chart.Name }}-{{ .Values.deployments.worker.name }}-trigger diff --git a/infra/afc/templates/service-als-kafka.yaml b/infra/afc/templates/service-als-kafka.yaml new file mode 100644 index 0000000..d34a82f --- /dev/null +++ b/infra/afc/templates/service-als-kafka.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.service.als_kafka.hostname | quote }} + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.als_kafka.type }} + ports: + - name: als-kafka-port + port: {{ .Values.service.als_kafka.port }} + protocol: TCP + targetPort: als-kafka-port + selector: + {{- include "afc.selectorLabels" . | nindent 4 }} + afc: als-kafka diff --git a/infra/afc/templates/service-msghnd.yaml b/infra/afc/templates/service-msghnd.yaml new file mode 100644 index 0000000..a9f1284 --- /dev/null +++ b/infra/afc/templates/service-msghnd.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.service.msghnd.hostname | quote }} + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.msghnd.type }} + ports: + - port: {{ .Values.service.msghnd.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "afc.selectorLabels" . | nindent 4 }} + afc: msghnd diff --git a/infra/afc/templates/service-objst.yaml b/infra/afc/templates/service-objst.yaml new file mode 100644 index 0000000..14f52e7 --- /dev/null +++ b/infra/afc/templates/service-objst.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.service.objst.hostname | quote }} + annotations: + cloud.google.com/load-balancer-type: "Internal" + networking.gke.io/internal-load-balancer-allow-global-access: "true" + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.objst.type }} + ports: + - port: {{ .Values.service.objst.fileStoragePort }} + targetPort: afc-objst-port + protocol: TCP + name: afc-objst-port + - port: {{ .Values.service.objst.historyViewPort }} + targetPort: afc-objst-hist + protocol: TCP + name: afc-objst-hist + selector: + {{- include "afc.selectorLabels" . | nindent 4 }} + afc: objst diff --git a/infra/afc/templates/service-rcache.yaml b/infra/afc/templates/service-rcache.yaml new file mode 100644 index 0000000..8b8e066 --- /dev/null +++ b/infra/afc/templates/service-rcache.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.service.rcache.hostname | quote }} + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.rcache.type }} + ports: + - name: rcache-port + port: {{ .Values.service.rcache.port }} + protocol: TCP + targetPort: rcache-port + selector: + {{- include "afc.selectorLabels" . | nindent 4 }} + afc: rcache diff --git a/infra/afc/templates/service-rmq.yaml b/infra/afc/templates/service-rmq.yaml new file mode 100644 index 0000000..fab2d49 --- /dev/null +++ b/infra/afc/templates/service-rmq.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.service.rmq.hostname | quote }} + annotations: + cloud.google.com/load-balancer-type: "Internal" + networking.gke.io/internal-load-balancer-allow-global-access: "true" + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.rmq.type }} + ports: + - port: {{ .Values.service.rmq.port }} + targetPort: rmqp + protocol: TCP + name: rmqp + selector: + {{- include "afc.selectorLabels" . | nindent 4 }} + afc: rmq diff --git a/infra/afc/templates/service-webui.yml b/infra/afc/templates/service-webui.yml new file mode 100644 index 0000000..1cca536 --- /dev/null +++ b/infra/afc/templates/service-webui.yml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.service.webui.hostname | quote }} + labels: + {{- include "afc.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.webui.type }} + ports: + - port: {{ .Values.service.webui.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "afc.selectorLabels" . | nindent 4 }} + afc: webui diff --git a/infra/afc/templates/serviceaccount.yaml b/infra/afc/templates/serviceaccount.yaml new file mode 100644 index 0000000..f783062 --- /dev/null +++ b/infra/afc/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "afc.serviceAccountName" . }} + labels: + {{- include "afc.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/infra/afc/templates/storageclass-b-ssd.yaml b/infra/afc/templates/storageclass-b-ssd.yaml new file mode 100644 index 0000000..02324ac --- /dev/null +++ b/infra/afc/templates/storageclass-b-ssd.yaml @@ -0,0 +1,11 @@ +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: b-ssd +provisioner: filestore.csi.storage.gke.io +reclaimPolicy: Retain +volumeBindingMode: Immediate +allowVolumeExpansion: false +parameters: + tier: BASIC_SSD + connect-mode: PRIVATE_SERVICE_ACCESS diff --git a/infra/afc/templates/tests/test-connection.yaml b/infra/afc/templates/tests/test-connection.yaml new file mode 100644 index 0000000..fa9291d --- /dev/null +++ b/infra/afc/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "afc.fullname" . }}-test-connection" + labels: + {{- include "afc.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "afc.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/infra/afc/templates/trigger-auth-worker.yaml b/infra/afc/templates/trigger-auth-worker.yaml new file mode 100644 index 0000000..38780c7 --- /dev/null +++ b/infra/afc/templates/trigger-auth-worker.yaml @@ -0,0 +1,10 @@ +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{ .Chart.Name }}-{{ .Values.deployments.worker.name }}-trigger + namespace: default +spec: + secretTargetRef: + - parameter: host + name: {{ .Chart.Name }}-rabbitmq-consumer-secret + key: RabbitMqHost \ No newline at end of file diff --git a/infra/afc/templates/vol-claim-afc-engine.yaml b/infra/afc/templates/vol-claim-afc-engine.yaml new file mode 100644 index 0000000..16eaa03 --- /dev/null +++ b/infra/afc/templates/vol-claim-afc-engine.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: cont-confs-claim +spec: + accessModes: + - ReadWriteMany + storageClassName: b-ssd + resources: + requests: + storage: 2.5Ti \ No newline at end of file diff --git a/infra/afc/templates/webui.ratapi.secret.yaml.example b/infra/afc/templates/webui.ratapi.secret.yaml.example new file mode 100644 index 0000000..2a09c3f --- /dev/null +++ b/infra/afc/templates/webui.ratapi.secret.yaml.example @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Chart.Name }}-webui-rat-api-secret +type: Opaque +stringData: + ratapi.conf: | + # Flask settings + DEBUG = False + PROPAGATE_EXCEPTIONS = False + LOG_LEVEL = 'WARNING' + SECRET_KEY = 'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGH' + + # Flask-SQLAlchemy settings + SQLALCHEMY_DATABASE_URI = 'postgresql://postgres_user:psql_password@psql_hostname/vhost_name' + + # Flask-User settings + USER_EMAIL_SENDER_EMAIL = 'admin@example.com' + + # RAT settings + GOOGLE_APIKEY = 'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLM' + HISTORY_DIR = '/mnt/nfs/rat_transfer/history' + DEFAULT_ULS_DIR = '/mnt/nfs/rat_transfer/ULS_Database' + AFC_APP_TYPE = 'server' \ No newline at end of file diff --git a/infra/afc/templates/worker.scale-trigger.secret.yaml.example b/infra/afc/templates/worker.scale-trigger.secret.yaml.example new file mode 100644 index 0000000..7208355 --- /dev/null +++ b/infra/afc/templates/worker.scale-trigger.secret.yaml.example @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Chart.Name }}-rabbitmq-consumer-secret +data: + # base64 encoded amqp connection string + RabbitMqHost: YW1xcDovL3VzZXI6cGFzc3dvcmRAcmFiYml0X21xX2hvc3Q6NTY3Mi9ob3N0 diff --git a/infra/afc/values.yaml b/infra/afc/values.yaml new file mode 100644 index 0000000..a86cc98 --- /dev/null +++ b/infra/afc/values.yaml @@ -0,0 +1,193 @@ +# Default values for afc. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: + msghnd: 1 + objst: 1 + rmq: 1 + worker: 1 + webui: 1 + rcache: 1 + als_kafka: 1 + als_siphon: 1 + + +image: + msghnd: + repository: 110738915961.dkr.ecr.us-east-1.amazonaws.com/afc-msghnd + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + #tag: "3.8.15.0" + webui: + repository: 110738915961.dkr.ecr.us-east-1.amazonaws.com/afc-server + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + #tag: "3.8.15.0" + objst: + repository: public.ecr.aws/w9v6y1o0/openafc/objstorage-image + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + #tag: "3.8.15.0" + worker: + repository: 110738915961.dkr.ecr.us-east-1.amazonaws.com/afc-worker + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + #tag: "3.8.15.0" + rmq: + repository: public.ecr.aws/w9v6y1o0/openafc/rmq-image + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + #tag: "3.8.15.0" + rcache: + repository: public.ecr.aws/w9v6y1o0/openafc/rcache-image + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + #tag: "3.8.15.0" + als_kafka: + repository: public.ecr.aws/w9v6y1o0/openafc/als-kafka-image + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + #tag: "3.8.15.0" + als_siphon: + repository: public.ecr.aws/w9v6y1o0/openafc/als-siphon-image + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + #tag: "3.8.15.0" + + +imagePullSecrets: [] +nameOverride: "afc-app" +#fullnameOverride: "afc-chart" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + msghnd: + hostname: msghnd + type: ClusterIP + port: 80 + containerPort: 8000 + threads_per_pod: 2 + webui: + hostname: webui + type: ClusterIP + port: 80 + rmq: + hostname: rmq + type: ClusterIP + port: 5672 + objst: + hostname: objst + type: ClusterIP + fileStoragePort: 5000 + historyViewPort: 4999 + scheme: "HTTP" + als_kafka: + hostname: als-kafka + type: ClusterIP + port: 9092 + max_request_size: ":10485760" # ":" is a part of workaroud of this bug in helm https://github.com/helm/helm/issues/1707 + rcache: + hostname: rcache + type: ClusterIP + port: 8000 + is_enabled: "TRUE" + ingress_ngnix: + hostname: "" + +deployments: + global: + mountPath: "/mnt/nfs" + als_siphon: + name: als-siphon + init_if_exists: "skip" + worker: + name: worker + afc_aep_enable: "1" + afc_aep_debug: "1" + afc_aep_real_mountpoint_relative: "rat_transfer/3dep/1_arcsec" + celery_concurrency: 2 + queue_length: 3 + + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + msghnd: + requests: + memory: 1200Mi + objst: + requests: + memory: 500Mi + rmq: + requests: + memory: 200Mi + worker: + requests: + memory: 4500Mi + webui: + requests: + memory: 200Mi + rcache: + requests: + memory: 100Mi + als_kafka: + requests: + memory: 500Mi + als_siphon: + requests: + memory: 100Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 1 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/infra/deploy_afc.sh b/infra/deploy_afc.sh new file mode 100755 index 0000000..0e757dc --- /dev/null +++ b/infra/deploy_afc.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +kubectl get all + +helm repo add bitnami https://charts.bitnami.com/bitnami +helm repo add kedacore https://kedacore.github.io/charts +helm repo update +helm install keda kedacore/keda --namespace keda --create-namespace + +helm upgrade --install ingress-nginx ingress-nginx --repo https://kubernetes.github.io/ingress-nginx --namespace ingress-nginx --create-namespace + +kubectl wait --namespace ingress-nginx \ + --for=condition=ready pod \ + --selector=app.kubernetes.io/component=controller \ + --timeout=120s + +helm install test-internal afc/ -f afc/values.yaml diff --git a/msghnd/Dockerfile b/msghnd/Dockerfile new file mode 100644 index 0000000..c371c9e --- /dev/null +++ b/msghnd/Dockerfile @@ -0,0 +1,90 @@ +# +# Copyright 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +# Install required packages +# +FROM alpine:3.18 as msghnd.preinstall +ENV PYTHONUNBUFFERED=1 +RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python && \ +apk add --update --no-cache py3-six py3-numpy py3-cryptography py3-sqlalchemy \ +py3-requests py3-flask py3-psycopg2 py3-pydantic=~1.10 && \ +apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \ +py3-confluent-kafka && \ +python3 -m ensurepip && \ +pip3 install --no-cache --upgrade pip setuptools +COPY msghnd/requirements.txt /wd/ +RUN pip3 install -r /wd/requirements.txt && mkdir -p /run/gunicorn /etc/xdg/fbrat +COPY gunicorn/wsgi.py /wd/ +COPY config/ratapi.conf /etc/xdg/fbrat/ +RUN echo "AFC_APP_TYPE = 'msghnd'" >> /etc/xdg/fbrat/ratapi.conf +# +# Build Message Handler application +# +FROM alpine:3.18 as msghnd.build +ENV PYTHONUNBUFFERED=1 +COPY --from=msghnd.preinstall / / +# Development env +RUN apk add --update --no-cache cmake ninja +# +COPY CMakeLists.txt LICENSE.txt version.txt Doxyfile.in /wd/ +COPY cmake /wd/cmake/ +COPY pkg /wd/pkg/ +COPY src /wd/src/ +RUN mkdir -p -m 777 /wd/build +ARG BUILDREV=localbuild +RUN cd /wd/build && \ +cmake -DCMAKE_INSTALL_PREFIX=/wd/__install -DCMAKE_PREFIX_PATH=/usr -DCMAKE_BUILD_TYPE=RatapiRelease -DSVN_LAST_REVISION=$BUILDREV -G Ninja /wd && \ +ninja -j$(nproc) install +# +# Install Message Handler application +# +FROM alpine:3.18 as msghnd.install +ENV PYTHONUNBUFFERED=1 +COPY --from=msghnd.preinstall / / +COPY --from=msghnd.build /wd/__install /usr/ +# +COPY src/afc-packages /wd/afc-packages +RUN pip3 install --use-pep517 --root-user-action=ignore \ + -r /wd/afc-packages/pkgs.msghnd \ + && rm -rf /wd/afc-packages +# +RUN mkdir -m 750 -p /var/lib/fbrat/AntennaPatterns && \ +mkdir -m 755 -p /var/spool/fbrat /var/lib/fbrat /var/celery /var/run/celery /var/log/celery +# Add user and group +RUN addgroup -g 1003 fbrat && \ +adduser -g '' -D -u 1003 -G fbrat -h /var/lib/fbrat -s /sbin/nologin fbrat && \ +chown fbrat:fbrat /var/lib/fbrat/AntennaPatterns /var/spool/fbrat /var/lib/fbrat /var/celery +# +LABEL revision="afc-msghnd" +WORKDIR /wd +EXPOSE 8000 +COPY msghnd/entrypoint.sh / + +# Prometheus stuff +COPY gunicorn/config.py /wd/gunicorn_config.py +# Directory for Prometheus's multiprocess housekeeping +ENV PROMETHEUS_MULTIPROC_DIR=/wd/prometheus_multiproc_dir +RUN mkdir -p $PROMETHEUS_MULTIPROC_DIR + +# Add debugging env if configured +ARG AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +COPY msghnd/devel.sh /wd/ +RUN chmod +x /wd/devel.sh +RUN /wd/devel.sh +# msghnd environment variables default values +ENV AFC_MSGHND_PORT=${AFC_MSGHND_PORT:-"8000"} +ENV AFC_MSGHND_BIND=${AFC_MSGHND_BIND:-"0.0.0.0"} +ENV AFC_MSGHND_PID=${AFC_MSGHND_PID:-"/run/gunicorn/openafc_app.pid"} +ENV AFC_MSGHND_ACCESS_LOG= +ENV AFC_MSGHND_ERROR_LOG=${AFC_MSGHND_ERROR_LOG:-"/proc/self/fd/2"} +ENV AFC_MSGHND_TIMEOUT=${AFC_MSGHND_TIMEOUT:-180} +ENV AFC_MSGHND_WORKERS=${AFC_MSGHND_WORKERS:-20} +ENV AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +ENV AFC_MSGHND_RATAFC_TOUT=${AFC_MSGHND_RATAFC_TOUT:-600} +RUN chmod +x /entrypoint.sh +CMD ["/entrypoint.sh"] +HEALTHCHECK CMD wget --no-verbose --tries=1 --spider \ + http://localhost:${AFC_MSGHND_PORT}/fbrat/ap-afc/healthy || exit 1 diff --git a/msghnd/devel.sh b/msghnd/devel.sh new file mode 100644 index 0000000..9fa0a18 --- /dev/null +++ b/msghnd/devel.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# +# Copyright 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +case "$AFC_DEVEL_ENV" in + "devel") + echo "Debug profile" + export NODE_OPTIONS='--openssl-legacy-provider' + apk add --update --no-cache cmake ninja yarn bash + ;; + "production") + echo "Production profile" + ;; + *) + echo "Uknown profile" + ;; +esac + +exit $? diff --git a/msghnd/entrypoint.sh b/msghnd/entrypoint.sh new file mode 100644 index 0000000..ee00ed2 --- /dev/null +++ b/msghnd/entrypoint.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# +# Copyright (C) 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +case "$AFC_DEVEL_ENV" in + "devel") + echo "Running debug profile" + AFC_MSGHND_LOG_LEVEL="debug" + echo "AFC_MSGHND_PORT = ${AFC_MSGHND_PORT}" + echo "AFC_MSGHND_BIND = ${AFC_MSGHND_BIND}" + echo "AFC_MSGHND_PID = ${AFC_MSGHND_PID}" + echo "AFC_MSGHND_ACCESS_LOG = ${AFC_MSGHND_ACCESS_LOG}" + echo "AFC_MSGHND_ERROR_LOG = ${AFC_MSGHND_ERROR_LOG}" + echo "AFC_MSGHND_TIMEOUT = ${AFC_MSGHND_TIMEOUT}" + echo "AFC_MSGHND_WORKERS = ${AFC_MSGHND_WORKERS}" + echo "AFC_MSGHND_LOG_LEVEL = ${AFC_MSGHND_LOG_LEVEL}" + echo "AFC_MSGHND_RATAFC_TOUT = ${AFC_MSGHND_RATAFC_TOUT}" + ;; + "production") + echo "Running production profile" + AFC_MSGHND_LOG_LEVEL="info" + ;; + *) + echo "Uknown profile" + AFC_MSGHND_LOG_LEVEL="info" + ;; +esac + +gunicorn \ +--bind "${AFC_MSGHND_BIND}:${AFC_MSGHND_PORT}" \ +--pid "${AFC_MSGHND_PID}" \ +--workers "${AFC_MSGHND_WORKERS}" \ +--timeout "${AFC_MSGHND_TIMEOUT}" \ +${AFC_MSGHND_ACCESS_LOG:+--access-logfile "$AFC_MSGHND_ACCESS_LOG"} \ +--error-logfile "${AFC_MSGHND_ERROR_LOG}" \ +--log-level "${AFC_MSGHND_LOG_LEVEL}" \ +--worker-class gevent \ +wsgi:app + +# +sleep infinity + +exit $? diff --git a/msghnd/requirements.txt b/msghnd/requirements.txt new file mode 100644 index 0000000..c12dee1 --- /dev/null +++ b/msghnd/requirements.txt @@ -0,0 +1,60 @@ +alembic==1.8.1 +amqp==5.1.1 +bcrypt==4.0.1 +billiard==3.6.4.0 +blinker==1.6.2 +celery==5.2.7 +certifi==2023.7.22 +cffi==1.15.1 +charset-normalizer==3.0.1 +click==8.1.3 +click-didyoumean==0.3.0 +click-plugins==1.1.1 +click-repl==0.2.0 +defusedxml==0.7.1 +dnspython==2.2.1 +email-validator==1.3.0 +Flask==2.3.2 +Flask-JSONRPC==1.0.2 +Flask-Login==0.6.2 +Flask-Mail==0.9.1 +Flask-Migrate==2.6.0 +Flask-Script==2.0.5 +Flask-SQLAlchemy==2.5.1 +Flask-User==1.0.2.1 +Flask-WTF==1.1.1 +futures==3.0.5 +gevent==23.9.1 +greenlet==3.0.1 +gunicorn==20.1.0 +idna==3.4 +IPy==1.1 +itsdangerous==2.1.2 +Jinja2==3.1.2 +jsmin==3.0.1 +json5==0.9.10 +jwt==1.3.1 +kombu==5.2.4 +Mako==1.2.4 +MarkupSafe==2.1.1 +nose==1.3.7 +passlib==1.7.4 +pathlib==1.0.1 +prettytable==3.5.0 +prometheus-client==0.17.1 +prompt-toolkit==3.0.33 +pycparser==2.21 +python2-secrets==1.0.5 +pytz==2022.6 +pyxdg==0.28 +PyYAML==6.0.1 +SQLAlchemy +typeguard==2.13.3 +urllib3==1.26.18 +vine==5.0.0 +wcwidth==0.2.5 +Werkzeug==2.3.3 +WsgiDAV==4.1.0 +WTForms==3.0.1 +python_dateutil==2.8.2 +pika==1.3.2 diff --git a/objstorage/Dockerfile b/objstorage/Dockerfile new file mode 100644 index 0000000..c7449c6 --- /dev/null +++ b/objstorage/Dockerfile @@ -0,0 +1,27 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +FROM alpine:3.18 + +RUN apk add py3-pip py3-gunicorn py3-gevent \ + gcc python3-dev libc-dev + +COPY objstorage/docker-entrypoint.sh /wd/ +RUN chmod +x /wd/docker-entrypoint.sh + +COPY src/afc-packages /wd/afc-packages +RUN pip3 install --use-pep517 --root-user-action=ignore \ + -r /wd/afc-packages/pkgs.objstorage \ + && rm -rf /wd/afc-packages + +ENV AFC_OBJST_PORT=${AFC_OBJST_PORT:-"5000"} +ENV AFC_OBJST_WORKERS=${AFC_OBJST_WORKERS:-10} +ENV AFC_OBJST_HIST_PORT=${AFC_OBJST_HIST_PORT:-"4999"} +ENV AFC_OBJST_HIST_WORKERS=${AFC_OBJST_HIST_WORKERS:-2} +CMD ["/wd/docker-entrypoint.sh"] +HEALTHCHECK CMD wget --no-verbose --tries=1 --spider \ + http://localhost:${AFC_OBJST_PORT}/healthy || exit 1 diff --git a/objstorage/README.md b/objstorage/README.md new file mode 100644 index 0000000..6c8d066 --- /dev/null +++ b/objstorage/README.md @@ -0,0 +1,95 @@ +This work is licensed under the OpenAFC Project License, a copy of which is included with this software program. + +
+
+ +## Table of Contents +- [**Introduction**](#introduction) +- [**Setup**](#setup) + - [The object storage HTTP server configuration](#the-object-storage-http-server-configuration) + - [The history view HTTP server configuration](#the-history-view-http-server-configuration) + - [Configuration of service which uses object storage](#configuration-of-docker-service-which-uses-object-storage) + - [Building HTTP servers docker image](#building-http-servers-docker-image) + - [docker-compose examples](#docker-compose-example) + - [Apache configuration](#apache-configuration) + +- [Back to main readme](/README.md) +

+ +# **Introduction** + +This document describes the usage of the HTTP servers as storage for the RATAPI dynamic data. + +The object storage HTTP server implements file exchange between HTTPD, celery workers, and the web client. +The history view server provides read access to the debug files. + +# **Setup** +## The object storage HTTP server configuration +The object storage HTTP server receive its configuration from the following environment variables: +- **AFC_OBJST_HOST** - file storage server host (default: 0.0.0.0). +- **AFC_OBJST_PORT** - file storage server post (default: 5000). + - The object storage Dockerfile exposes port 5000 for access to the object storage server. +- **AFC_OBJST_MEDIA** - The media used for storing files by the server (default: LocalFS). The possible values are + - **LocalFS** - store files on docker's FS. + - **GoogleCloudBucket** - store files on Google Store. +- **AFC_OBJST_LOCAL_DIR** - file system path to stored files in file storage docker (default: /storage). Used only when **AFC_OBJST_MEDIA**=**LocalFS** +- **AFC_OBJST_LOG_LVL** - logging level of the file storage. The relevant values are DEBUG and ERROR. + +Using Google Storage bucket as file storage requires creating the bucket ([Create storage buckets](https://cloud.google.com/storage/docs/creating-buckets)), +the service account ([Service accounts](https://cloud.google.com/iam/docs/service-accounts)), +and the service account credentials JSON file ([Create access credentials](https://developers.google.com/workspace/guides/create-credentials#service-account)). +Accordingly, the file storage server requires the following two variables: + +- **AFC_OBJST_GOOGLE_CLOUD_BUCKET** - Bucket name as seen in column "Name" in [Storage Browser](https://console.cloud.google.com/storage/browser) +- **AFC_OBJST_GOOGLE_CLOUD_CREDENTIALS_JSON** - Path to service account credentials JSON file in the docker image. + - The directory contains the file must be mounted to the file storage docker image. + +## The history view HTTP server configuration +The history view HTTP server receive its configuration from the following environment variables: +- **AFC_OBJST_HIST_PORT** - history view server port (default: 4999) + - The object storage Dockerfile exposes port 4999 for access to the history server. + +## Configuration of docker service which uses object storage +The docker service accesses file storage according to the following environment variables: +- **AFC_OBJST_HOST** - file storage server host +- **AFC_OBJST_PORT** - file storage server post +- **AFC_OBJST_SCHEME** - file storage server scheme, HTTP or HTTPS + +**AFC_OBJST_HOST** and **AFC_OBJST_PORT** environment variables have to be declared to enable using HTTP object storage. + +## Building HTTP servers docker image +Use Dockerfile for build or see docker-compose.yaml example below. + +## docker-compose example + +``` +version: '3.2' +services: + objst: + image: http_servers + environment: + # Object storage port + - AFC_OBJST_PORT=5000 + # History view server port + - AFC_OBJST_HIST_PORT=4999 + # Use docker FS to store files + - AFC_OBJST_MEDIA=LocalFS + # Some folder inside the image for file storing. Ignored when AFC_OBJST_MEDIA=GoogleCloudBucket + - AFC_OBJST_LOCAL_DIR=/storage + # Google Cloud credentials file. Ignored when AFC_OBJST_MEDIA="GoogleCloudBucket" + - AFC_OBJST_GOOGLE_CLOUD_CREDENTIALS_JSON=/credentials/google.cert.json + # Google Cloud bucket name. Ignored when AFC_OBJST_MEDIA="GoogleCloudBucket" + - AFC_OBJST_GOOGLE_CLOUD_BUCKET=wcc-afc-objstorage + build: + context: . + + service_which_stores_in_objst: + image: some_name + environment: + # Object storage host is a name of objst service + - AFC_OBJST_HOST=objst + # Object storage port as declared in "objst:environment:AFC_OBJST_PORT" + - AFC_OBJST_PORT=5000 + # Use HTTP (not HTTPS) to access file storage + - AFC_OBJST_SCHEME=HTTP +``` diff --git a/objstorage/docker-entrypoint.sh b/objstorage/docker-entrypoint.sh new file mode 100644 index 0000000..0ac1e48 --- /dev/null +++ b/objstorage/docker-entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# +# Copyright (C) 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +case "$AFC_DEVEL_ENV" in + "devel") + echo "Running debug profile" + echo "AFC_OBJST_PORT = ${AFC_OBJST_PORT}" + echo "AFC_OBJST_WORKERS = ${AFC_OBJST_WORKERS}" + echo "AFC_OBJST_HIST_PORT = ${AFC_OBJST_HIST_PORT}" + echo "AFC_OBJST_HIST_WORKERS = ${AFC_OBJST_HIST_WORKERS}" + ;; + "production") + echo "Running production profile" + AFC_MSGHND_LOG_LEVEL="info" + ;; + *) + echo "Uknown profile" + AFC_MSGHND_LOG_LEVEL="info" + ;; +esac + +gunicorn --workers ${AFC_OBJST_WORKERS} --worker-class gevent --bind 0.0.0.0:${AFC_OBJST_PORT} afcobjst:objst_app & +gunicorn --workers ${AFC_OBJST_HIST_WORKERS} --worker-class gevent --bind 0.0.0.0:${AFC_OBJST_HIST_PORT} afcobjst:hist_app & + +sleep infinity diff --git a/objstorage/test.sh b/objstorage/test.sh new file mode 100755 index 0000000..06d76d5 --- /dev/null +++ b/objstorage/test.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +function run { + dd if=/dev/urandom of=tmp bs=50K count=1 + curl -F "file=@tmp;filename=1.txt" http://127.0.0.1:5000/$1/a/b + echo + wget --post-file=./tmp 127.0.0.1:5000/$1/1/2/3/2.txt + wget http://127.0.0.1:5000/$1/a/b/1.txt + wget http://127.0.0.1:5000/$1/1/2/3/2.txt + wget --spider http://127.0.0.1:5000/$1/a/b/1.txt + wget --spider http://127.0.0.1:5000/$1/1/2/3/2.txt + curl -X DELETE http://127.0.0.1:5000/$1/1 + curl -X DELETE http://127.0.0.1:5000/$1/a +} + +run dbg +run cfg +run pro + diff --git a/pkg/CMakeLists.txt b/pkg/CMakeLists.txt new file mode 100644 index 0000000..d08433f --- /dev/null +++ b/pkg/CMakeLists.txt @@ -0,0 +1,81 @@ +# Platform-independent CPack configuration +set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE.txt") + + +# Combine space-separated list of internal libraries for ...Config.cmake +string(REPLACE ";" " " TARGET_LIBS_SPACESEP "${TARGET_LIBS}") + +if(TARGET_LIBS) + # CMake configuration for this library + install( + EXPORT ${CMAKE_MODULE_NAME}Targets + FILE ${CMAKE_MODULE_NAME}Targets.cmake + DESTINATION ${PKG_INSTALL_CMAKE_CONFIG_DIR} + COMPONENT development + ) +endif(TARGET_LIBS) +configure_file(${CMAKE_MODULE_NAME}Config.cmake.in ${CMAKE_MODULE_NAME}Config.cmake @ONLY) +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_MODULE_NAME}Config.cmake + DESTINATION ${PKG_INSTALL_CMAKE_CONFIG_DIR} + COMPONENT development +) +configure_file(${CMAKE_MODULE_NAME}ConfigVersion.cmake.in ${CMAKE_MODULE_NAME}ConfigVersion.cmake @ONLY) +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_MODULE_NAME}ConfigVersion.cmake + DESTINATION ${PKG_INSTALL_CMAKE_CONFIG_DIR} + COMPONENT development +) + +if(UNIX) +# CPack to create pre-rpm tarball +set(CPACK_SOURCE_GENERATOR "TBZ2") +set(CPACK_SOURCE_PACKAGE_FILE_NAME + "${CMAKE_PROJECT_NAME}-${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}") +set(CPACK_SOURCE_IGNORE_FILES + "/build/;/doc/;/puppet/;/.svn/;~$;.sh$;${CPACK_SOURCE_IGNORE_FILES}") +include(CPack) + +# RPM spec generator +set(RPM_PKG_NAME ${CMAKE_PROJECT_NAME}) +set(RPM_PKG_SRC "${CMAKE_BINARY_DIR}/${CPACK_SOURCE_PACKAGE_FILE_NAME}.tar.bz2") +set(RPM_VERSION ${PROJECT_VERSION}) +set(RPM_RELEASE ${SVN_LAST_REVISION}) +set(RPM_DIST_DIR "${CMAKE_BINARY_DIR}/dist") + +# Create spec file dynamically +set(RPM_SPEC_DOW Sun Mon Tue Wed Thu Fri Sat) +string(TIMESTAMP NOW_DOW_IX "%w") +message(DOW ${NOW_DOW_IX}) +list(GET RPM_SPEC_DOW ${NOW_DOW_IX} NOW_DOW) +set(RPM_SPEC_MON NIL Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) +string(TIMESTAMP NOW_MON_IX "%m") +message(MON ${NOW_MON_IX}) +list(GET RPM_SPEC_MON ${NOW_MON_IX} NOW_MON) +string(TIMESTAMP NOW_DAY_YEAR "%d %Y") +set(RPM_CHANGELOG_DATE "${NOW_DOW} ${NOW_MON} ${NOW_DAY_YEAR}") +configure_file(${RPM_PKG_NAME}.spec.in ${RPM_PKG_NAME}.spec @ONLY) + +# Target "package_source" defined by CPack +add_custom_target(rpm-prep + DEPENDS + ${CMAKE_CURRENT_BINARY_DIR}/${RPM_PKG_NAME}.spec + package_source + COMMAND mkdir -p ${RPM_DIST_DIR}/SOURCES + COMMAND mkdir -p ${RPM_DIST_DIR}/SPECS + COMMAND mkdir -p ${RPM_DIST_DIR}/SRPMS + COMMAND mkdir -p ${RPM_DIST_DIR}/RPMS + COMMAND cp ${RPM_PKG_SRC} ${RPM_DIST_DIR}/SOURCES + COMMAND cp ${RPM_PKG_NAME}.spec ${RPM_DIST_DIR}/SPECS +) +# Prepend to external RPMBUILD_ARGS definition +set(RPMBUILD_ARGS --define \"_topdir ${RPM_DIST_DIR}\" ${RPMBUILD_ARGS}) +add_custom_target(rpm + DEPENDS rpm-prep + COMMAND rpmbuild -ba ${RPMBUILD_ARGS} ${RPM_DIST_DIR}/SPECS/${RPM_PKG_NAME}.spec +) + +endif(UNIX) diff --git a/pkg/fbrat.spec.in b/pkg/fbrat.spec.in new file mode 100644 index 0000000..4f0314e --- /dev/null +++ b/pkg/fbrat.spec.in @@ -0,0 +1,208 @@ +# Package spec formatted according to best practices documented at +# http://fedoraproject.org/wiki/How_to_create_an_RPM_package +# http://fedoraproject.org/wiki/Packaging:Guidelines +# https://fedoraproject.org/wiki/PackagingDrafts/ScriptletSnippets + +%bcond_without afcengine + +Name: @RPM_PKG_NAME@ +Version: @RPM_VERSION@ +Release: @RPM_RELEASE@%{?dist} +License: Commercial +Url: http://rkf-eng.com/facebook_rat +Source0: %{name}-%{version}.tar.bz2 +BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root + + +BuildRequires: cmake3 >= 3.1 +BuildRequires: ninja-build +BuildRequires: make +BuildRequires: devtoolset-11-gcc-c++ >= 11.2 +# Need rpm macros in python-devel +BuildRequires: python-devel +%if 0%{?fedora} +BuildRequires: boost-devel >= 1.54 +%endif +%if 0%{?rhel} +BuildRequires: boost169-devel >= 1.54 +%endif +BuildRequires: armadillo-devel >= 4.300 +BuildRequires: gdal-devel < 2.0 +BuildRequires: qt5-qtbase-devel >= 5.6 +BuildRequires: gtest-devel >= 1.6 +BuildRequires: systemd +BuildRequires: selinux-policy-devel +BuildRequires: selinux-policy-targeted, checkpolicy + +# Runtime requirements +Requires: httpd +Requires: mod_wsgi +Requires: python-flask +Requires: python-flask-script +Requires: python-flask_jsonrpc +Requires: python2-flask-sqlalchemy +Requires: python2-flask-migrate +Requires: python2-flask-user +Requires: python-pyxdg +Requires: python-cryptography +Requires: python2-bcrypt +Requires: python-blinker +Requires: python-wsgidav +Requires: python2-numpy +Requires: python-sqlalchemy +Requires: python-jwt +Requires: python-prettytable +Requires(post): policycoreutils, initscripts +Requires(preun): policycoreutils, initscripts +Requires(postun): policycoreutils +Summary: Facebook RAT +%description +All non-data runtime files for the Facebook RLAN AFC Tool. + +%package devel +Requires: %{name} = %{version}-%{release} +%if 0%{?fedora} +Requires: boost-devel >= 1.54 +%endif +%if 0%{?rhel} +Requires: boost169-devel >= 1.54 +%endif +Requires: armadillo-devel >= 4.300 +Requires: gdal-devel < 2.0 +Requires: qt5-qtbase-devel >= 5.6 +Requires: gtest-devel >= 1.6 +Summary: Development files for the Facebook RAT +%description devel +Headers and development libraries for the Facebook RLAN AFC Tool. + + +# Ninja macros not in CentOS +%define __ninja %{_bindir}/ninja-build +%define __ninja_common_opts -v %{?_smp_mflags} +%define ninja_build \ + %{__ninja} %{__ninja_common_opts} +%define ninja_install \ + DESTDIR=%{buildroot} %{__ninja} install %{__ninja_common_opts} +# Install helpers +%define mkdir %{_bindir}/mkdir -p +%define install_bin %{_bindir}/install -D -m 755 +%define install_data %{_bindir}/install -D -m 644 +%define cmake_pkgdir %{_libdir}/cmake +%define sysconfigdir %{_sysconfdir}/sysconfig +%define xdg_confdir %{_sysconfdir}/xdg +%define xdg_appdir %{_datadir}/applications + +### +# Install config +%define fbrat_groupname fbrat +%define fbrat_username fbrat +#: selinux modules +%define selpackagedir %{_datadir}/selinux/packages +%define selincludedir %{_datadir}/selinux/devel/include + + +%prep +%setup -q +%mkdir build + + +%build +pushd build + +CMAKEARGS="\ + -DSVN_LAST_REVISION=%{getenv:BUILDREV} + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DBUILD_AFCENGINE=%{?with_afcengine:ON}%{!?with_afcengine:OFF} \ + -G Ninja" +if [[ -d "/usr/include/boost169" ]]; then + CMAKEARGS="${CMAKEARGS} -DBOOST_INCLUDEDIR=/usr/include/boost169 -DBOOST_LIBRARYDIR=/usr/lib64/boost169" +fi +%cmake3 ${CMAKEARGS} .. + +%ninja_build + +popd + +# build selinux modules +make -C selinux -f /usr/share/selinux/devel/Makefile + + +%install +pushd build + +%ninja_install + +popd + +# SELinux module +%install_data selinux/fbrat.pp %{buildroot}%{selpackagedir}/fbrat/fbrat.pp +%install_data selinux/fbrat.if %{buildroot}%{selincludedir}/services/fbrat.if +# System configuration +%mkdir %{buildroot}%{xdg_confdir}/fbrat +cat <%{buildroot}%{xdg_confdir}/fbrat/ratapi.conf +EOF +# Home for the local account +%mkdir %{buildroot}%{_sharedstatedir}/fbrat +# Data containers +%mkdir %{buildroot}%{_datadr}/fbrat/afc-engine + +: + + +%check +pushd build +: + + +%clean +rm -rf %{buildroot} + + +%files +%defattr(-, root, root) +%doc LICENSE.txt +%{_bindir}/* +%{_libdir}/lib*.so.* +%{python2_sitelib}/ratapi* +%{_datadir}/fbrat +%{selpackagedir}/fbrat +%dir %{xdg_confdir}/fbrat +%config(noreplace) %{xdg_confdir}/fbrat/ratapi.conf +%dir %attr(-, %{fbrat_username}, %{fbrat_groupname}) %{_sharedstatedir}/fbrat + +%post +/sbin/ldconfig +getent group %{fbrat_groupname} >/dev/null || groupadd -r %{fbrat_groupname} +getent passwd %{fbrat_username} >/dev/null || useradd -r -g %{fbrat_groupname} -d %{_sharedstatedir}/fbrat -s /sbin/nologin -c "User for FBRAT WSGI" %{fbrat_username} +if [ "$1" -le "1" ] ; then # First install + semodule -i %{selpackagedir}/fbrat/fbrat.pp 2>/dev/null || : + fixfiles -R fbrat restore +fi + +%preun +if [ "$1" -lt "1" ] ; then # Final removal + semodule -r fbrat 2>/dev/null || : + fixfiles -R fbrat restore +fi + +%postun +/sbin/ldconfig +if [ "$1" -ge "1" ] ; then # Upgrade + semodule -i %{selpackagedir}/fbrat/fbrat.pp 2>/dev/null || : + fixfiles -R fbrat restore +fi + + +%files devel +%defattr(-, root, root) +%doc LICENSE.txt +%{_includedir}/fbrat +%{_libdir}/*.so +%{cmake_pkgdir}/fbrat +%{selincludedir}/services/*.if + + +%changelog + +* @RPM_CHANGELOG_DATE@ RKF Support - @RPM_VERSION@-@RPM_RELEASE@ +- See corresponding Version Description Document for details. diff --git a/pkg/fbratConfig.cmake.in b/pkg/fbratConfig.cmake.in new file mode 100644 index 0000000..4120901 --- /dev/null +++ b/pkg/fbratConfig.cmake.in @@ -0,0 +1,10 @@ +# ... +# (compute PREFIX relative to file location) +# ... +set(CPOFG_INCLUDE_DIRS "@CMAKE_INSTALL_PREFIX@/@PKG_INSTALL_INCLUDEDIR@") +set(CPOFG_BINDIR "@CMAKE_INSTALL_PREFIX@/@PKG_INSTALL_BINDIR@") +set(CPOFG_LIBRARIES @TARGET_LIBS_SPACESEP@) + +if(NOT TARGET cpofg) + include("${CMAKE_CURRENT_LIST_DIR}/cpofgTargets.cmake") +endif() diff --git a/pkg/fbratConfigVersion.cmake.in b/pkg/fbratConfigVersion.cmake.in new file mode 100644 index 0000000..8ea9c21 --- /dev/null +++ b/pkg/fbratConfigVersion.cmake.in @@ -0,0 +1,12 @@ + +set(PACKAGE_VERSION "@PROJECT_VERSION@") + +# Check whether the requested PACKAGE_FIND_VERSION is compatible +if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}") + set(PACKAGE_VERSION_COMPATIBLE FALSE) +else() + set(PACKAGE_VERSION_COMPATIBLE TRUE) + if ("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}") + set(PACKAGE_VERSION_EXACT TRUE) + endif() +endif() diff --git a/prereqs/.bashrc b/prereqs/.bashrc new file mode 100644 index 0000000..0516cfe --- /dev/null +++ b/prereqs/.bashrc @@ -0,0 +1,71 @@ +# +# Copyright © 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# .bashrc + +if [ -f /etc/bashrc ]; then + . /etc/bashrc +fi + +# User specific aliases and functions +EDITOR=vim +set -o vi + +# +# AFC project +# +# Defines directory where cloned source code +export BRCM_OPENAFC_PRJ=/wd/afc/open-afc +# Defines directory where placed built objects and binaries +export BRCM_WORKINGDIR=/wd/afc/oafc_bld + +alias afc_help='echo -e " \ + afc_wd - cd to workdir and prepare dev env\n \ + afc_cm - run cmake on sources\n \ + afc_bld - run ninja-build \n \ + afc_ins - run ninja-build install \n \ + BRCM_OPENAFC_PRJ = ${BRCM_OPENAFC_PRJ} \n \ + BRCM_WORKINGDIR = ${BRCM_WORKINGDIR} \n \ + N_THREADS = ${N_THREADS} \n \ + APIDOCDIR = ${APIDOCDIR} \n \ + LD_LIBRARY_PATH = ${LD_LIBRARY_PATH} \n \ +"' +alias afc_wd='pushd ${BRCM_OPENAFC_PRJ} && \ +source /opt/rh/devtoolset-11/enable && \ +popd && \ +pushd ${BRCM_WORKINGDIR} && \ +export N_THREADS=$(nproc --all) && \ +export APIDOCDIR=${BRCM_WORKINGDIR}/testroot/share/doc/fbrat-api && \ +export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:${BRCM_WORKINGDIR}/testroot/lib64 +' +alias afc_cm='cmake3 -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_PREFIX_PATH=/usr \ + -DAPIDOC_INSTALL_PATH=${APIDOCDIR} \ + -DBOOST_INCLUDEDIR=/usr/include/boost169 \ + -DBOOST_LIBRARYDIR=/usr/lib64/boost169 \ + -DBUILD_WITH_COVERAGE=off \ + -G Ninja ${BRCM_OPENAFC_PRJ}' +alias afc_bld='ninja-build -j$N_THREADS' +alias afc_ins='ninja-build install -j$N_THREADS' + +#GREP +alias g='grep' +alias gk='grep -r --exclude-dir={.git,.svn} --exclude={tags,*~,*.swp,*.o,*.ko,cscope.out}' +alias gkl='grep -rl --exclude-dir={.git,.svn} --exclude={tags,*~,*.swp,*.o,*.ko,cscope.out}' +#GIT +alias gb='git branch' +alias gba='git branch -a' +alias gc='git commit -v' +alias gd='git diff' +alias gl='git pull' +alias gp='git push' +alias gst='git status' +alias glo='git log --oneline' + +alias rm='rm -i' +alias mv='mv -i' +alias cp='cp -i' +alias ll='ls -l' +alias lla='ls -la' diff --git a/prometheus/Dockerfile-cadvisor b/prometheus/Dockerfile-cadvisor new file mode 100644 index 0000000..73e36bc --- /dev/null +++ b/prometheus/Dockerfile-cadvisor @@ -0,0 +1,35 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# Cadvisor dockerfile + +FROM gcr.io/cadvisor/cadvisor:v0.47.2 + +# Housekeeping interval +ENV CADVISOR_HOUSEKEEPING_INTERVAL=5s + +# Metrics - comma separated list of metric groups (see +# https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md for +# more details) +ENV CADVISOR_METRICS=cpu,memory,process,sched + +# Additional arguments +ENV CADVISOR_ADDITIONAL_ARGS= + +EXPOSE 9090 + +ENTRYPOINT /usr/bin/cadvisor \ + -logtostderr \ + -docker_only=true \ + -store_container_labels=false \ + -housekeeping_interval=$CADVISOR_HOUSEKEEPING_INTERVAL \ + -enable_metrics=$CADVISOR_METRICS \ + -enable_load_reader=true \ + $CADVISOR_ADDITIONAL_ARGS + +HEALTHCHECK CMD \ + wget --quiet --tries=1 --spider http://localhost:8080/healthz || exit 1 \ No newline at end of file diff --git a/prometheus/Dockerfile-grafana b/prometheus/Dockerfile-grafana new file mode 100644 index 0000000..6b83b2c --- /dev/null +++ b/prometheus/Dockerfile-grafana @@ -0,0 +1,12 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# Grafana dockerfile (for future expansion) + +FROM grafana/grafana-oss:10.2.0-ubuntu + +EXPOSE 3000/tcp diff --git a/prometheus/Dockerfile-nginxexporter b/prometheus/Dockerfile-nginxexporter new file mode 100644 index 0000000..fa2075d --- /dev/null +++ b/prometheus/Dockerfile-nginxexporter @@ -0,0 +1,17 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# Dockerfile for Nginx Exporter that brings Nginx metrics to Prometheus format + +FROM bitnami/nginx-exporter:0.11.0-debian-11-r370 + +# URL on which Nginx' stub_status is exported +ENV NGINXEXPORTER_STUB_URL=http://dispatcher:8080/stub_status + +EXPOSE 9113/tcp + +ENTRYPOINT nginx-prometheus-exporter -nginx.scrape-uri=$NGINXEXPORTER_STUB_URL diff --git a/prometheus/Dockerfile-prometheus b/prometheus/Dockerfile-prometheus new file mode 100644 index 0000000..6bc7185 --- /dev/null +++ b/prometheus/Dockerfile-prometheus @@ -0,0 +1,43 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +FROM ubuntu/prometheus:2.46.0-22.04_stable + +# Database directory. May be mapped to something external for persistence. +# Evaluated in entrypoint.sh (i.e. not baked-in) +ENV PROMETHEUS_DATA=/prometheus/data + +# Scrape interval. Must have unit suffix (e.g. 15s for 15 seconds, 1m for one +# minute, etc.) By default taken from prometheus.yml (5 seconds as of time of +# this writing) +# ENV PROMETHEUS_SCRAPE_INTERVAL + +# Scrape timeout. Must be smaller than scrape interval. Must have unit suffix +# (e.g. 10s for 10 seconds, 1m for one minute, etc.) By default taken from +# prometheus.yml (2 seconds as of time of this writing) +# ENV PROMETHEUS_SCRAPE_TIMEOUT + +# PERSISTING PROMETHEUS DATA +# Prometheus database is at /prometheus/data inside the container. It can be' +# persisted if mapped to some external directory + +RUN apt update +RUN apt upgrade -y +RUN apt install -y curl + +WORKDIR /prometheus + +COPY prometheus.yml /prometheus/ +COPY prometheus_entrypoint.sh /prometheus +RUN chmod +x /prometheus/prometheus_entrypoint.sh + +EXPOSE 9090/TCP + +ENTRYPOINT ["./prometheus_entrypoint.sh"] + +HEALTHCHECK CMD curl --fail http://localhost:9090/-/healthy || exit 1 + diff --git a/prometheus/compose_node-exporter.yaml b/prometheus/compose_node-exporter.yaml new file mode 100644 index 0000000..166ac81 --- /dev/null +++ b/prometheus/compose_node-exporter.yaml @@ -0,0 +1,19 @@ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# This is an addendum to main docker-compose.yaml that adds node-exporter +# service (Prometheus data source for host compose runs on) +--- +services: + node_exporter: + image: bitnami/node-exporter:1.6.1-debian-11-r78 + restart: always + command: + - --path.rootfs=/host + volumes: + - /:/host:ro,rslave + dns_search: [.] diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml new file mode 100644 index 0000000..f22aad7 --- /dev/null +++ b/prometheus/prometheus.yml @@ -0,0 +1,53 @@ +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +global: + scrape_interval: 5s + scrape_timeout: 2s + evaluation_interval: 15s # Interval for recording rules + +scrape_configs: + - job_name: prometheus + static_configs: + - targets: [localhost:9090] + - job_name: rmq + static_configs: + - targets: [rmq:15692] + metrics_path: metrics/detailed + params: + vhost: [fbrat, rcache] + family: [queue_coarse_metrics] + - job_name: msghnd + static_configs: + - targets: [msghnd:8000] + metrics_path: fbrat/ap-afc/metrics + - job_name: als_siphon + static_configs: + - targets: [als_siphon:8080] + - job_name: nginxexporter + static_configs: + - targets: [nginxexporter:9113] + - job_name: uls_downloader + static_configs: + - targets: [uls_downloader:8000] + - job_name: cadvisor + static_configs: + - targets: [cadvisor:8080] + metric_relabel_configs: + - source_labels: [name] + regex: "(.*?)_(rat_server|als_siphon|uls_downloader|bulk_postgres|\ + als_kafka|cert_db|rcache_tool|[^_]+)_[0-9]+" + replacement: "$1" + target_label: project + - source_labels: [name] + regex: "(.*?)_(rat_server|als_siphon|uls_downloader|bulk_postgres|\ + als_kafka|cert_db|rcache_tool|[^_]+)_[0-9]+" + replacement: "$2" + target_label: service + - regex: image + action: labeldrop + - regex: container_label_.* + action: labeldrop diff --git a/prometheus/prometheus_entrypoint.sh b/prometheus/prometheus_entrypoint.sh new file mode 100644 index 0000000..dbd5bd6 --- /dev/null +++ b/prometheus/prometheus_entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# +# Copyright (C) 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +# Runs prometheus with parameter specified over environment + +if [ "$PROMETHEUS_SCRAPE_INTERVAL" != "" ] +then + sed -i "s/scrape_interval:\\s*[0-9]\\+./scrape_interval: $PROMETHEUS_SCRAPE_INTERVAL/g" prometheus.yml +fi + +if [ "$PROMETHEUS_SCRAPE_TIMEOUT" != "" ] +then + sed -i "s/scrape_timeout:\\s*[0-9]\\+./scrape_timeout: $PROMETHEUS_SCRAPE_TIMEOUT/g" prometheus.yml +fi + +prometheus --web.enable-lifecycle --storage.tsdb.path="$PROMETHEUS_DATA" diff --git a/publish/__init__.py b/publish/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/publish/breakpad_extract.py b/publish/breakpad_extract.py new file mode 100644 index 0000000..cfa2db8 --- /dev/null +++ b/publish/breakpad_extract.py @@ -0,0 +1,188 @@ +#!/usr/bin/python +''' This program walks a "debuginfo" tree and extracts Google +breakpad symbol data into a symbol tree. +''' + +import sys +import os +import argparse +import subprocess +import re +import shutil +import logging + + +def is_elf(filename): + ''' Read magic header for a file to determine if it has ELF data. + + :param filename: The file name to open and check. + :return: True if the file contains ELF debug data. + ''' + try: + fobj = open(filename, 'rb') + except IOError: + return False + + head = fobj.read(4) + return (head == '\177ELF') + + +def dump_syms(outdir, debugdir, filepath, log): + ''' Manage the output of a "breakpad-dumpsyms" invocation for a single + input file. + + :param outdir: The symbol output root directory. + :type outdir: str + :param debugdir: The directory containing associated ".debug" files. + :type debugdir: str + :param filepath: The non-".debug" file name to extract symbols from. + :type filepath: str + :param log: The logger object to write. + :type log: :py:class:`Logger` + ''' + + # Match pattern "MODULE operatingsystem architecture id name" + head_re = re.compile(r'^(MODULE) (\S+) (\S+) (\S+) (.+)') + + args = [ + 'breakpad-dumpsyms', + filepath, + debugdir, + ] + log.debug('Running: {0}'.format( + ' '.join('"{0}"'.format(arg) for arg in args))) + bucket = open('/dev/null', 'w') + proc = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=bucket) + outfile = None + + # First line is the only one that needs parsing + line = proc.stdout.readline() + match = head_re.match(line) + if match is None: + status = proc.wait() + if status != 0: + log.debug('No debug info for file "{0}"'.format(filepath)) + return + + log.error('Header line does not match format: "{0}"'.format(line)) + return + + (rec, opsys, arch, ident, name) = match.groups() + path = os.path.join(outdir, name, ident, name + '.sym') + parent = os.path.dirname(path) + if not os.path.isdir(parent): + os.makedirs(parent) + + log.info('Writing symbols for "{0}" from "{1}" to "{2}"'.format( + name, filepath, path)) + outfile = open(path, 'wb') + # Write re-named header line + outfile.write(' '.join((rec, opsys, arch, ident, name)) + '\n') + + # Remaining output is piped + while proc.poll() is None: + outfile.write(proc.stdout.read()) + + +class Extractor(object): + ''' Extract Breakpad symbols from a file or tree of files. + + :param symbol_root: The output symbol root directory. + :type symbol_root: str + :param debug_root: The input debuginfo root directory. + :type debug_root: str + :param verbose: Extra output information. + :type verbose: bool + ''' + + def __init__(self, symbol_root, debug_root, rel_root, log): + self._dst = str(symbol_root) + self._rel_root = str(rel_root) + self._debug_root = str(debug_root) + self._log = log + + def _handle(self, arg, dirname, names): + for name in names: + path = os.path.join(dirname, name) + self._process(path) + + def _process(self, filepath): + if not os.path.isfile(filepath) or os.path.islink(filepath): + self._log.debug('Ignored non-file "{0}"'.format(filepath)) + return + if filepath.endswith('.debug'): + self._log.debug('Ignored debug object file "{0}"'.format(filepath)) + return + if filepath.endswith('.o'): + self._log.debug('Ignored plain object file "{0}"'.format(filepath)) + return + if not is_elf(filepath): + self._log.debug('Ignored non-ELF file "{0}"'.format(filepath)) + return + + self._log.debug('Processing "{0}"'.format(filepath)) + # Generate parentrel as relative path from debug root + parentrel = os.path.dirname(filepath) + if self._rel_root != '/': + parentrel = os.path.relpath(parentrel, self._rel_root) + else: + parentrel = parentrel.lstrip('/') + debugrel = str(self._debug_root).lstrip('/') + debugdir = os.path.join(self._rel_root, debugrel, parentrel) + + dump_syms(self._dst, debugdir, filepath, self._log) + + def run(self, source): + ''' Walk the input tree and extract symbols. + :param scan_root: The file or root directory to walk. + ''' + if os.path.isfile(source): + self._process(source) + else: + os.path.walk(source, self._handle, None) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--verbose', type=int, default=0, + help='''\ +Verboisty levek: +0 is none, 1 is INFO, 2 is DEBUG. +Default is %(default)s.''') + parser.add_argument('--fileroot', type=str, default='/', + help='Root path for unpackaged files.''') + parser.add_argument('--debuginfo', type=str, default='/usr/lib/debug', + help='''\ +Input debuginfo source tree path. +Default is "%(default)s".''' + ) + parser.add_argument('symboldir', type=str, + help='Output breakpad symbol tree path.') + parser.add_argument('source', type=str, nargs='+', + help='''\ +Input root directory or file. Directories are recursed. +Only ELF-format real files (i.e. not symlinks) are read.''') + args = parser.parse_args() + + if not os.path.isdir(args.symboldir): + raise ValueError('Bad output path "{0}"'.format(args.symboldir)) + + if args.verbose >= 2: + log_level = logging.DEBUG + elif args.verbose == 1: + log_level = logging.INFO + else: + log_level = logging.WARNING + logging.basicConfig(level=log_level) + logger = logging.getLogger('') + + ext = Extractor(args.symboldir, rel_root=args.fileroot, + debug_root=args.debuginfo, log=logger) + for src in args.source: + ext.run(src) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/publish/breakpad_extract_rpm.py b/publish/breakpad_extract_rpm.py new file mode 100644 index 0000000..cdfa715 --- /dev/null +++ b/publish/breakpad_extract_rpm.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +import sys +import os +import argparse +import subprocess +import shutil +import logging +import tempfile + +LOGGER = logging.getLogger() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--verbose', type=int, default=0, + help='''\ +Verboisty levek: +0 is none, 1 is INFO, 2 is DEBUG. +Default is %(default)s.''') + parser.add_argument('symboldir', type=str, + help='Output breakpad symbol tree path.') + parser.add_argument('packages', type=str, nargs='+', + help='''Input packages to extract and read.''') + args = parser.parse_args() + + if args.verbose >= 2: + log_level = logging.DEBUG + elif args.verbose == 1: + log_level = logging.INFO + else: + log_level = logging.WARNING + logging.basicConfig(level=log_level) + + symbol_path = os.path.abspath(args.symboldir) + if not os.path.isdir(symbol_path): + os.path.makedirs(symbol_path) + + tmp_path = tempfile.mkdtemp() + LOGGER.debug('Temporary package contents under %s', tmp_path) + + bitbucket = open(os.devnull, 'wb') + for pkg_name in args.packages: + LOGGER.info('Extracting package %s ...', pkg_name) + cpio = subprocess.Popen( + ['cpio', '-idm'], cwd=tmp_path, + stdin=subprocess.PIPE, + stderr=bitbucket, + ) + rpm = subprocess.Popen( + ['rpm2cpio', pkg_name], + stdout=cpio.stdin + ) + (stdout, stderr) = rpm.communicate() + if rpm.returncode != 0: + LOGGER.error('rpm2cpio stderr:\n%s', stderr) + raise RuntimeError('Failed to run rpm2cpio') + (stdout, stderr) = cpio.communicate() + if cpio.returncode != 0: + LOGGER.error('cpio stderr:\n%s', stderr) + raise RuntimeError('Failed to run cpio') + + LOGGER.info('Extracting all symbols...') + subprocess.check_call( + [ + 'python', 'breakpad_extract.py', + '--verbose={0}'.format(args.verbose), + '--fileroot={0}'.format(tmp_path), + symbol_path, + '{0}/usr/lib64'.format(tmp_path), + '{0}/usr/bin'.format(tmp_path), + '{0}/usr/sbin'.format(tmp_path), + ], + ) + + LOGGER.debug('Cleaning up %s', tmp_path) + shutil.rmtree(tmp_path) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/publish/reposign.py b/publish/reposign.py new file mode 100755 index 0000000..5028b3b --- /dev/null +++ b/publish/reposign.py @@ -0,0 +1,96 @@ +#!/usr/bin/python2 +# Uses CentOS packages: python2-gnupg pexpect rpm-sign +import argparse +import contextlib +import gnupg +import logging +import os +import pexpect +import shutil +import subprocess +import sys +import tempfile +from .util import GpgHome, RpmSigner + +LOGGER = logging.getLogger(__name__) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + '--log-level', + dest='log_level', + default='info', + metavar='LEVEL', + help='Console logging lowest level displayed. Defaults to info.') + parser.add_argument( + '--keyid', + help='The key ID to sign with. Mandatory if the keyring has more than one keypair.') + parser.add_argument( + '--passphrase-fd', + type=int, + default=0, + help='The FD to read passphrase from. Defaults to stdin.') + parser.add_argument('gpgkey', type=str, + help='GPG-encoded private keypair file.') + parser.add_argument('repodir', + nargs='+', + help='RPM repository root to sign.') + args = parser.parse_args() + + log_level_name = args.log_level.upper() + logging.basicConfig(level=log_level_name) + + with contextlib.closing(GpgHome()) as gpghome: + with contextlib.closing(open(args.gpgkey, 'rb')) as gpgfile: + gpghome.gpg.import_keys(gpgfile.read()) + with contextlib.closing(os.fdopen(args.passphrase_fd, 'rb')) as ppfile: + passphrase = ppfile.read() + + # Ensure desired ID is present + use_key_id = gpghome.find_key(args.keyid) + # Verify passphrase before use + gpghome.check_key(use_key_id, passphrase) + LOGGER.info('Signing with key ID "%s"', use_key_id) + + failures = 0 + for repo_path in args.repodir: + LOGGER.info('Signing repository: %s', repo_path) + repomd_path = os.path.join(repo_path, 'repodata', 'repomd.xml') + sigfile_path = os.path.join( + repo_path, 'repodata', 'repomd.xml.asc') + pubkey_path = os.path.join(repo_path, 'repodata', 'repomd.xml.key') + try: + with contextlib.closing(open(repomd_path, 'rb')) as repomd_file: + sigdata = gpghome.gpg.sign_file( + repomd_file, + keyid=use_key_id, + passphrase=passphrase, + detach=True, + binary=False # armored + ) + with contextlib.closing(open(sigfile_path, 'wb')) as sigfile: + sigfile.write(str(sigdata)) + except Exception as err: + LOGGER.error('Failed signing: %s', err) + failures += 1 + + try: + keydata = gpghome.gpg.export_keys( + use_key_id, + armor=True + ) + with contextlib.closing(open(pubkey_path, 'wb')) as keyfile: + keyfile.write(str(keydata)) + except Exception as err: + LOGGER.error('Failed exporting: %s', err) + failures += 1 + + if failures == 0: + return 0 + else: + return 2 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/publish/rpmsign.py b/publish/rpmsign.py new file mode 100644 index 0000000..4d53b7e --- /dev/null +++ b/publish/rpmsign.py @@ -0,0 +1,73 @@ +#!/usr/bin/python2 +# Uses CentOS packages: python2-gnupg pexpect rpm-sign +import argparse +import contextlib +import gnupg +import logging +import os +import pexpect +import shutil +import subprocess +import sys +import tempfile +from .util import GpgHome, RpmSigner + +LOGGER = logging.getLogger(__name__) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + '--log-level', + dest='log_level', + default='info', + metavar='LEVEL', + help='Console logging lowest level displayed. Defaults to info.') + parser.add_argument( + '--keyid', + help='The key ID to sign with. Mandatory if the keyring has more than one keypair.') + parser.add_argument( + '--passphrase-fd', + type=int, + default=0, + help='The FD to read passphrase from. Defaults to stdin.') + parser.add_argument('gpgkey', type=str, + help='GPG-encoded private keypair file.') + parser.add_argument('rpmfile', + nargs='+', + help='RPM file(s) to sign.') + args = parser.parse_args() + + log_level_name = args.log_level.upper() + logging.basicConfig(level=log_level_name) + + with contextlib.closing(GpgHome()) as gpghome: + with contextlib.closing(open(args.gpgkey, 'rb')) as gpgfile: + gpghome.gpg.import_keys(gpgfile.read()) + with contextlib.closing(os.fdopen(args.passphrase_fd, 'rb')) as ppfile: + passphrase = ppfile.read() + + # Ensure desired ID is present + use_key_id = gpghome.find_key(args.keyid) + # Verify passphrase before use + gpghome.check_key(use_key_id, passphrase) + LOGGER.info('Signing with key ID "%s"', use_key_id) + + signer = RpmSigner(gpghome) + failures = 0 + for file_path in args.rpmfile: + LOGGER.info('Signing file: %s', file_path) + try: + signer.sign_package(file_path, use_key_id, passphrase) + except Exception as err: + LOGGER.error('Failed signing: %s', err) + failures += 1 + + if failures == 0: + return 0 + else: + return 2 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/publish/split_repos.py b/publish/split_repos.py new file mode 100755 index 0000000..1bfa5fa --- /dev/null +++ b/publish/split_repos.py @@ -0,0 +1,264 @@ +#!/usr/bin/python +''' This program takes a source RPM repository (or collection of packages) +An divides them into two categories: + - public, including only noarch and binary + - private, including SRPMs and debuginfo/debugsource binary + +''' + +import os +import argparse +import re +import shutil +import subprocess +import tempfile +import rpm +import logging +import time +from .util import clear_path, find_bin + +CREATEREPO_BIN = find_bin('createrepo') + +LOGGER = logging.getLogger(__name__) + + +def pat_match(pattern, full, positive=True): + ''' Construct a function to match a pattern or not match. + @param pattern The pattern to check. + @param full Determine if a full or partial match is required. + @param positive Determine if the function returns true or false upon a match. + @return A matching function. + ''' + re_obj = re.compile(pattern) + if full: + re_func = re_obj.match + else: + re_func = re_obj.search + + if positive: + return lambda fn: re_func(fn) is not None + else: + return lambda fn: re_func(fn) is None + + +def any_match(matchers, value): + ''' Determine if a matcher function list is non-empty and has any matches + for a value. ''' + return matchers and any(item(value) for item in matchers) + + +class ListItem(object): + ''' Keep track of anamed item to match, and whether or not any candiate text has matched yet. + ''' + + def __init__(self, exact_match): + self._text = exact_match + self._matched = False + + def matched(self): + return self._matched + + def name(self): + return self._text + + def __call__(self, text): + if self._text == text: + self._matched = True + return True + return False + + +def main(argv): + parser = argparse.ArgumentParser() + parser.add_argument( + '--log-level', + dest='log_level', + default='info', + metavar='LEVEL', + help='Console logging lowest level displayed. Defaults to info.') + parser.add_argument( + '--include', + type=str, + action='append', + default=[], + help='A regexp pattern to include if a file name matches') + parser.add_argument( + '--exclude', + type=str, + action='append', + default=[], + help='A regexp pattern to exclude if a file name matches') + parser.add_argument( + '--private', + type=str, + action='append', + default=[], + help='A regexp pattern to determine if a package name is private.') + parser.add_argument( + '--public', + type=str, + action='append', + default=[], + help='A regexp pattern to determine if a package name is public. This overrides --private arguments.') + parser.add_argument( + '--public-list', + type=str, + help='A file containing a whitelist of public package names (not file name patterns).') + parser.add_argument('--ignore-dupes', action='store_true', default=False, + help='Ignore duplicate versions of a package name.') + parser.add_argument( + '--ignore-input-signatures', + action='store_true', + default=False, + help='Ignore package signatures on inputs. Packages may be re-signed and those signatures lost anyway.') + parser.add_argument('inpath', type=str, + help='The input path to scan for files') + parser.add_argument('outpath', type=str, + help='The output path to create repositories') + args = parser.parse_args(argv[1:]) + + log_level_name = args.log_level.upper() + logging.basicConfig(level=log_level_name, format='%(message)s') + + if not os.path.exists(CREATEREPO_BIN): + raise RuntimeError('Missing "createrepo" executable') + + # include if matching + fn_incl = [] + for pat in args.include: + fn_incl.append(pat_match(pat, full=True)) + # include if not matching + fn_excl = [] + for pat in args.exclude: + fn_excl.append(pat_match(pat, full=True)) + + # patterns for private package names + priv_pat = [r'-devel$', r'-debuginfo$', r'-debugsource$'] + if args.private: + priv_pat += args.private + priv_pname = [] + for pat in priv_pat: + priv_pname.append(pat_match(pat, full=False)) + pub_pname = [] + for pat in args.public: + pub_pname.append(pat_match(pat, full=False)) + exact_names = set() + if args.public_list: + with open(args.public_list, 'r') as listfile: + for line in listfile: + line = line.strip() + if not line: + continue + matcher = ListItem(line) + exact_names.add(matcher) + pub_pname.append(matcher) + + # read package name, not file name + rpm_trans = rpm.TransactionSet() + if args.ignore_input_signatures: + rpm_trans.setVSFlags(rpm._RPMVSF_NOSIGNATURES) + + # Clean up first + if not os.path.exists(args.outpath): + os.makedirs(args.outpath) + pub_path = os.path.join(args.outpath, 'public') + priv_path = os.path.join(args.outpath, 'private') + + for repopath in (pub_path, priv_path): + LOGGER.info('Clearing path {0}'.format(repopath)) + clear_path(repopath) + + # Error on duplicates + seen_src = {} + seen_pkg = {} + + for (dirpath, dirnames, filenames) in os.walk(args.inpath): + for filename in filenames: + # Only care about RPM files + if not filename.endswith('.rpm'): + continue + if fn_incl and not any_match(fn_incl, filename): + LOGGER.info('Ignoring {0}'.format(filename)) + continue + if any_match(fn_excl, filename): + LOGGER.info('Excluding {0}'.format(filename)) + continue + + # get package info + src_path = os.path.join(dirpath, filename) + with open(src_path, 'rb') as rpmfile: + try: + head = rpm_trans.hdrFromFdno(rpmfile.fileno()) + except rpm.error as err: + LOGGER.warning('Bad header in "{0}"'.format(filename)) + raise + pkgname = head[rpm.RPMTAG_NAME] + # source rpms have no source name + is_src = not head[rpm.RPMTAG_SOURCERPM] + is_pub = any_match(pub_pname, pkgname) + is_priv = any_match(priv_pname, pkgname) + + labels = [] + if is_src: + labels.append('src') + if is_pub: + labels.append('pub') + if is_priv: + labels.append('priv') + LOGGER.info('File {fn} pkg {pkg} {labels}'.format( + fn=filename, + pkg=pkgname, + labels=','.join(labels) + )) + + if is_src: + pkggrp = seen_src + archname = 'SRPM' + else: + pkggrp = seen_pkg + archname = head[rpm.RPMTAG_ARCH] + + # check for dupes + if pkgname in pkggrp and not args.ignore_dupes: + raise ValueError( + 'Duplicate package "{0}" in "{1}" and "{2}"'.format( + pkgname, filename, pkggrp[pkgname])) + pkggrp[pkgname] = filename + + # SRPMs are always in private repos + # Any not-explicitly-public packages are private + if is_src or not is_pub: + tgtpath = priv_path + else: + tgtpath = pub_path + + arch_path = os.path.join(tgtpath, archname) + if not os.path.exists(arch_path): + os.mkdir(arch_path) + # copy, preserving permission/time + dst_path = os.path.join(tgtpath, archname, filename) + shutil.copyfile(src_path, dst_path) + shutil.copystat(src_path, dst_path) + + unmatched = set() + for matcher in exact_names: + if not matcher.matched(): + unmatched.add(matcher.name()) + if unmatched: + raise RuntimeError( + 'Unmatched whitelist names: {0}'.format(', '.join(unmatched))) + + # Build repo information + for repopath in (pub_path, priv_path): + LOGGER.info('Publishing {0}'.format(repopath)) + subprocess.check_call([ + CREATEREPO_BIN, + '--no-database', + '--simple-md-filenames', + repopath + ]) + + +if __name__ == '__main__': + import sys + sys.exit(main(sys.argv)) diff --git a/publish/take_packages.py b/publish/take_packages.py new file mode 100755 index 0000000..f39fecd --- /dev/null +++ b/publish/take_packages.py @@ -0,0 +1,184 @@ +#!/usr/bin/python +''' This program takes a source RPM repository (or collection of packages) +An moves files out of the source tree into a destination directory based +on the package name and architecture. +''' + +import os +import argparse +import re +import shutil +import rpm +import logging +import time +from .util import clear_path, find_bin + +LOGGER = logging.getLogger(__name__) + + +def any_match(matchers, value): + ''' Determine if a matcher function list is non-empty and has any matches + for a value. ''' + return any(item(value) for item in matchers) + + +class Matcher(object): + ''' Keep track of a named item to match, and whether or not any + candidate text has matched yet. + + :param re_match: A regular expression pattern to match. + :type re_match: str + ''' + + def __init__(self, re_match): + self._re = re.compile('^' + re_match + '$') + self._matched = False + + def matched(self): + ''' Determine if this matcher has ever matched a value. + :return: True if any match has occurred. + ''' + return self._matched + + def __str__(self): + return self._re.pattern + + def _check(self, text): + match = self._re.match(text) + return bool(match is not None) + + def __call__(self, text): + if self._check(text): + self._matched = True + return True + return False + + +def main(argv): + parser = argparse.ArgumentParser( + argv[0], + description='Move RPM packages into a repository tree.' + ) + parser.add_argument( + '--log-level', + dest='log_level', + default='info', + metavar='LEVEL', + help='Console logging lowest level displayed. Defaults to info.') + parser.add_argument( + '--include-pattern', + type=str, + action='append', + default=[], + help='A regexp pattern to determine if a package "name.arch" (not file name) is taken.') + parser.add_argument( + '--include-list', + type=str, + help='A text file containing an include pattern on each line.') + parser.add_argument('--ignore-dupes', action='store_true', default=False, + help='Ignore duplicate versions of a package name.') + parser.add_argument( + '--ignore-input-signatures', + action='store_true', + default=False, + help='Ignore package signatures on inputs. Packages may be re-signed and those signatures lost anyway.') + parser.add_argument('inpath', type=str, nargs='+', + help='The input path to scan for files.') + parser.add_argument('outpath', type=str, + help='The output directory to move files into.') + args = parser.parse_args(argv[1:]) + + log_level_name = args.log_level.upper() + logging.basicConfig(level=log_level_name, format='%(message)s') + + # include if matching + pkg_incl = [] + for pat in args.include_pattern: + pkg_incl.append(Matcher(pat)) + if args.include_list: + with open(args.include_list, 'r') as listfile: + for line in listfile: + line = line.strip() + if not line: + continue + pkg_incl.append(Matcher(line)) + # exclude if matching + pkg_excl = [] + + # read package name, not file name + rpm_trans = rpm.TransactionSet() + if args.ignore_input_signatures: + rpm_trans.setVSFlags(rpm._RPMVSF_NOSIGNATURES) + + # Clean up first + tgtpath = args.outpath + if not os.path.exists(tgtpath): + os.makedirs(tgtpath) + LOGGER.info('Clearing output: %s', tgtpath) + clear_path(tgtpath) + + # Error on duplicates + seen_fullnames = {} + + for inpath in args.inpath: + LOGGER.info('Scanning input: %s', inpath) + for (dirpath, dirnames, filenames) in os.walk(inpath): + for filename in filenames: + # Only care about RPM files + if not filename.endswith('.rpm'): + continue + + # get package info + src_path = os.path.join(dirpath, filename) + rel_path = os.path.relpath(src_path, inpath) + with open(src_path, 'rb') as rpmfile: + try: + head = rpm_trans.hdrFromFdno(rpmfile.fileno()) + except rpm.error as err: + LOGGER.warning('Bad header in "%s"', rel_path) + raise + pkgname = head[rpm.RPMTAG_NAME] + # source rpms have no source name, + # so use the same name as 'yum' does + if not head[rpm.RPMTAG_SOURCERPM]: + archname = 'src' + else: + archname = head[rpm.RPMTAG_ARCH] + + fullname = '{0}.{1}'.format(pkgname, archname) + LOGGER.info('File %s name %s', rel_path, fullname) + + if not any_match(pkg_incl, fullname): + LOGGER.info('Ignoring name %s', fullname) + continue + if pkg_excl and any_match(pkg_excl, fullname): + LOGGER.info('Excluding name %s', fullname) + continue + + # check for dupes + if fullname in seen_fullnames and not args.ignore_dupes: + raise ValueError( + 'Duplicate package "{0}" in "{1}" and "{2}"'.format( + fullname, rel_path, seen_fullnames[fullname])) + seen_fullnames[fullname] = rel_path + + # move, preserving permission/time and relative tree structure + dst_path = os.path.join(tgtpath, rel_path) + dst_dir = os.path.dirname(dst_path) + if not os.path.exists(dst_dir): + os.makedirs(dst_dir) + LOGGER.info('Moving %s to %s', fullname, dst_path) + shutil.move(src_path, dst_path) + + unmatched = set() + for matcher in pkg_incl: + if not matcher.matched(): + unmatched.add(str(matcher)) + if unmatched: + raise RuntimeError( + 'Unmatched include names: {0}'.format(', '.join(unmatched))) + + +if __name__ == '__main__': + import sys + sys.exit(main(sys.argv)) diff --git a/publish/test/__init__.py b/publish/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/publish/test/test_take_packages.py b/publish/test/test_take_packages.py new file mode 100644 index 0000000..cc531dd --- /dev/null +++ b/publish/test/test_take_packages.py @@ -0,0 +1,29 @@ + +import os +import shutil +import tempfile +import unittest +from ..take_packages import main + + +class TestTakePackages(unittest.TestCase): + + def setUp(self): + unittest.TestCase.setUp(self) + self._testdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self._testdir) + self._testdir = None + unittest.TestCase.tearDown(self) + + def test_split(self): + inpath_a = os.path.join(self._testdir, 'input-a') + os.mkdir(inpath_a) + inpath_b = os.path.join(self._testdir, 'input-b') + os.mkdir(inpath_b) + outpath = os.path.join(self._testdir, 'output') + + self.assertFalse(os.path.exists(outpath)) + main(['take_packages', '--log=debug', inpath_a, inpath_b, outpath]) + self.assertTrue(os.path.exists(outpath)) diff --git a/publish/util.py b/publish/util.py new file mode 100644 index 0000000..01280ac --- /dev/null +++ b/publish/util.py @@ -0,0 +1,174 @@ +#!/usr/bin/python2 +import gnupg +import logging +import os +import pexpect +import shutil +import subprocess +import tempfile + +LOGGER = logging.getLogger(__name__) + + +def find_bin(name): + ''' Search the system paths for an executable. + + :param name: The name to search for. + :type name: str + :return: The path to the executable. + :rtype: str + :raise RuntimeError: if the name could not be found. + ''' + import distutils.spawn + path = distutils.spawn.find_executable(name) + if not path: + raise RuntimeError('Executable not found for "{0}"'.format(name)) + return path + + +def clear_path(path): + ''' Either create a path or remove all items under (but not including) the path. ''' + if os.path.exists(path): + for (dirpath, dirnames, filenames) in os.walk(path, topdown=False): + for dirname in dirnames: + os.rmdir(os.path.join(dirpath, dirname)) + for filename in filenames: + os.unlink(os.path.join(dirpath, filename)) + else: + os.mkdir(path) + + +class GpgHome(object): + ''' A class to implement a local, temporary GPG configuration. + + :ivar gpghome: The path to the GPG configuration. + :ivar gpg: The :py:cls:`gnupg.GPG` object being used in that home. + ''' + + def __init__(self, gpg_bin=None): + self._gpg_bin = gpg_bin or find_bin('gpg2') + self.gpghome = os.path.abspath(tempfile.mkdtemp()) + self.gpg = gnupg.GPG( + gpgbinary=self._gpg_bin, + gnupghome=self.gpghome + ) + + def __del__(self): + if self.gpghome: + self.close() + + def close(self): + ''' Delete the local configuration. + + This can only be called once per instance. + ''' + if not self.gpghome: + raise RuntimeError('Attempt to close multiple times') + del self.gpg + shutil.rmtree(self.gpghome) + self.gpghome = None + + def find_key(self, key_id=None): + ''' Search for a specific key, or use the only one present. + + :param key_id: The optional key to search for. + If not provided and the keyring has only a single private key + then that key is used. + :type key_id: str or None + :return: The found key ID. + :rtype: str + :raise RuntimeError: if the desired key cannot be found. + ''' + key_infos = self.gpg.list_keys() + if not key_infos: + raise RuntimeError('GPG file has no keys') + LOGGER.debug('Keyring has %d keys', len(key_infos)) + + found_id = None + for key_info in key_infos: + LOGGER.debug('Keyring has ID %s', key_info['keyid']) + if key_id is None or key_info['keyid'] == key_id: + found_id = key_info['keyid'] + break + + if not found_id: + if key_id: + raise RuntimeError('Key ID "{0}" not found'.format(args.keyid)) + else: + raise RuntimeError('Key ID required but not provided') + return found_id + + def check_key(self, key_id, passphrase): + ''' Verify a valid key and passphrase pairing. + + :param str key_id: The GPG key to sign with. + :param str passphrase: The passphrase to unlock the private key. + :except Exception: If the verification fails for any reason. + ''' + sig = self.gpg.sign('test', keyid=key_id, passphrase=passphrase) + if not sig: + raise RuntimeError('Failed to verify passphrase') + + +class RpmSigner(object): + ''' A class to wrap the `rpmsign` shell utility. + + This is based on example at http://aaronhawley.livejournal.com/10615.html + + :param gpghome: The GPG home directory to sign with. + ''' + + def __init__(self, gpghome, rpmsign_bin=None): + if isinstance(gpghome, GpgHome): + self._gpghome = gpghome.gpghome + else: + self._gpghome = gpghome + self._rpmsign_bin = rpmsign_bin or find_bin('rpmsign') + + def sign_package(self, file_path, key_id, passphrase): + ''' Sign an individual package. + + :param str file_path: The file path to sign. + :param str key_id: The GPG key to sign with. + :param str passphrase: The passphrase to unlock the private key. + :except Exception: If the signing fails for any reason. + ''' + digest_algo = 'sha256' + cmd = [ + self._rpmsign_bin, + '--define', '_gpg_name {0}'.format(key_id), + '--define', '_gpg_digest_algo {0}'.format(digest_algo), + '--addsign', + '-v', + file_path, + ] + LOGGER.debug( + 'Calling ' + ' '.join(['"{0}"'.format(arg.replace('"', '\\"')) for arg in cmd])) + + try: + os.environ['GNUPGHOME'] = self._gpghome + # On CentOS-7 wait for prompt then provide password + proc = pexpect.spawn(cmd[0], cmd[1:]) + proc.expect('Enter pass phrase:') + proc.sendline(passphrase) + + outstr = '' + while True: + try: + # Signing may take considerable time on large (100+ MiB) + # packages + outstr += proc.read_nonblocking(size=100, timeout=300) + except pexpect.EOF: + pass + if not proc.isalive(): + break + outstr += proc.read() + exitcode = proc.exitstatus + + if exitcode != 0: + LOGGER.error('Failed with error:\n%s', outstr) + raise subprocess.CalledProcessError(exitcode, cmd[0], outstr) + else: + LOGGER.debug('Success with output:\n%s', outstr) + finally: + del os.environ['GNUPGHOME'] diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..6ec5b01 --- /dev/null +++ b/pylintrc @@ -0,0 +1,424 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +msg-template={path}:{line}: [{msg_id}, {obj}] {msg} ({symbol}) + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[BASIC] + +# Naming hint for argument names +argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct argument names +argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for attribute names +attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct attribute names +attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming hint for function names +function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct function names +function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for method names +method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct method names +method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming hint for variable names +variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: en_US (myspell). +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/rabbitmq/Dockerfile b/rabbitmq/Dockerfile new file mode 100644 index 0000000..4287651 --- /dev/null +++ b/rabbitmq/Dockerfile @@ -0,0 +1,19 @@ +FROM rabbitmq:3.12.2-alpine + +# LOG level for console output (debug, info, warning, error, critical, none) +ENV RMQ_LOG_CONSOLE_LEVEL=warning + +COPY rabbitmq/rabbitmq.conf /etc/rabbitmq/ +RUN echo log.console.level=$RMQ_LOG_CONSOLE_LEVEL >> /etc/rabbitmq/rabbitmq.conf + +COPY rabbitmq/definitions.json /etc/rabbitmq/ +# Add debugging env if configured +ARG AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +COPY rabbitmq/devel.sh / +RUN chmod +x /devel.sh +RUN /devel.sh +# +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["rabbitmq-server"] +HEALTHCHECK --start-period=60s --interval=20s --timeout=5s --retries=3 \ + CMD rabbitmq-diagnostics -q check_port_connectivity || exit 1 diff --git a/rabbitmq/definitions.json b/rabbitmq/definitions.json new file mode 100644 index 0000000..403ee1a --- /dev/null +++ b/rabbitmq/definitions.json @@ -0,0 +1,38 @@ +{ + "users": [ + { + "name": "celery", + "password": "celery", + "tags": "administrator" + }, + { + "name": "rcache", + "password": "rcache", + "tags": "administrator" + } + ], + "vhosts": [ + { + "name": "fbrat" + }, + { + "name": "rcache" + } + ], + "permissions": [ + { + "user": "celery", + "vhost": "fbrat", + "configure": ".*", + "write": ".*", + "read": ".*" + }, + { + "user": "rcache", + "vhost": "rcache", + "configure": ".*", + "write": ".*", + "read": ".*" + } + ] +} diff --git a/rabbitmq/devel.sh b/rabbitmq/devel.sh new file mode 100644 index 0000000..2a7c9b5 --- /dev/null +++ b/rabbitmq/devel.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# +# Copyright 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +set eux +rabbitmq-plugins enable --offline rabbitmq_management +rm -f /etc/rabbitmq/conf.d/20-management_agent.disable_metrics_collector.conf +cp /plugins/rabbitmq_management-*/priv/www/cli/rabbitmqadmin /usr/local/bin/rabbitmqadmin +chmod +x /usr/local/bin/rabbitmqadmin +apk add --no-cache python3 +rabbitmqadmin --version + +AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +case "$AFC_DEVEL_ENV" in + "devel") + echo "Debug profile" + cat << EOF >> /etc/rabbitmq/rabbitmq.conf +log.console.level = debug +log.connection.level = debug +log.channel.level = debug +log.queue.level = debug +log.default.level = debug +EOF + ;; + "production") + echo "Production profile" + ;; + *) + echo "Uknown profile" + ;; +esac + +exit $? diff --git a/rabbitmq/rabbitmq.conf b/rabbitmq/rabbitmq.conf new file mode 100644 index 0000000..6801782 --- /dev/null +++ b/rabbitmq/rabbitmq.conf @@ -0,0 +1,5 @@ +loopback_users.guest = false +listeners.tcp.default = 5672 +management.tcp.port = 15672 +# +management.load_definitions = /etc/rabbitmq/definitions.json diff --git a/rat_server/Dockerfile b/rat_server/Dockerfile new file mode 100644 index 0000000..af44898 --- /dev/null +++ b/rat_server/Dockerfile @@ -0,0 +1,92 @@ +# +# Copyright 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +# Install required packages +# +FROM httpd:2.4.58-alpine3.18 as rat_server.preinstall +ENV PYTHONUNBUFFERED=1 +RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python && \ +apk add --update --no-cache py3-six py3-numpy py3-cryptography py3-sqlalchemy \ +py3-requests py3-flask py3-psycopg2 py3-pydantic=~1.10 postfix && \ +apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \ +py3-confluent-kafka && \ +python3 -m ensurepip && \ +pip3 install --no-cache --upgrade pip setuptools +COPY rat_server/requirements.txt /wd/ +RUN pip3 install -r /wd/requirements.txt && mkdir -p /etc/xdg/fbrat +COPY config/ratapi.conf /etc/xdg/fbrat/ +RUN echo "DEFAULT_ULS_DIR = '/mnt/nfs/rat_transfer/ULS_Database'" >> /etc/xdg/fbrat/ratapi.conf +RUN echo "AFC_APP_TYPE = 'server'" >> /etc/xdg/fbrat/ratapi.conf +# +# Build Message Handler application +# +FROM httpd:2.4.58-alpine3.18 as rat_server.build +COPY --from=rat_server.preinstall / / +# Development env +RUN apk add --update --no-cache build-base cmake ninja yarn \ + apr-util-dev python3-dev +RUN pip3 install --no-cache --upgrade mod_wsgi \ + && mod_wsgi-express install-module +# +# +COPY CMakeLists.txt LICENSE.txt version.txt Doxyfile.in /wd/ +COPY cmake /wd/cmake/ +COPY pkg /wd/pkg/ +COPY src /wd/src/ +RUN mkdir -p -m 777 /wd/build +ARG BUILDREV=localbuild +ARG NODE_OPTIONS=--openssl-legacy-provider +RUN cd /wd/build && \ +cmake -DCMAKE_INSTALL_PREFIX=/wd/__install -DCMAKE_PREFIX_PATH=/usr -DCMAKE_BUILD_TYPE=RatapiWebDebug -DSVN_LAST_REVISION=$BUILDREV -G Ninja /wd && \ +ninja -j$(nproc) install +# +# Install Message Handler application +# +FROM httpd:2.4.58-alpine3.18 as install_image +COPY --from=rat_server.preinstall / / +COPY --from=rat_server.build /wd/__install /usr/ +COPY --from=rat_server.build /usr/lib/python3.11/site-packages/mod_wsgi /usr/lib/python3.11/site-packages/mod_wsgi +# +COPY src/afc-packages /wd/afc-packages +RUN pip3 install --use-pep517 --root-user-action=ignore \ + -r /wd/afc-packages/pkgs.rat_server \ + && rm -rf /wd/afc-packages +# +# Add user and group +RUN addgroup -g 1003 fbrat && \ +adduser -g '' -D -u 1003 -G fbrat -h /var/lib/fbrat -s /sbin/nologin fbrat +RUN mkdir -p /var/spool/fbrat /var/lib/fbrat /mnt/nfs /var/lib/fbrat/daily_uls_parse +RUN chown fbrat:fbrat /var/spool/fbrat /usr/share/fbrat /var/lib/fbrat /var/lib/fbrat/daily_uls_parse +# +LABEL revision="afc-rat_server" +WORKDIR /wd +EXPOSE 80 443 +COPY rat_server/httpd.conf /usr/local/apache2/conf/ +COPY rat_server/httpd-default.conf rat_server/httpd-info.conf rat_server/httpd-vhosts.conf \ +rat_server/httpd-mpm.conf rat_server/httpd-ssl.conf /usr/local/apache2/conf/extra/ +COPY rat_server/fbrat.wsgi /usr/local/apache2/fbrat.wsgi +RUN mkdir /wd/private +COPY rat_server/copy-private.sh private* /wd/private/ +RUN chmod +x /wd/private/copy-private.sh +RUN /wd/private/copy-private.sh +RUN rm -rf /wd/private + +RUN mkdir -p /usr/share/ca-certificates/certs +COPY rat_server/http.key rat_server/http.crt /usr/share/ca-certificates/certs/ +ENV XDG_DATA_DIRS=$XDG_DATA_DIRS:/usr/local/share:/usr/share:/usr/share/fbrat:/mnt/nfs +# Add debugging env if configured +ARG AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +COPY rat_server/devel.sh /wd/ +RUN chmod +x /wd/devel.sh +RUN /wd/devel.sh +# TODO to rename following variable after source split +ENV AFC_MSGHND_RATAFC_TOUT=${AFC_MSGHND_RATAFC_TOUT:-600} +ENV AFC_RATAPI_LOG_LEVEL=${AFC_RATAPI_LOG_LEVEL:-WARNING} +COPY rat_server/entrypoint.sh / +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] +HEALTHCHECK CMD wget --no-verbose --tries=1 --spider localhost/fbrat/ratapi/v1/healthy || exit 1 +# diff --git a/rat_server/README.md b/rat_server/README.md new file mode 100644 index 0000000..e8e2fab --- /dev/null +++ b/rat_server/README.md @@ -0,0 +1,25 @@ +## Overview +The rat_server component is an apache web server (httpd) that gives an entry point for the REST API calls and serves up the web user interface. It does not handle the MTLS or SSL authentication by default; those are managed by the dispatcher component. + +The pages that are served are kept in the src/ratapi and src/web directories. + +## Environment and configuration +The configuration for the server is managed by the *.conf files in this directory. While rat_server has no specific configuration via the environment, it uses the Rabbit MQ (rmq), Object Storage (objst), ALS, and Rcache components and needs to have parameters set in accord with those components. The example below provides the variables to be defined. You can see the (readme)[../README.md] for the values set by the sample .env file included there. +``` + # RabbitMQ server name: + - BROKER_TYPE=external + - BROKER_FQDN=rmq + # Filestorage params: + - AFC_OBJST_HOST=objst + - AFC_OBJST_PORT=5000 + - AFC_OBJST_SCHEME=HTTP + # ALS params + - ALS_KAFKA_SERVER_ID=rat_server + - ALS_KAFKA_CLIENT_BOOTSTRAP_SERVERS=${ALS_KAFKA_SERVER_}:${ALS_KAFKA_CLIENT_PORT_} + - ALS_KAFKA_MAX_REQUEST_SIZE=${ALS_KAFKA_MAX_REQUEST_SIZE_} + # Rcache parameters + - RCACHE_ENABLED=${RCACHE_ENABLED} + - RCACHE_POSTGRES_DSN=postgresql://postgres:postgres@bulk_postgres/rcache + - RCACHE_SERVICE_URL=http://rcache:${RCACHE_CLIENT_PORT} + - RCACHE_RMQ_DSN=amqp://rcache:rcache@rmq:5672/rcache +``` \ No newline at end of file diff --git a/rat_server/copy-private.sh b/rat_server/copy-private.sh new file mode 100755 index 0000000..2d2f89a --- /dev/null +++ b/rat_server/copy-private.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +if [ -e /wd/private/images ]; then + cp -R /wd/private/images/* /usr/share/fbrat/www/images/ +fi + +if [ -e /wd/private/templates ]; then + cp -R /wd/private/templates/* /usr/lib/python3.11/site-packages/ratapi/templates/ +fi + diff --git a/rat_server/devel.sh b/rat_server/devel.sh new file mode 100644 index 0000000..36481eb --- /dev/null +++ b/rat_server/devel.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# +# Copyright 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +AFC_DEVEL_ENV=${AFC_DEVEL_ENV:=production} +case "$AFC_DEVEL_ENV" in + "devel") + echo "Debug profile" + export NODE_OPTIONS='--openssl-legacy-provider' + apk add --update --no-cache cmake ninja yarn bash + ;; + "production") + echo "Production profile" + ;; + *) + echo "Uknown profile" + ;; +esac + +exit $? diff --git a/rat_server/entrypoint.sh b/rat_server/entrypoint.sh new file mode 100644 index 0000000..8a20efc --- /dev/null +++ b/rat_server/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +echo "httpd $HTTPD_OPTIONS -DFOREGROUND >" +httpd $HTTPD_OPTIONS -DFOREGROUND & +# +postfix start +sleep infinity + +exit $? diff --git a/rat_server/fbrat.wsgi b/rat_server/fbrat.wsgi new file mode 100644 index 0000000..fb14fd7 --- /dev/null +++ b/rat_server/fbrat.wsgi @@ -0,0 +1,12 @@ +# Entry point for the RAT application +import ratapi + +# Flask application +app = ratapi.create_app( + config_override={ + 'APPLICATION_ROOT': '/fbrat', + } +) + +# Expose WSGI +application = app.wsgi_app diff --git a/rat_server/http.crt b/rat_server/http.crt new file mode 100644 index 0000000..450d266 --- /dev/null +++ b/rat_server/http.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID6jCCAtKgAwIBAgICK4EwDQYJKoZIhvcNAQELBQAwgakxCzAJBgNVBAYTAi0t +MRIwEAYDVQQIDAlTb21lU3RhdGUxETAPBgNVBAcMCFNvbWVDaXR5MRkwFwYDVQQK +DBBTb21lT3JnYW5pemF0aW9uMR8wHQYDVQQLDBZTb21lT3JnYW5pemF0aW9uYWxV +bml0MRUwEwYDVQQDDAwwMWEzZDBlMWNhNjkxIDAeBgkqhkiG9w0BCQEWEXJvb3RA +MDFhM2QwZTFjYTY5MB4XDTIzMDExNzAxMTYwMVoXDTI0MDExNzAxMTYwMVowgakx +CzAJBgNVBAYTAi0tMRIwEAYDVQQIDAlTb21lU3RhdGUxETAPBgNVBAcMCFNvbWVD +aXR5MRkwFwYDVQQKDBBTb21lT3JnYW5pemF0aW9uMR8wHQYDVQQLDBZTb21lT3Jn +YW5pemF0aW9uYWxVbml0MRUwEwYDVQQDDAwwMWEzZDBlMWNhNjkxIDAeBgkqhkiG +9w0BCQEWEXJvb3RAMDFhM2QwZTFjYTY5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA9TnAjkZIlTlnq9M/Sejn+X9av8uqbsEBKkCAdIGAYOUihn2Awbhw +UQKigAdpH3sb42A6kHEySLvq7cqqJsgspPmmCGXCbeFrpBWaqF5NMTIpsJX1ljd2 +N3abPpp0Ph1hS9MJBL491Ia12krLaxZ/Vf5387+up1OL8ftSmOAdjwqPN1eKTAsy +DAul94a4s+Ur3PFPHyRWCiMvjV3Y6MMNSPGXSYxyBC/GEH5WLZsO1uO2CahZLDLz +XlJqZAtWnnzS5V+5tNvxbBhj3EwPcvWS8+pqUX8k0mCVUwFY+tPuNjhDVPucjXbk +HSBtHkORFNohdHxA/VGbj6uDKGEyZMe7RQIDAQABoxowGDAJBgNVHRMEAjAAMAsG +A1UdDwQEAwIF4DANBgkqhkiG9w0BAQsFAAOCAQEAn1LIxyoQ3t2UlKcZwjQk0dvc +I6JuzSMRbt/wmshKuzmsE5YF6+PCqSH+Ol3rUEea8MXCxzcbPq2mKH/edHEnrTGw +h6dJwXb8CRav6NexfvKmJ0MxjuAJS9NSnitxzeiSQdjKsXOcw+sKqtU8PM7hhB/l +87CU1cYRGCtnuiI2ntY1Tzblx2VvvO7V0RcgRi+4xDGs256AE0RkznjT1ylKV0wI +6Z0bK2+FF896gkG4Y8pbPyFM8/5FRYytiSVzxh1AWaZGs6vZHczxWNqdVbKDooos +EgY8Huzk0U8+W33WbrU/rKIU6CkZeBmJtscPHOYpIe2Oivyd/m3JdObCzZQ9OQ== +-----END CERTIFICATE----- diff --git a/rat_server/http.key b/rat_server/http.key new file mode 100644 index 0000000..75657a6 --- /dev/null +++ b/rat_server/http.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA9TnAjkZIlTlnq9M/Sejn+X9av8uqbsEBKkCAdIGAYOUihn2A +wbhwUQKigAdpH3sb42A6kHEySLvq7cqqJsgspPmmCGXCbeFrpBWaqF5NMTIpsJX1 +ljd2N3abPpp0Ph1hS9MJBL491Ia12krLaxZ/Vf5387+up1OL8ftSmOAdjwqPN1eK +TAsyDAul94a4s+Ur3PFPHyRWCiMvjV3Y6MMNSPGXSYxyBC/GEH5WLZsO1uO2CahZ +LDLzXlJqZAtWnnzS5V+5tNvxbBhj3EwPcvWS8+pqUX8k0mCVUwFY+tPuNjhDVPuc +jXbkHSBtHkORFNohdHxA/VGbj6uDKGEyZMe7RQIDAQABAoIBAQCtmLahgUu8p6im +UKNK9R/S1b8ua0U5plPmz0agM1ToQw0P3CSb/q80CgNzUsuuR35UljifLCMGrlD/ +CSsuzSIdn8VTyIW9N4j13X1gl3FZ1EMDCQWT06tSVBpOVRTZK/9GqByISQyIONzf +rXcXVhPKkpvj59wCA/jb0qiEJJOIP/SkxQnh8SeuwXJh/RDc4xl2n+QWwDyOXa/A +QOtWBhp0Y+2xCQSWcrH+lnf2uWPRV5XxnCtS0knnXjZeOCIZnNGyjg50hDCprlMM +OzbWITMmGnyFutoyqKgEhZH5izaCkitaWsnPEfyNCetNATD9X+osqJbIRv/oi0Ei +GSRuCzKBAoGBAPy/c37jZkd722vk8Z6EJfUaKbMR3Ob/f74Ql/fy8elNmg8ZDXPK +/NluUJGglq+0giDm536VSdwu+HIAaaaF1bFN1pJp41qRRZ+ArM5kPAdrpWzhBJ06 +3c/H0MKyPlo0lj+RRDYYeSotAsF5baoPNwu9tYtHnDwC1jOwAJWP+uQ1AoGBAPhh +hdPQwyVL/cOuteKhys+LI+Yh5G82/Y0FtsAzT+hMAFNmPK4sAQGpLxxx/mMdPeKl +OB/0rHozd5pnrdNbgNn46wDaA9QPhn5rlvAt0A/x6xO1aZFfUoIE/tLJixrSfqf0 +MkXsha4GSNGrJTew/2cW8FzHFIjtIE0cs1iOezzRAoGBAOgvRH2Dj1kJ6iey+VgC +1A/XCgmr1kAK5SIIgmLQNvV5SZanEBmYlzFzSuaNHQCqlYR+OpmpsWFpcOjhgizs +88Ne20hDqA1yOQBvJ9CuegrjE+PyztdV1aDkUd4Z+nfJqWEaJQyA7QYWtVphH6JD +OfE6RMV/coIapQZ0oATFcNklAoGAM71890bBn/9YeW7njLJPYTSG4eWlhFVcNAhA +rYEC4E0UtErT0SRxgTsRCUflyhfJUHFCY8XAOCSIga2fVCv3h3CG48KGkaI6ThNz +eczRTsECSzS7LQFCWrtXqek6BPhcUfhYkKBYqIu+l46jThqc2Yi5wVnnOS7MT8Pu +yD/GBEECgYBN45MEB1y4dAngspoOJ2zHoXh3B3ILKGEfJsQZGSwRYFyO/o6dLplz +tyvGqqMFGsjPu+q5z7skPmQ2vxQ4sSSS2+Y/X3X0qEQWKlfCU4j4ECDyWym6+9ct +oPydKY99dcqjxXbjmLfH8lDeXaQM7ZnXhUPUVF4L0v5WAkXlX2V1mQ== +-----END RSA PRIVATE KEY----- diff --git a/rat_server/httpd-default.conf b/rat_server/httpd-default.conf new file mode 100644 index 0000000..eab319c --- /dev/null +++ b/rat_server/httpd-default.conf @@ -0,0 +1,93 @@ +# +# This configuration file reflects default settings for Apache HTTP Server. +# +# You may change these, but chances are that you may not need to. +# + +# +# Timeout: The number of seconds before receives and sends time out. +# +Timeout 900 + +# +# KeepAlive: Whether or not to allow persistent connections (more than +# one request per connection). Set to "Off" to deactivate. +# +KeepAlive On + +# +# MaxKeepAliveRequests: The maximum number of requests to allow +# during a persistent connection. Set to 0 to allow an unlimited amount. +# We recommend you leave this number high, for maximum performance. +# +MaxKeepAliveRequests 100 + +# +# KeepAliveTimeout: Number of seconds to wait for the next request from the +# same client on the same connection. +# +KeepAliveTimeout 15 + +# +# UseCanonicalName: Determines how Apache constructs self-referencing +# URLs and the SERVER_NAME and SERVER_PORT variables. +# When set "Off", Apache will use the Hostname and Port supplied +# by the client. When set "On", Apache will use the value of the +# ServerName directive. +# +UseCanonicalName Off + +# +# AccessFileName: The name of the file to look for in each directory +# for additional configuration directives. See also the AllowOverride +# directive. +# +AccessFileName .htaccess + + Require all denied + + +# +# ServerTokens +# This directive configures what you return as the Server HTTP response +# Header. The default is 'Full' which sends information about the OS-Type +# and compiled in modules. +# Set to one of: Full | OS | Minor | Minimal | Major | Prod +# where Full conveys the most information, and Prod the least. +# +ServerTokens OS + +# +# Optionally add a line containing the server version and virtual host +# name to server-generated pages (internal error documents, FTP directory +# listings, mod_status and mod_info output etc., but not CGI generated +# documents or custom error documents). +# Set to "EMail" to also include a mailto: link to the ServerAdmin. +# Set to one of: On | Off | EMail +# +ServerSignature On + +# +# HostnameLookups: Log the names of clients or just their IP addresses +# e.g., www.apache.org (on) or 204.62.129.132 (off). +# The default is off because it'd be overall better for the net if people +# had to knowingly turn this feature on, since enabling it means that +# each client request will result in AT LEAST one lookup request to the +# nameserver. +# +HostnameLookups Off + +# +# Set a timeout for how long the client may take to send the request header +# and body. +# The default for the headers is header=20-40,MinRate=500, which means wait +# for the first byte of headers for 20 seconds. If some data arrives, +# increase the timeout corresponding to a data rate of 500 bytes/s, but not +# above 40 seconds. +# The default for the request body is body=20,MinRate=500, which is the same +# but has no upper limit for the timeout. +# To disable, set to header=0 body=0 +# + + RequestReadTimeout header=20-40,MinRate=500 body=20,MinRate=500 + diff --git a/rat_server/httpd-info.conf b/rat_server/httpd-info.conf new file mode 100644 index 0000000..2ae154a --- /dev/null +++ b/rat_server/httpd-info.conf @@ -0,0 +1,42 @@ +# +# Get information about the requests being processed by the server +# and the configuration of the server. +# +# Required modules: mod_authz_core, mod_authz_host, +# mod_info (for the server-info handler), +# mod_status (for the server-status handler) + +# +# Allow server status reports generated by mod_status, +# with the URL of http://servername/server-status +# Change the ".example.com" to match your domain to enable. + +# +# SetHandler server-status +# Require host .example.com +# Require ip 127 +# + +# +# ExtendedStatus controls whether Apache will generate "full" status +# information (ExtendedStatus On) or just basic information (ExtendedStatus +# Off) when the "server-status" handler is called. The default is Off. +# +#ExtendedStatus On + +# +# Allow remote server configuration reports, with the URL of +# http://servername/server-info (requires that mod_info.c be loaded). +# Change the ".example.com" to match your domain to enable. +# +# +# SetHandler server-info +# Require host .example.com +# Require ip 127 +# + + + + + + diff --git a/rat_server/httpd-mpm.conf b/rat_server/httpd-mpm.conf new file mode 100644 index 0000000..b05f061 --- /dev/null +++ b/rat_server/httpd-mpm.conf @@ -0,0 +1,35 @@ +# +# Server-Pool Management (MPM specific) +# + +# prefork MPM +# StartServers: number of server processes to start +# MinSpareServers: minimum number of server processes which are kept spare +# MaxSpareServers: maximum number of server processes which are kept spare +# MaxRequestWorkers: maximum number of server processes allowed to start +# MaxConnectionsPerChild: maximum number of connections a server process serves +# before terminating +# +# StartServers 5 +# MinSpareServers 5 +# MaxSpareServers 10 +# MaxRequestWorkers 250 +# MaxConnectionsPerChild 0 +# + +# worker MPM +# StartServers: initial number of server processes to start +# MinSpareThreads: minimum number of worker threads which are kept spare +# MaxSpareThreads: maximum number of worker threads which are kept spare +# ThreadsPerChild: constant number of worker threads in each server process +# MaxRequestWorkers: maximum number of worker threads +# MaxConnectionsPerChild: maximum number of connections a server process serves +# before terminating + + StartServers 2 + MinSpareThreads 3 + MaxSpareThreads 6 + ThreadsPerChild 3 + MaxRequestWorkers 30 + MaxConnectionsPerChild 0 + diff --git a/rat_server/httpd-ssl.conf b/rat_server/httpd-ssl.conf new file mode 100644 index 0000000..eb7ee2e --- /dev/null +++ b/rat_server/httpd-ssl.conf @@ -0,0 +1,288 @@ +# +# This is the Apache server configuration file providing SSL support. +# It contains the configuration directives to instruct the server how to +# serve pages over an https connection. For detailed information about these +# directives see +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. You have been warned. +# +# Required modules: mod_log_config, mod_setenvif, mod_ssl, +# socache_shmcb_module (for default value of SSLSessionCache) + +# +# Pseudo Random Number Generator (PRNG): +# Configure one or more sources to seed the PRNG of the SSL library. +# The seed data should be of good random quality. +# WARNING! On some platforms /dev/random blocks if not enough entropy +# is available. This means you then cannot use the /dev/random device +# because it would lead to very long connection times (as long as +# it requires to make more entropy available). But usually those +# platforms additionally provide a /dev/urandom device which doesn't +# block. So, if available, use this one instead. Read the mod_ssl User +# Manual for more details. +# +#SSLRandomSeed startup file:/dev/random 512 +#SSLRandomSeed startup file:/dev/urandom 512 +#SSLRandomSeed connect file:/dev/random 512 +#SSLRandomSeed connect file:/dev/urandom 512 + +# +# When we also provide SSL we have to listen to the +# standard HTTP port (see above) and to the HTTPS port +# +Listen 443 + +## +## SSL Global Context +## +## All SSL configuration in this context applies both to +## the main server and all SSL-enabled virtual hosts. +## + +# SSL Cipher Suite: +# List the ciphers that the client is permitted to negotiate, +# and that httpd will negotiate as the client of a proxied server. +# See the OpenSSL documentation for a complete list of ciphers, and +# ensure these follow appropriate best practices for this deployment. +# httpd 2.2.30, 2.4.13 and later force-disable aNULL, eNULL and EXP ciphers, +# while OpenSSL disabled these by default in 0.9.8zf/1.0.0r/1.0.1m/1.0.2a. +#SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5:!RC4 +#SSLProxyCipherSuite HIGH:MEDIUM:!MD5:!RC4:!3DES +SSLCipherSUite "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-GCM-SHA384" + +# By the end of 2016, only TLSv1.2 ciphers should remain in use. +# Older ciphers should be disallowed as soon as possible, while the +# kRSA ciphers do not offer forward secrecy. These changes inhibit +# older clients (such as IE6 SP2 or IE8 on Windows XP, or other legacy +# non-browser tooling) from successfully connecting. +# +# To restrict mod_ssl to use only TLSv1.2 ciphers, and disable +# those protocols which do not support forward secrecy, replace +# the SSLCipherSuite and SSLProxyCipherSuite directives above with +# the following two directives, as soon as practical. +# SSLCipherSuite HIGH:MEDIUM:!SSLv3:!kRSA +# SSLProxyCipherSuite HIGH:MEDIUM:!SSLv3:!kRSA + +# User agents such as web browsers are not configured for the user's +# own preference of either security or performance, therefore this +# must be the prerogative of the web server administrator who manages +# cpu load versus confidentiality, so enforce the server's cipher order. +SSLHonorCipherOrder on + +# SSL Protocol support: +# List the protocol versions which clients are allowed to connect with. +# Disable SSLv3 by default (cf. RFC 7525 3.1.1). TLSv1 (1.0) should be +# disabled as quickly as practical. By the end of 2016, only the TLSv1.2 +# protocol or later should remain in use. +SSLProtocol +TLSv1.2 +TLSv1.3 +SSLProxyProtocol +TLSv1.2 +TLSv1.3 + +# Pass Phrase Dialog: +# Configure the pass phrase gathering process. +# The filtering dialog program (`builtin' is an internal +# terminal dialog) has to provide the pass phrase on stdout. +SSLPassPhraseDialog builtin + +# Inter-Process Session Cache: +# Configure the SSL Session Cache: First the mechanism +# to use and second the expiring timeout (in seconds). +#SSLSessionCache "dbm:/usr/local/apache2/logs/ssl_scache" +SSLSessionCache "shmcb:/usr/local/apache2/logs/ssl_scache(512000)" +SSLSessionCacheTimeout 300 + +# OCSP Stapling (requires OpenSSL 0.9.8h or later) +# +# This feature is disabled by default and requires at least +# the two directives SSLUseStapling and SSLStaplingCache. +# Refer to the documentation on OCSP Stapling in the SSL/TLS +# How-To for more information. +# +# Enable stapling for all SSL-enabled servers: +#SSLUseStapling On + +# Define a relatively small cache for OCSP Stapling using +# the same mechanism that is used for the SSL session cache +# above. If stapling is used with more than a few certificates, +# the size may need to be increased. (AH01929 will be logged.) +#SSLStaplingCache "shmcb:/usr/local/apache2/logs/ssl_stapling(32768)" + +# Seconds before valid OCSP responses are expired from the cache +#SSLStaplingStandardCacheTimeout 3600 + +# Seconds before invalid OCSP responses are expired from the cache +#SSLStaplingErrorCacheTimeout 600 + +## +## SSL Virtual Host Context +## + + + +# General setup for the virtual host +ServerName localhost + +DocumentRoot "/usr/local/apache2/htdocs" + +ErrorLog /proc/self/fd/2 +ServerSignature On +CustomLog /proc/self/fd/2 combined + +# SSL Engine Switch: +# Enable/Disable SSL for this virtual host. +SSLEngine on + +# Server Certificate: +# Point SSLCertificateFile at a PEM encoded certificate. If +# the certificate is encrypted, then you will be prompted for a +# pass phrase. Note that a kill -HUP will prompt again. Keep +# in mind that if you have both an RSA and a DSA certificate you +# can configure both in parallel (to also allow the use of DSA +# ciphers, etc.) +# Some ECC cipher suites (http://www.ietf.org/rfc/rfc4492.txt) +# require an ECC certificate which can also be configured in +# parallel. +SSLCertificateFile "/usr/share/ca-certificates/certs/http.crt" + +# Server Private Key: +# If the key is not combined with the certificate, use this +# directive to point at the key file. Keep in mind that if +# you've both a RSA and a DSA private key you can configure +# both in parallel (to also allow the use of DSA ciphers, etc.) +# ECC keys, when in use, can also be configured in parallel +SSLCertificateKeyFile "/usr/share/ca-certificates/certs/http.key" + +# Server Certificate Chain: +# Point SSLCertificateChainFile at a file containing the +# concatenation of PEM encoded CA certificates which form the +# certificate chain for the server certificate. Alternatively +# the referenced file can be the same as SSLCertificateFile +# when the CA certificates are directly appended to the server +# certificate for convenience. +#SSLCertificateChainFile "/usr/local/apache2/conf/server-ca.crt" + +# Certificate Authority (CA): +# Set the CA certificate verification path where to find CA +# certificates for client authentication or alternatively one +# huge file containing all of them (file must be PEM encoded) +# Note: Inside SSLCACertificatePath you need hash symlinks +# to point to the certificate files. Use the provided +# Makefile to update the hash symlinks after changes. +#SSLCACertificatePath "/usr/local/apache2/conf/ssl.crt" +#SSLCACertificateFile "/usr/local/apache2/conf/ssl.crt/ca-bundle.crt" + +# Certificate Revocation Lists (CRL): +# Set the CA revocation path where to find CA CRLs for client +# authentication or alternatively one huge file containing all +# of them (file must be PEM encoded). +# The CRL checking mode needs to be configured explicitly +# through SSLCARevocationCheck (defaults to "none" otherwise). +# Note: Inside SSLCARevocationPath you need hash symlinks +# to point to the certificate files. Use the provided +# Makefile to update the hash symlinks after changes. +#SSLCARevocationPath "/usr/local/apache2/conf/ssl.crl" +#SSLCARevocationFile "/usr/local/apache2/conf/ssl.crl/ca-bundle.crl" +#SSLCARevocationCheck chain + +# Client Authentication (Type): +# Client certificate verification type and depth. Types are +# none, optional, require and optional_no_ca. Depth is a +# number which specifies how deeply to verify the certificate +# issuer chain before deciding the certificate is not valid. +#SSLVerifyClient require +#SSLVerifyDepth 10 + +# TLS-SRP mutual authentication: +# Enable TLS-SRP and set the path to the OpenSSL SRP verifier +# file (containing login information for SRP user accounts). +# Requires OpenSSL 1.0.1 or newer. See the mod_ssl FAQ for +# detailed instructions on creating this file. Example: +# "openssl srp -srpvfile /usr/local/apache2/conf/passwd.srpv -add username" +#SSLSRPVerifierFile "/usr/local/apache2/conf/passwd.srpv" + +# Access Control: +# With SSLRequire you can do per-directory access control based +# on arbitrary complex boolean expressions containing server +# variable checks and other lookup directives. The syntax is a +# mixture between C and Perl. See the mod_ssl documentation +# for more details. +# +#SSLRequire ( %{SSL_CIPHER} !~ m/^(EXP|NULL)/ \ +# and %{SSL_CLIENT_S_DN_O} eq "Snake Oil, Ltd." \ +# and %{SSL_CLIENT_S_DN_OU} in {"Staff", "CA", "Dev"} \ +# and %{TIME_WDAY} >= 1 and %{TIME_WDAY} <= 5 \ +# and %{TIME_HOUR} >= 8 and %{TIME_HOUR} <= 20 ) \ +# or %{REMOTE_ADDR} =~ m/^192\.76\.162\.[0-9]+$/ +# + +# SSL Engine Options: +# Set various options for the SSL engine. +# o FakeBasicAuth: +# Translate the client X.509 into a Basic Authorisation. This means that +# the standard Auth/DBMAuth methods can be used for access control. The +# user name is the `one line' version of the client's X.509 certificate. +# Note that no password is obtained from the user. Every entry in the user +# file needs this password: `xxj31ZMTZzkVA'. +# o ExportCertData: +# This exports two additional environment variables: SSL_CLIENT_CERT and +# SSL_SERVER_CERT. These contain the PEM-encoded certificates of the +# server (always existing) and the client (only existing when client +# authentication is used). This can be used to import the certificates +# into CGI scripts. +# o StdEnvVars: +# This exports the standard SSL/TLS related `SSL_*' environment variables. +# Per default this exportation is switched off for performance reasons, +# because the extraction step is an expensive operation and is usually +# useless for serving static content. So one usually enables the +# exportation for CGI and SSI requests only. +# o StrictRequire: +# This denies access when "SSLRequireSSL" or "SSLRequire" applied even +# under a "Satisfy any" situation, i.e. when it applies access is denied +# and no other module can change it. +# o OptRenegotiate: +# This enables optimized SSL connection renegotiation handling when SSL +# directives are used in per-directory context. +#SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire + + SSLOptions +StdEnvVars + + + SSLOptions +StdEnvVars + + +# SSL Protocol Adjustments: +# The safe and default but still SSL/TLS standard compliant shutdown +# approach is that mod_ssl sends the close notify alert but doesn't wait for +# the close notify alert from client. When you need a different shutdown +# approach you can use one of the following variables: +# o ssl-unclean-shutdown: +# This forces an unclean shutdown when the connection is closed, i.e. no +# SSL close notify alert is sent or allowed to be received. This violates +# the SSL/TLS standard but is needed for some brain-dead browsers. Use +# this when you receive I/O errors because of the standard approach where +# mod_ssl sends the close notify alert. +# o ssl-accurate-shutdown: +# This forces an accurate shutdown when the connection is closed, i.e. a +# SSL close notify alert is send and mod_ssl waits for the close notify +# alert of the client. This is 100% SSL/TLS standard compliant, but in +# practice often causes hanging connections with brain-dead browsers. Use +# this only for browsers where you know that their SSL implementation +# works correctly. +# Notice: Most problems of broken clients are also related to the HTTP +# keep-alive facility, so you usually additionally want to disable +# keep-alive for those clients, too. Use variable "nokeepalive" for this. +# Similarly, one has to force some clients to use HTTP/1.0 to workaround +# their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and +# "force-response-1.0" for this. +BrowserMatch "MSIE [2-5]" \ + nokeepalive ssl-unclean-shutdown \ + downgrade-1.0 force-response-1.0 + +# Per-Server Logging: +# The home of a custom SSL log file. Use this when you want a +# compact non-error SSL logfile on a virtual host basis. +CustomLog /proc/self/fd/2 \ + "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" + + diff --git a/rat_server/httpd-vhosts.conf b/rat_server/httpd-vhosts.conf new file mode 100644 index 0000000..41e74b3 --- /dev/null +++ b/rat_server/httpd-vhosts.conf @@ -0,0 +1,41 @@ +# Virtual Hosts +# +# Required modules: mod_log_config + +# If you want to maintain multiple domains/hostnames on your +# machine you can setup VirtualHost containers for them. Most configurations +# use only name-based virtual hosts so the server doesn't need to worry about +# IP addresses. This is indicated by the asterisks in the directives below. +# +# Please see the documentation at +# +# for further details before you try to setup virtual hosts. +# +# You may use the command line option '-S' to verify your virtual host +# configuration. + +# +# VirtualHost example: +# Almost any Apache directive may go into a VirtualHost container. +# The first VirtualHost section is used for all requests that do not +# match a ServerName or ServerAlias in any block. +# + + ServerName localhost + + DocumentRoot "/usr/local/apache2" + + Alias /fbrat/www/ /usr/share/fbrat/www/ + + Options Indexes FollowSymLinks MultiViews + Require all granted + + + # + # LogLevel trace8 + # + ServerSignature On + SetEnvIf Request_URI "/healthy$" is-nolog=1 + CustomLog /proc/self/fd/1 common env=!is-nolog + + diff --git a/rat_server/httpd.conf b/rat_server/httpd.conf new file mode 100644 index 0000000..8030ec5 --- /dev/null +++ b/rat_server/httpd.conf @@ -0,0 +1,603 @@ +# +# This is the main Apache HTTP server configuration file. It contains the +# configuration directives that give the server its instructions. +# See for detailed information. +# In particular, see +# +# for a discussion of each configuration directive. +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. You have been warned. +# +# Configuration and logfile names: If the filenames you specify for many +# of the server's control files begin with "/" (or "drive:/" for Win32), the +# server will use that explicit path. If the filenames do *not* begin +# with "/", the value of ServerRoot is prepended -- so "logs/access_log" +# with ServerRoot set to "/usr/local/apache2" will be interpreted by the +# server as "/usr/local/apache2/logs/access_log", whereas "/logs/access_log" +# will be interpreted as '/logs/access_log'. + +# +# ServerRoot: The top of the directory tree under which the server's +# configuration, error, and log files are kept. +# +# Do not add a slash at the end of the directory path. If you point +# ServerRoot at a non-local disk, be sure to specify a local disk on the +# Mutex directive, if file-based mutexes are used. If you wish to share the +# same ServerRoot for multiple httpd daemons, you will need to change at +# least PidFile. +# +ServerRoot "/usr/local/apache2" + +# +# Mutex: Allows you to set the mutex mechanism and mutex file directory +# for individual mutexes, or change the global defaults +# +# Uncomment and change the directory if mutexes are file-based and the default +# mutex file directory is not on a local disk or is not appropriate for some +# other reason. +# +# Mutex default:logs + +# +# Listen: Allows you to bind Apache to specific IP addresses and/or +# ports, instead of the default. See also the +# directive. +# +# Change this to Listen on specific IP addresses as shown below to +# prevent Apache from glomming onto all bound IP addresses. +# +#Listen 12.34.56.78:80 +Listen 80 + +# +# Dynamic Shared Object (DSO) Support +# +# To be able to use the functionality of a module which was built as a DSO you +# have to place corresponding `LoadModule' lines at this location so the +# directives contained in it are actually available _before_ they are used. +# Statically compiled modules (those listed by `httpd -l') do not need +# to be loaded here. +# +# Example: +# LoadModule foo_module modules/mod_foo.so +# +#LoadModule mpm_event_module modules/mod_mpm_event.so +#LoadModule mpm_prefork_module modules/mod_mpm_prefork.so +LoadModule mpm_worker_module modules/mod_mpm_worker.so +LoadModule authn_file_module modules/mod_authn_file.so +LoadModule authn_dbm_module modules/mod_authn_dbm.so +LoadModule authn_anon_module modules/mod_authn_anon.so +#LoadModule authn_dbd_module modules/mod_authn_dbd.so +#LoadModule authn_socache_module modules/mod_authn_socache.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authz_host_module modules/mod_authz_host.so +LoadModule authz_groupfile_module modules/mod_authz_groupfile.so +LoadModule authz_user_module modules/mod_authz_user.so +LoadModule authz_dbm_module modules/mod_authz_dbm.so +LoadModule authz_owner_module modules/mod_authz_owner.so +#LoadModule authz_dbd_module modules/mod_authz_dbd.so +LoadModule authz_core_module modules/mod_authz_core.so +#LoadModule authnz_ldap_module modules/mod_authnz_ldap.so +#LoadModule authnz_fcgi_module modules/mod_authnz_fcgi.so +LoadModule access_compat_module modules/mod_access_compat.so +LoadModule auth_basic_module modules/mod_auth_basic.so +#LoadModule auth_form_module modules/mod_auth_form.so +LoadModule auth_digest_module modules/mod_auth_digest.so +#LoadModule allowmethods_module modules/mod_allowmethods.so +#LoadModule isapi_module modules/mod_isapi.so +#LoadModule file_cache_module modules/mod_file_cache.so +LoadModule cache_module modules/mod_cache.so +#LoadModule cache_disk_module modules/mod_cache_disk.so +#LoadModule cache_socache_module modules/mod_cache_socache.so +LoadModule socache_shmcb_module modules/mod_socache_shmcb.so +#LoadModule socache_dbm_module modules/mod_socache_dbm.so +#LoadModule socache_memcache_module modules/mod_socache_memcache.so +#LoadModule socache_redis_module modules/mod_socache_redis.so +#LoadModule watchdog_module modules/mod_watchdog.so +#LoadModule macro_module modules/mod_macro.so +#LoadModule dbd_module modules/mod_dbd.so +#LoadModule bucketeer_module modules/mod_bucketeer.so +#LoadModule dumpio_module modules/mod_dumpio.so +#LoadModule echo_module modules/mod_echo.so +#LoadModule example_hooks_module modules/mod_example_hooks.so +#LoadModule case_filter_module modules/mod_case_filter.so +#LoadModule case_filter_in_module modules/mod_case_filter_in.so +#LoadModule example_ipc_module modules/mod_example_ipc.so +#LoadModule buffer_module modules/mod_buffer.so +#LoadModule data_module modules/mod_data.so +#LoadModule ratelimit_module modules/mod_ratelimit.so +LoadModule reqtimeout_module modules/mod_reqtimeout.so +LoadModule ext_filter_module modules/mod_ext_filter.so +#LoadModule request_module modules/mod_request.so +LoadModule include_module modules/mod_include.so +LoadModule filter_module modules/mod_filter.so +#LoadModule reflector_module modules/mod_reflector.so +LoadModule substitute_module modules/mod_substitute.so +#LoadModule sed_module modules/mod_sed.so +#LoadModule charset_lite_module modules/mod_charset_lite.so +LoadModule deflate_module modules/mod_deflate.so +#LoadModule xml2enc_module modules/mod_xml2enc.so +#LoadModule proxy_html_module modules/mod_proxy_html.so +#LoadModule brotli_module modules/mod_brotli.so +LoadModule mime_module modules/mod_mime.so +#LoadModule ldap_module modules/mod_ldap.so +LoadModule log_config_module modules/mod_log_config.so +#LoadModule log_debug_module modules/mod_log_debug.so +#LoadModule log_forensic_module modules/mod_log_forensic.so +LoadModule logio_module modules/mod_logio.so +#LoadModule lua_module modules/mod_lua.so +LoadModule env_module modules/mod_env.so +LoadModule mime_magic_module modules/mod_mime_magic.so +#LoadModule cern_meta_module modules/mod_cern_meta.so +LoadModule expires_module modules/mod_expires.so +LoadModule headers_module modules/mod_headers.so +#LoadModule ident_module modules/mod_ident.so +LoadModule usertrack_module modules/mod_usertrack.so +#LoadModule unique_id_module modules/mod_unique_id.so +LoadModule setenvif_module modules/mod_setenvif.so +LoadModule version_module modules/mod_version.so +#LoadModule remoteip_module modules/mod_remoteip.so +LoadModule proxy_module modules/mod_proxy.so +#LoadModule proxy_connect_module modules/mod_proxy_connect.so +#LoadModule proxy_ftp_module modules/mod_proxy_ftp.so +LoadModule proxy_http_module modules/mod_proxy_http.so +#LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so +#LoadModule proxy_scgi_module modules/mod_proxy_scgi.so +#LoadModule proxy_uwsgi_module modules/mod_proxy_uwsgi.so +#LoadModule proxy_fdpass_module modules/mod_proxy_fdpass.so +#LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so +#LoadModule proxy_ajp_module modules/mod_proxy_ajp.so +#LoadModule proxy_balancer_module modules/mod_proxy_balancer.so +#LoadModule proxy_express_module modules/mod_proxy_express.so +#LoadModule proxy_hcheck_module modules/mod_proxy_hcheck.so +#LoadModule session_module modules/mod_session.so +#LoadModule session_cookie_module modules/mod_session_cookie.so +#LoadModule session_crypto_module modules/mod_session_crypto.so +#LoadModule session_dbd_module modules/mod_session_dbd.so +#LoadModule slotmem_shm_module modules/mod_slotmem_shm.so +#LoadModule slotmem_plain_module modules/mod_slotmem_plain.so +LoadModule ssl_module modules/mod_ssl.so +#LoadModule optional_hook_export_module modules/mod_optional_hook_export.so +#LoadModule optional_hook_import_module modules/mod_optional_hook_import.so +#LoadModule optional_fn_import_module modules/mod_optional_fn_import.so +#LoadModule optional_fn_export_module modules/mod_optional_fn_export.so +#LoadModule dialup_module modules/mod_dialup.so +#LoadModule http2_module modules/mod_http2.so +#LoadModule proxy_http2_module modules/mod_proxy_http2.so +#LoadModule md_module modules/mod_md.so +#LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so +#LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so +#LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so +#LoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so +LoadModule unixd_module modules/mod_unixd.so +#LoadModule heartbeat_module modules/mod_heartbeat.so +#LoadModule heartmonitor_module modules/mod_heartmonitor.so +#LoadModule dav_module modules/mod_dav.so +LoadModule status_module modules/mod_status.so +# LoadModule autoindex_module modules/mod_autoindex.so +#LoadModule asis_module modules/mod_asis.so +#LoadModule info_module modules/mod_info.so +LoadModule suexec_module modules/mod_suexec.so +# +# LoadModule cgid_module modules/mod_cgid.so +# +# +# #LoadModule cgi_module modules/mod_cgi.so +# +#LoadModule dav_fs_module modules/mod_dav_fs.so +#LoadModule dav_lock_module modules/mod_dav_lock.so +LoadModule vhost_alias_module modules/mod_vhost_alias.so +LoadModule negotiation_module modules/mod_negotiation.so +LoadModule dir_module modules/mod_dir.so +#LoadModule imagemap_module modules/mod_imagemap.so +LoadModule actions_module modules/mod_actions.so +#LoadModule speling_module modules/mod_speling.so +#LoadModule userdir_module modules/mod_userdir.so +LoadModule alias_module modules/mod_alias.so +LoadModule rewrite_module modules/mod_rewrite.so +LoadModule wsgi_module "/usr/lib/python3.11/site-packages/mod_wsgi/server/mod_wsgi-py311.cpython-311-x86_64-linux-musl.so" + + +# +# If you wish httpd to run as a different user or group, you must run +# httpd as root initially and it will switch. +# +# User/Group: The name (or #number) of the user/group to run httpd as. +# It is usually good practice to create a dedicated user and group for +# running httpd, as with most system services. +# +User www-data +Group www-data +#User fbrat +#Group fbrat + + + +# 'Main' server configuration +# +# The directives in this section set up the values used by the 'main' +# server, which responds to any requests that aren't handled by a +# definition. These values also provide defaults for +# any containers you may define later in the file. +# +# All of these directives may appear inside containers, +# in which case these default settings will be overridden for the +# virtual host being defined. +# + +# +# ServerAdmin: Your address, where problems with the server should be +# e-mailed. This address appears on some server-generated pages, such +# as error documents. e.g. admin@your-domain.com +# +#ServerAdmin you@example.com + +# +# ServerName gives the name and port that the server uses to identify itself. +# This can often be determined automatically, but we recommend you specify +# it explicitly to prevent problems during startup. +# +# If your host doesn't have a registered DNS name, enter its IP address here. +# +#ServerName rat_server + +# +# Deny access to the entirety of your server's filesystem. You must +# explicitly permit access to web content directories in other +# blocks below. +# + + Options FollowSymlinks + AllowOverride none + + +# +# Note that from this point forward you must specifically allow +# particular features to be enabled - so if something's not working as +# you might expect, make sure that you have specifically enabled it +# below. +# + +# +# DocumentRoot: The directory out of which you will serve your +# documents. By default, all requests are taken from this directory, but +# symbolic links and aliases may be used to point to other locations. +# +#DocumentRoot "/usr/local/apache2" +# + # + # Possible values for the Options directive are "None", "All", + # or any combination of: + # Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews + # + # Note that "MultiViews" must be named *explicitly* --- "Options All" + # doesn't give it to you. + # + # The Options directive is both complicated and important. Please see + # http://httpd.apache.org/docs/2.4/mod/core.html#options + # for more information. + # + #Options Indexes FollowSymLinks MultiViews + + # + # AllowOverride controls what directives may be placed in .htaccess files. + # It can be "All", "None", or any combination of the keywords: + # AllowOverride FileInfo AuthConfig Limit + # + #AllowOverride None + + # + # Controls who can get stuff from this server. + # + #Require all granted +# + +# +# DirectoryIndex: sets the file that Apache will serve if a directory +# is requested. +# + + DirectoryIndex index.html + + +# +# The following lines prevent .htaccess and .htpasswd files from being +# viewed by Web clients. +# + + Require all denied + + +# +# ErrorLog: The location of the error log file. +# If you do not specify an ErrorLog directive within a +# container, error messages relating to that virtual host will be +# logged here. If you *do* define an error logfile for a +# container, that host's errors will be logged there and not here. +# +ErrorLog /proc/self/fd/2 + +# +# LogLevel: Control the number of messages logged to the error_log. +# Possible values include: debug, info, notice, warn, error, crit, +# alert, emerg. +# +LogLevel info + + + # + # The following directives define some format nicknames for use with + # a CustomLog directive (see below). + # + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%h %l %u %t \"%r\" %>s %b" common + LogFormat "%{Referer}i -> %U" referer + LogFormat "%{User-agent}i" agent + LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-agent}i\"" forwarded + + +# + # You need to enable mod_logio.c to use %I and %O +# LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio +# + + # + # The location and format of the access logfile (Common Logfile Format). + # If you do not define any access logfiles within a + # container, they will be logged here. Contrariwise, if you *do* + # define per- access logfiles, transactions will be + # logged therein and *not* in this file. + # + CustomLog /proc/self/fd/1 common + #CustomLog /proc/self/fd/2 combined + + + +# + # + # Redirect: Allows you to tell clients about documents that used to + # exist in your server's namespace, but do not anymore. The client + # will make a new request for the document at its new location. + # Example: + # Redirect permanent /foo http://www.example.com/bar + + # + # Alias: Maps web paths into filesystem paths and is used to + # access content that does not live under the DocumentRoot. + # Example: + # Alias /webpath /full/filesystem/path + # + # If you include a trailing / on /webpath then the server will + # require it to be present in the URL. You will also likely + # need to provide a section to allow access to + # the filesystem path. + + # + # ScriptAlias: This controls which directories contain server scripts. + # ScriptAliases are essentially the same as Aliases, except that + # documents in the target directory are treated as applications and + # run by the server when requested rather than as documents sent to the + # client. The same rules about trailing "/" apply to ScriptAlias + # directives as to Alias. + # +# ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/" +# Alias /icons/ "/usr/local/apache2/icons/" +# +# Options Indexes MultiViews +# AllowOverride None +# Require all granted +# + +# + +# + # + # ScriptSock: On threaded servers, designate the path to the UNIX + # socket used to communicate with the CGI daemon of mod_cgid. + # + #Scriptsock cgisock +# + +# +# "/usr/local/apache2/cgi-bin" should be changed to whatever your ScriptAliased +# CGI directory exists, if you have that configured. +# +# +# AllowOverride None +# Options None +# Require all granted +# + + + SetOutputFilter DEFLATE + SetEnvIfNoCase Request_URI \.(?:gif|jpg|png|ico|zip|gz|mp4|flv)$ no-gzip + AddOutputFilterByType DEFLATE application/json + AddOutputFilterByType DEFLATE application/rss+xml + AddOutputFilterByType DEFLATE application/x-javascript application/javascript application/ecmascript + AddOutputFilterByType DEFLATE text/css + AddOutputFilterByType DEFLATE text/html text/plain text/xml + + DeflateFilterNote Input instream + DeflateFilterNote Output outstream + DeflateFilterNote Ratio ratio + + +# + # + # Avoid passing HTTP_PROXY environment to CGI's on this or any proxied + # backend servers which have lingering "httpoxy" defects. + # 'Proxy' request header is undefined by the IETF, not listed by IANA + # +# RequestHeader unset Proxy early +# + + + # + # TypesConfig points to the file containing the list of mappings from + # filename extension to MIME-type. + # + TypesConfig conf/mime.types + + # + # AddType allows you to add to or override the MIME configuration + # file specified in TypesConfig for specific file types. + # + #AddType application/x-gzip .tgz + # + # AddEncoding allows you to have certain browsers uncompress + # information on the fly. Note: Not all browsers support this. + # + #AddEncoding x-compress .Z + #AddEncoding x-gzip .gz .tgz + # + # If the AddEncoding directives above are commented-out, then you + # probably should define those extensions to indicate media types: + # + #AddType application/x-compress .Z + #AddType application/x-gzip .gz .tgz + AddType application/x-x509-ca-cert .crt + AddType application/x-x509-ca-cert .pem + + + # + # AddHandler allows you to map certain file extensions to "handlers": + # actions unrelated to filetype. These can be either built into the server + # or added with the Action directive (see below) + # + # To use CGI scripts outside of ScriptAliased directories: + # (You will also need to add "ExecCGI" to the "Options" directive.) + # + #AddHandler cgi-script .cgi + + # For type maps (negotiated resources): + #AddHandler type-map var + + # + # Filters allow you to process content before it is sent to the client. + # + # To parse .shtml files for server-side includes (SSI): + # (You will also need to add "Includes" to the "Options" directive.) + # + #AddType text/html .shtml + #AddOutputFilter INCLUDES .shtml + + + + #WSGIDaemonProcess ratapi display-name=(wsgi:ratapi) processes=2 threads=2 user=fbrat group=fbrat response-buffer-size=1048576 header-buffer-size=1048576 connect-timeout=60 + WSGIDaemonProcess ratapi display-name=(wsgi:ratapi) processes=2 threads=2 user=fbrat group=fbrat + WSGIProcessGroup ratapi + WSGIApplicationGroup %{GLOBAL} + # Apache does not process auth'n/auth'z + WSGIPassAuthorization on + + WSGIScriptAlias /fbrat /usr/local/apache2/fbrat.wsgi process-group=ratapi application-group=%{GLOBAL} + #WSGIScriptAlias /fbrat /usr/local/apache2/cgi-bin/fbrat.wsgi + + WSGIScriptReloading On + + Require all granted + + + + # Rat_Server fbrat path redirection (default: disabled) + + RedirectMatch ^/$ /fbrat + + + WSGISocketPrefix /var/run/wsgi + + +# +# The mod_mime_magic module allows the server to use various hints from the +# contents of the file itself to determine its type. The MIMEMagicFile +# directive tells the module where the hint definitions are located. +# +MIMEMagicFile conf/magic + +# +# Customizable error responses come in three flavors: +# 1) plain text 2) local redirects 3) external redirects +# +# Some examples: +#ErrorDocument 500 "The server made a boo boo." +#ErrorDocument 404 /missing.html +#ErrorDocument 404 "/cgi-bin/missing_handler.pl" +#ErrorDocument 402 http://www.example.com/subscription_info.html +# + +# +# MaxRanges: Maximum number of Ranges in a request before +# returning the entire resource, or one of the special +# values 'default', 'none' or 'unlimited'. +# Default setting is to accept 200 Ranges. +#MaxRanges unlimited + +# +# EnableMMAP and EnableSendfile: On systems that support it, +# memory-mapping or the sendfile syscall may be used to deliver +# files. This usually improves server performance, but must +# be turned off when serving from networked-mounted +# filesystems or if support for these functions is otherwise +# broken on your system. +# Defaults: EnableMMAP On, EnableSendfile Off +# +#EnableMMAP off +EnableSendfile on + +# Supplemental configuration +# +# The configuration files in the conf/extra/ directory can be +# included to add extra features or to modify the default configuration of +# the server, or you may simply copy their contents here and change as +# necessary. + +# Server-pool management (MPM specific) +Include conf/extra/httpd-mpm.conf + +# Multi-language error messages +#Include conf/extra/httpd-multilang-errordoc.conf + +# Fancy directory listings +#Include conf/extra/httpd-autoindex.conf + +# Language settings +#Include conf/extra/httpd-languages.conf + +# User home directories +#Include conf/extra/httpd-userdir.conf + +# Real-time info on requests and configuration +Include conf/extra/httpd-info.conf + +# Virtual hosts +Include conf/extra/httpd-vhosts.conf + +# Local access to the Apache HTTP Server Manual +#Include conf/extra/httpd-manual.conf + +# Distributed authoring and versioning (WebDAV) +#Include conf/extra/httpd-dav.conf + +# Various default settings +Include conf/extra/httpd-default.conf + +# Configure mod_proxy_html to understand HTML4/XHTML1 +# +#Include conf/extra/proxy-html.conf +# + +# Secure (SSL/TLS) connections +#Include conf/extra/httpd-ssl.conf +# +# Note: The following must must be present to support +# starting without SSL on platforms with no /dev/random equivalent +# but a statically compiled-in mod_ssl. +# + +SSLRandomSeed startup builtin +SSLRandomSeed connect builtin + +ServerSignature Off +ServerTokens Prod diff --git a/rat_server/requirements.txt b/rat_server/requirements.txt new file mode 100644 index 0000000..a7afc83 --- /dev/null +++ b/rat_server/requirements.txt @@ -0,0 +1,58 @@ +alembic==1.8.1 +amqp==5.1.1 +bcrypt==4.0.1 +billiard==3.6.4.0 +blinker==1.6.2 +celery==5.2.7 +certifi==2023.7.22 +cffi==1.15.1 +charset-normalizer==3.0.1 +click==8.1.3 +click-didyoumean==0.3.0 +click-plugins==1.1.1 +click-repl==0.2.0 +defusedxml==0.7.1 +dnspython==2.2.1 +email-validator==1.3.0 +Flask==2.3.2 +Flask-JSONRPC==2.1.0 +Flask-Login==0.6.2 +Flask-Mail==0.9.1 +Flask-Migrate==2.6.0 +Flask-Script==2.0.5 +Flask-SQLAlchemy==2.5.1 +Flask-User==1.0.2.1 +Flask-WTF==1.1.1 +futures==3.0.5 +gevent==23.9.1 +greenlet==3.0.1 +idna==3.4 +IPy==1.1 +itsdangerous==2.1.2 +Jinja2==3.1.2 +jsmin==3.0.1 +json5==0.9.10 +jwt==1.3.1 +kombu==5.2.4 +Mako==1.2.4 +MarkupSafe==2.1.1 +nose==1.3.7 +passlib==1.7.4 +pathlib==1.0.1 +prettytable==3.5.0 +prometheus-client==0.17.1 +prompt-toolkit==3.0.33 +pycparser==2.21 +python_dateutil==2.8.2 +python2-secrets==1.0.5 +pytz==2022.6 +pyxdg==0.28 +PyYAML==6.0.1 +typeguard==2.12.1 +urllib3==1.26.18 +vine==5.0.0 +wcwidth==0.2.5 +Werkzeug==2.3.3 +WsgiDAV==2.4.1 +WTForms==3.0.1 +pika==1.3.2 diff --git a/ratdb/Dockerfile b/ratdb/Dockerfile new file mode 100644 index 0000000..a4ab219 --- /dev/null +++ b/ratdb/Dockerfile @@ -0,0 +1,14 @@ +FROM postgres:14.9-alpine +ENV POSTGRES_PASSWORD=N3SF0LVKJx1RAhFGx4fcw +ENV PGDATA=/mnt/nfs/psql/data +ENV POSTGRES_DB=fbrat +# +ENV AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +COPY ratdb/devel.sh / +RUN chmod +x /devel.sh +RUN /devel.sh +# +ENV AFC_RATDB_CONNS=${AFC_RATDB_CONNS:-1000} +ENTRYPOINT docker-entrypoint.sh postgres -c max_connections=$AFC_RATDB_CONNS +HEALTHCHECK --start-period=20s --interval=10s --timeout=5s \ + CMD pg_isready -U postgres || exit 1 diff --git a/ratdb/devel.sh b/ratdb/devel.sh new file mode 100755 index 0000000..8f95af1 --- /dev/null +++ b/ratdb/devel.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +case "$AFC_DEVEL_ENV" in + 'devel') + echo -e "\033[0;32mDebug profile\033[0m" + apk add --no-cache bash + ;; + 'production') + echo -e "\033[0;32mProduction profile\033[0m" + ;; + *) + echo "Uknown profile" + ;; +esac + +exit $? diff --git a/rcache/Dockerfile b/rcache/Dockerfile new file mode 100644 index 0000000..d299b61 --- /dev/null +++ b/rcache/Dockerfile @@ -0,0 +1,66 @@ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +FROM alpine:3.18 + +# True if enabled (default is True) +# ENV RCACHE_ENABLED=True + +# Port REST API service listen on +# ENV RCACHE_CLIENT_PORT + +# Connection string to Postgres DB +# ENV RCACHE_POSTGRES_DSN=postgresql://[user[:password]]@host[:port]/database[?options] + +# What to do if database already exists: +# - 'leave' (leave as is - default) +# - 'recreate' (completely recreate - e.g. removing alembic, if any) +# - 'clean' (only clean the cache table) +ENV RCACHE_IF_DB_EXISTS=leave + +# Maximum number of simultaneous precomputation requests. Default is 10 +ENV RCACHE_PRECOMPUTE_QUOTA=10 + +# REST API URL to send precompute requests to. No precomputation if absent +# ENV RCACHE_AFC_REQ_URL + +# REST API URL for requesting list of active AFC Configs. Default max link +# distance (130km) is used for spatial invalidation if unspecified or not +# responding +# ENV RCACHE_RULESETS_URL + +# REST API URL to use for requesting AFC Config by Ruleset ID. Default max link +# distance (130km) is used for spatial invalidation if unspecified or not +# responding +# ENV RCACHE_CONFIG_RETRIEVAL_URL + +# Additional command line parameters for uvicorn +ENV RCACHE_UVICORN_PARAMS="--no-access-log --log-level info" + +WORKDIR /wd + +RUN apk add --update --no-cache python3=~3.11 py3-sqlalchemy=~1.4 py3-pip \ + py3-requests py3-pydantic=~1.10 py3-alembic py3-psycopg2 py3-greenlet \ + py3-aiohttp curl + +COPY src/afc-packages /wd/afc-packages +RUN pip3 install --use-pep517 --root-user-action=ignore \ + -r /wd/afc-packages/pkgs.rcache \ + && rm -rf /wd/afc-packages + +COPY rcache/requirements.txt /wd +RUN pip3 install --root-user-action=ignore -r /wd/requirements.txt +COPY rcache/rcache_app.py rcache/rcache_app.py rcache/rcache_db_async.py \ + rcache/rcache_service.py /wd/ +COPY tools/rcache/rcache_tool.py /wd +RUN chmod a+x /wd/rcache_tool.py + +ENTRYPOINT uvicorn rcache_app:app --host 0.0.0.0 --port ${RCACHE_CLIENT_PORT} \ + ${RCACHE_UVICORN_PARAMS} + +HEALTHCHECK --start-period=20s \ + CMD curl -f http://localhost:${RCACHE_CLIENT_PORT}/healthcheck || exit 1 \ No newline at end of file diff --git a/rcache/README.md b/rcache/README.md new file mode 100644 index 0000000..a5a7a98 --- /dev/null +++ b/rcache/README.md @@ -0,0 +1,191 @@ +Copyright (C) 2022 Broadcom. All rights reserved.\ +The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate that +owns the software below. This work is licensed under the OpenAFC Project +License, a copy of which is included with this software program. + +# Rcache - AFC Response Cache + +## Table of contents +- [Operation overview](#overview) + - [Rcache parts](#rcache_parts) + - [Rcache counterparts](#rcache_counterparts) +- [Associated containers and their configuration](#service) +- [Client API](#client_api) +- [REST API](#rest_api) +- [`rcache_tool.py` - test and manipulation tool](#rcache_tool) +- [Database schema](#db_schema) + - [Rcache table](#rcache_table) + - [Switches table](#switches_table) + +## Operation overview + +AFC Response computation takes a lot of time, hence it is beneficial to reuse previously computed AFC Response wherever possible. Rcache (Response Cache) is a mechanism for collection and retrieval of previously computed AFC Responses. + +### Rcache parts + +Rcache consists of following parts, scattered over various corners of AFC source tree: + +- **Rcache service** (sources in `rcache`). REST API service that runs in a separate container and responsible for all write-related operations, namely: + - **Update.** Writes newly computed AFC Responses to Postgres database. + - **Invalidate.** Marks as invalid cache entries affected by FS (aka ULS) data change or by AFC Config changes. + - **Precompute.** Invalidated cache entries recomputed in order to avoid Message Handler /RaServer delay when AFC Request will arrive next time. This is called precomputation (as it happens before AFC Request arrival). + +- **Rcache client library** (own and shared sources located in `src/afc-packages/rcache`). All AFC Services that interoperate with Rcache do it through this client library. + Rcache library not only communicates with Rcache service, but also directly interoperates with other parts of AFC system: + - With **Postgres database** - to perform cache lookup + - With **RabbitMQ** - to pass computed AFC Response from Worker to Message Handler / Rat Server. + +- **Rcache tool** (sources located in `tools/rcache`). Script for Rcache testing - functional and load. + This script is intended to be executed in its own container (for which there is `Dockerfile` in sources) that can be hitched to main AFC docker composition (for which there is `compose_rcache.yaml` in sources). + Also it is included in Rcache service container - and may be ran from there. + +### Rcache counterparts + +Following services interoperate with Rcache service or library: + +- **Postgres database**. Rcache client library performs cache lookup directly in database (on behalf of Message Handler and Rat Server), Rcache service performs write operations. + +- **Message Handler / Rat Server**. Uses Rcache client library to: + + - Perform Rcache lookup + - Receive computed AFC Response sent by Worker via RabbitMQ. This route is used for synchronous non-GUI requests only. Responses on asynchronous and GUI requests passed in a legacy way - via ObjStore (without Rcache library involvement) + +- **Worker**. Uses Rcache client library to: + + - Pass (via RabbitMQ) computed synchronous non-GUI AFC Response to Message Handler or Rat Server + - Write (via Rcache REST API service) computed AFC Response to Postgres database. + +- **RabbitMQ**. Used by Rcache client library to pass computed synchronous non-GUI AFC Responses from Worker to Message Handler or Rat Server + +- **FS (aka ULS) downloader**. Uses Rcache library to notify Rcache service which regions were invalidated by changes in FS data. + +- **???**. For the following actions there are REST APIs in Rcache service (but *no* Rcache client library routines), however as of time of this writing it is not quite clear who'll drive them: + + - Invalidation on AFC Config changes. + - Full invalidation. + - Setting precompute quota (maximum number of simultaneous precomputations). + +## Associated containers and their configuration + +All Rcache service, Rcache client library and, to certain extent, Rcache tool are configured by means of environment variables, passed to containers. All these variables have `RCACHE_` prefix. Here are these environment variables + +|Name|Current value|Defined in|Images
using it|Meaning| +|----|-------------|----------|---------------|-------| +|RCACHE_ENABLED|True|`.env`|rcache,
msghnd,
rat_server,
worker,
uls_downloader|TRUE to use Rcache for synchronous non-GUI requests. FALSE to use legacy ObjStore-based cache| +|RCACHE_CLIENT_PORT|8000|.env,
docker-compose.yaml,
tools/rcache/
Dockerfile|rcache,
rcache_tool|Port on which Rcache REST API service is listening. Also this port is a part of **RCACHE_SERVICE_URL** environment variable, passed to other containers (see below)| +|RCACHE_
POSTGRES_
DSN
|postgresql://
postgres:postgres@
bulk_postgres/rcache
|docker-compose.yaml,
tools/rcache/
Dockerfile|rcache,
rat_server,
msghnd,
rcache_tool|Connection string to Postgres database that stores cache| +|RCACHE_
SERVICE_
URL
|http://rcache:
$\{RCACHE_CLIENT_PORT\}
|docker-compose.yaml,
tools/rcache/
Dockerfile|msghnd,
rat_server,
worker,
uls_downloader,
rcache_tool|Rcache service REST API URL| +|RCACHE_RMQ_
DSN
|amqp://rcache:rcache@
rmq:5672/rcache
|docker-compose.yaml|msghnd,
rat_server,
worker|RabbitMQ AMQP URL Worker uses to send and msghnd/rat_server to receive AFC Response. Note that user and vhost (rcache and rcache in this URL) should be properly configured in RabbitMQ service| +|RCACHE_AFC_
REQ_URL
|http://msghnd:8000/
fbrat/ap-afc/
availableSpectrumInquiry?
nocache=True
|docker-compose.yaml|rcache|REST API Rcache service uses to launch precomputation requests. No precomputation if this environment variable empty or not defined| +|RCACHE_
RULESETS_
URL
|http://rat_server/fbrat/
ratapi/v1/GetRulesetIDs
|docker-compose.yaml|rcache|REST API URL to request list of Ruleset IDs in use. If empty or not defined default AFC distance (130km as of time of this writing) is used for spatial invalidation| +|RCACHE_CONFIG_
RETRIEVAL_URL
|http://rat_server/fbrat/
ratapi/v1/
GetAfcConfigByRulesetID
|docker-compose.yaml|rcache|REST API URL to request AFC Config by for Ruleset ID. If empty or not defined default AFC distance (130km as of time of this writing) is used for spatial invalidation| +|RCACHE_UPDATE_
ON_SEND
|True|worker/Dockerfile|worker|TRUE if worker sends update request to Rcache service (this speeds up AFC request processing, but prolongs worker existence). FALSE if msghnd/rat_server send update request to Rcache service. This setting consumed by worker, but indirectly affects msghnd and rat_server services operation| +|RCACHE_IF_
DB_EXISTS
|leave|rcache/Dockerfile|rcache|What to do with Rcache database if it exists when service started: **leave** - to leave as is, **recreate** - to create from scratch (losing unknown tables, like from Alembic, etc.), **clean** - to clean but leave other tables intact| +|RCACHE_
PRECOMPUTE_
QUOTA
|10|rcache/Dockerfile|rcache|Maximum number of parallel precompute requests. Also can be changed on the fly with Rcache service REST API| + +## Client API
+ +All Rcache client API presented by `RcacheClient` class, defined in shared `rcache_client` module (that should be imported) - and subsequently will (at least - should) be added there. + +Data structures, used in certain interfaces of this class (as well as in Rcache REST APIs, database manipulations, etc.), are defined in `rcache_models` module (that also should be imported). + +Among other data structures, defined in `rcache_models` module there is `RcacheClientSettings` class, containing Rcache client parameters. By default these parameters are taken from environment variables ([see above](#service)), but they also can be explicitly specified. + + +## REST API + +Rcache service is FastAPI-based, so all its REST APIs are self-documented on `/docs`, `/redoc` and even `/openapi.json` endpoints (see ). + +Here is one possible way to reach this trove of knowledge (uppercase are values you need to figure out): + +1. Start AFC docker compose. + There are no sane ways of doing it - pick your favorite insane. +2. Inspect `rcache` docker container: + `docker container inspect AFCPROJECTNAME_rcache_INSTANCEINDEX` +3. Find `rcache` container IP in host file system: + In printed JSON it is at `[0]["NetworkSettings"]["Networks"][DEFAULTNETWORKNAME]["IPAddress"]` +4. Do ssh or plink local port forwarding from your client machine to rcache container IP, port 8000 (or whatever `RCACHE_CLIENT_PORT` is set to): + `SSHORPLINK -L LOCALPORT:RCACHEIPADDRESS:8000` +4. On client machine browse to `localhost:LOCALPORT/redoc` + +## `rcache_tool.py` - test and manipulation tool + +Rcache tool requires certain uncommon Python modules to be installed (pydantic, aiohttp, sqlalchemy, tabulate). Also it uses shared Rcache sources. Hence this script supposed to be run from container. + +Dockerfile for this container can be found in `tools/rcache` - it should be built from top level AFC checkout directory. To run properly this container should be connected to AFC internal network. This might be achieved with `docker run --network NETWORKNAME ...` or by hitching this container to AFC docker compose (`tools/rcache/compose_rcache.yaml` may be used for this). + +Besides its own container this script exists in Rcache service container - and may be ran from there. + +General invocation format: +`./rcache_tool.py SUBCOMMAND PARAMETERS` + +List of available subcommands may be obtained with +`./rcache_tool.py --help` + +Help on individual subcommand parameters may be obtained with +`./rcache_tool.py help SUBCOMMAND` + +Some practical use cases (all examples assumed to be executed from rcache container, so rcache service location is guessed automagically): + +- **Print service status**. Once: + `./rcache_tool.py status` + ... and once per second (e.g. in parallel with some test): + `./rcache_tool.py status --interval 1` + +- **Disable cache invalidation** (e.g. by ULS downloader). Useful to prevent ULS Downloader's influence on performance testing: + `./rcache_tool.py invalidate --disable` + ... and re-enable it back: + `./rcache_tool.py invalidate --enable` + ***Restoring original state is essential, as enable/disable state is persisted in database*** + +- **Invalidate all cache** *AND* prevent precomputer from its re-validation: + `./rcache_tool.py precompute --disable` + `./rcache_tool.py invalidate --all` + ... and restore precomputation back: + `./rcache_tool.py precompute --enable` + ***Restoring original state is essential, as enable/disable state is persisted in database*** + +- **Disable caching**: + `./rcache_tool.py precompute --disable` + `./rcache_tool.py update --disable` + `./rcache_tool.py invalidate --all` + ... and restore status quo ante: + `./rcache_tool.py precompute --enable` + `./rcache_tool.py update --enable` + ***Restoring original state is essential, as enable/disable state is persisted in database*** + +- **Do Rcache update stress test** (`afc_load_tool.py` also can do it), parallel writing from 20 streams: + `./rcache_tool.py mass_fill --max_idx 1000000 --threads 20` + + +## Database schema + +Rcache uses database, consisting of two tables. + +### Rcache table + +Table name is `aps`, each of its rows represent data on a single AP. It has following columns: + +|Column|Is
primary
key|Has
index|Data
type|Comment| +|------|--------------------|------------|------------|-------| +|serial_number|Yes|Yes|String|AP Serial number| +|rulesets|Yes|Yes|String|Request Ruleset IDs, concatenated with `|`. Kinda AP region| +|cert_ids|Yes|Yes|String|Request Certification IDs, concatenated with `|`. Kinda AP manufacturer| +|state|No|Yes|Enum
(Valid,
Invalid,
Precomp)|Row state. **Valid** - may be used, **Invalid** - invalidated, to be precomputed, **Precomp** - precomputation is in progress| +|config_ruleset|No|Yes|String|Ruleset ID used for computation. Used for invalidation by Ruleset ID (i.e. by AFC Config change)| +|lat_deg|No|Yes|Float|AP Latitude as north-positive degree. Used for spatial invalidation (on FS data change)| +|lon_deg|No|Yes|Float|AP Longitude as east-positive degree. Ditto. +|last_update|No|Yes|DateTime|Timetag of last update - eventually may be used for removal of old rows| +|req_cfg_digest|No|Yes|String|Hash, computed over AFC Request and AFC Config. Used as key for cache lookup| +|validity_period_sec|No|No|Float|Validity period of original response. Used to compute expiration time of response retrieved from cache| +|request|No|No|String|AFC Request as string. Used for precomputation| +|response|No|No|String|AFC Response as string. The meat of all this story| + +### Switches table
+ +Table name is `switches`, each of its rows represent enable/disable setting. It has following columns: + +|Column|Comment| +|------|-------| +|name|Setting name. This is a primary key. As of time of this writing, names were: *Update*, *Invalidate*, *Precompute*| +|state|*True* if enabled, *False* if disabled. Default is *True*| diff --git a/rcache/rcache_app.py b/rcache/rcache_app.py new file mode 100644 index 0000000..b77bd46 --- /dev/null +++ b/rcache/rcache_app.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +""" AFC Request cache """ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# pylint: disable=wrong-import-order, global-statement, unnecessary-pass + +import fastapi +import logging +import uvicorn +import sys +from typing import Annotated, Optional + +from rcache_common import dp, get_module_logger, set_dp_printer +from rcache_models import IfDbExists, RcacheServiceSettings, RcacheUpdateReq, \ + RcacheInvalidateReq, RcacheSpatialInvalidateReq, RcacheStatus +from rcache_service import RcacheService + +__all__ = ["app"] + +LOGGER = get_module_logger() +LOGGER.setLevel(logging.INFO) + +# Parameters (passed via environment variables) +settings = RcacheServiceSettings() + +# Request cache object (created upon first request) +rcache_service: Optional[RcacheService] = None + + +def get_service() -> RcacheService: + """ Returns service object (creating it on first call """ + global rcache_service + if not settings.enabled: + raise \ + fastapi.HTTPException( + fastapi.status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Rcache not enabled (RCACHE_ENABLED environment " + "variable set to False") + if rcache_service is None: + rcache_service = \ + RcacheService( + rcache_db_dsn=settings.postgres_dsn, + precompute_quota=settings.precompute_quota, + afc_req_url=settings.afc_req_url, + rulesets_url=settings.rulesets_url, + config_retrieval_url=settings.config_retrieval_url) + return rcache_service + + +# FastAPI APP +app = fastapi.FastAPI() + + +@app.on_event("startup") +async def startup() -> None: + """ App startup event handler """ + set_dp_printer(fastapi.logger.logger.error) + logging.basicConfig(level=logging.INFO) + if not settings.enabled: + return + service = get_service() + if not service.check_db_server(): + LOGGER.error("Can't connect to postgres database server") + sys.exit() + await service.connect_db( + create_if_absent=True, + recreate_db=settings.if_db_exists == IfDbExists.recreate, + recreate_tables=settings.if_db_exists == IfDbExists.clean) + + +@app.on_event("shutdown") +async def shutdown() -> None: + """ App shutdown event handler """ + if not settings.enabled: + return + await get_service().shutdown() + + +@app.get("/healthcheck") +async def healthcheck( + response: fastapi.Response, + service: RcacheService = fastapi.Depends(get_service)) -> None: + """ Healthcheck """ + response.status_code = fastapi.status.HTTP_200_OK if service.healthy() \ + else fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR + + +@app.post("/invalidate") +async def invalidate( + invalidate_req: RcacheInvalidateReq, + service: RcacheService = fastapi.Depends(get_service)) -> None: + """ Invalidate request handler """ + service.invalidate(invalidate_req) + + +@app.post("/spatial_invalidate") +async def spatial_invalidate( + spatial_invalidate_req: RcacheSpatialInvalidateReq, + service: RcacheService = fastapi.Depends(get_service)) -> None: + """ Spatial invalidate request handler """ + service.invalidate(spatial_invalidate_req) + + +@app.post("/update") +async def update( + update_req: RcacheUpdateReq, + service: RcacheService = fastapi.Depends(get_service)) -> None: + """ Cache update request handler """ + service.update(update_req) + + +@app.post("/invalidation_state/{enabled}") +async def set_invalidation_state( + enabled: Annotated[ + bool, + fastapi.Path( + title="true/false to enable/disable invalidation")], + service: RcacheService = fastapi.Depends(get_service)) -> None: + """ Enable/disable invalidation """ + await service.set_invalidation_enabled(enabled) + + +@app.get("/invalidation_state") +async def get_invalidation_state( + service: RcacheService = fastapi.Depends(get_service)) -> bool: + """ Return invalidation enabled state """ + return await service.get_invalidation_enabled() + + +@app.post("/precomputation_state/{enabled}") +async def set_precomputation_state( + enabled: Annotated[ + bool, + fastapi.Path( + title="true/false to enable/disable precomputation")], + service: RcacheService = fastapi.Depends(get_service)) -> None: + """ Enable/disable precomputation """ + await service.set_precomputation_enabled(enabled) + + +@app.get("/precomputation_state") +async def get_precomputation_state( + service: RcacheService = fastapi.Depends(get_service)) -> bool: + """ Return precomputation enabled state """ + return await service.get_precomputation_enabled() + + +@app.post("/update_state/{enabled}") +async def setupdate_state( + enabled: Annotated[ + bool, + fastapi.Path( + title="true/false to enable/disable update")], + service: RcacheService = fastapi.Depends(get_service)) -> None: + """ Enable/disable update """ + await service.set_update_enabled(enabled) + + +@app.get("/update_state") +async def get_precomputation_state( + service: RcacheService = fastapi.Depends(get_service)) -> bool: + """ Return update enabled state """ + return await service.get_update_enabled() + + +@app.get("/status") +async def get_service_status( + service: RcacheService = fastapi.Depends(get_service)) \ + -> RcacheStatus: + """ Get Rcache service status information """ + return await service.get_status() + + +@app.get("/precomputation_quota") +async def get_precompute_quota( + service: RcacheService = fastapi.Depends(get_service)) -> int: + """ Returns current precomputation quota (maximum number of simultaneously + running precomputations) """ + return service.precompute_quota + + +@app.post("/precomputation_quota/{quota}") +async def set_precompute_quota( + quota: Annotated[ + int, + fastapi.Path( + title="Maximum number of simultaneous precomputations", + ge=0)], + service: RcacheService = fastapi.Depends(get_service)) -> None: + """ Sets new precomputation quota (maximum number of simultaneously running + precomputations) """ + service.precompute_quota = quota + + +if __name__ == "__main__": + # Autonomous startup + uvicorn.run(app, host="0.0.0.0", port=settings.port, log_level="info") diff --git a/rcache/rcache_db_async.py b/rcache/rcache_db_async.py new file mode 100644 index 0000000..2c8c3dc --- /dev/null +++ b/rcache/rcache_db_async.py @@ -0,0 +1,293 @@ +""" Synchronous part of AFC Request Cache database stuff """ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# pylint: disable=wrong-import-order, invalid-name, useless-parent-delegation + +import urllib.parse +import sqlalchemy as sa +import sqlalchemy.ext.asyncio as sa_async +import sqlalchemy.dialects.postgresql as sa_pg +from typing import Any, Dict, List, Optional + +from rcache_common import dp, error, error_if, FailOnError, safe_dsn +from rcache_db import RcacheDb +from rcache_models import ApDbRespState, FuncSwitch, LatLonRect, ApDbPk + +__all__ = ["RcacheDbAsync"] + + +class RcacheDbAsync(RcacheDb): + """ Asynchronous work with database, used in Request cache service """ + + # Name of Postgres asynchronous driver (to use in DSN) + ASYNC_DRIVER_NAME = "asyncpg" + + def __init__(self, rcache_db_dsn: str) -> None: + """ Constructor + + Arguments: + rcache_db_dsn -- Postgres database connection string + """ + super().__init__(rcache_db_dsn) + + async def disconnect(self) -> None: + """ Disconnect database """ + if self._engine: + await self._engine.dispose() + self._engine = None + + async def connect(self, fail_on_error=True) -> bool: + """ Connect to database, that is assumed to be existing + + Arguments: + fail_on_error -- True to fail on error, False to return success + status + Returns True on success, Fail on failure (if fail_on_error is False) + """ + engine: Any = None + with FailOnError(fail_on_error): + try: + if self._engine is not None: + self._engine.dispose() + self._engine = None + error_if(not self.rcache_db_dsn, + "AFC Request Cache URL was not specified") + engine = self._create_engine(self.rcache_db_dsn) + dsn_parts = urllib.parse.urlsplit(self.rcache_db_dsn) + self.db_name = dsn_parts.path.strip("/") + self._read_metadata() + async with engine.connect(): + pass + self._engine = engine + engine = None + return True + finally: + if engine is not None: + await engine.dispose() + return False + + async def update_cache(self, rows: List[Dict[str, Any]]) -> None: + """ Update cache with computed AFC Requests + + Arguments: + rows -- List of request/response/request-config digest triplets + """ + if not rows: + return + assert self._engine is not None + assert len(rows) <= self.max_update_records() + try: + ins = sa_pg.insert(self.ap_table).values(rows) + ins = ins.on_conflict_do_update( + index_elements=self.ap_pk_columns, + set_={col_name: ins.excluded[col_name] + for col_name in self.ap_table.columns.keys() + if col_name not in self.ap_pk_columns}) + async with self._engine.begin() as conn: + await conn.execute(ins) + except sa.exc.SQLAlchemyError as ex: + error(f"Cache database upsert failed: {ex}") + + async def invalidate(self, ruleset: Optional[str] = None, + limit: Optional[int] = None) -> int: + """ Invalidate cache - completely or for given config + + Arguments: + ruleset -- None for complete invalidation, ruleset ID for config-based + invalidation + limit -- Optional maximum number of rows to invalidate (10000000 + rows invalidates for half an hour!) + Returns number of rows invalidated + """ + assert self._engine + try: + upd = sa.update(self.ap_table).\ + values(state=ApDbRespState.Invalid.name) + if ruleset: + upd = upd.where(self.ap_table.c.config_ruleset == ruleset) + if limit is None: + upd = \ + upd.where( + self.ap_table.c.state == ApDbRespState.Valid.name) + else: + pk_columns = [self.ap_table.c[col_name] + for col_name in self.ap_pk_columns] + upd = \ + upd.where( + sa.tuple_(*pk_columns).in_( + sa.select(pk_columns). + where(self.ap_table.c.state == + ApDbRespState.Valid.name). + limit(limit))) + async with self._engine.begin() as conn: + rp = await conn.execute(upd) + return rp.rowcount + except sa.exc.SQLAlchemyError as ex: + error(f"Cache database invalidation failed: {ex}") + return 0 # Will never happen + + async def spatial_invalidate(self, rect: LatLonRect) -> None: + """ Spatial invalidation + + Arguments: + rect -- Lat/lon rectangle to invalidate + """ + assert self._engine is not None + c_lat = self.ap_table.c.lat_deg + c_lon = self.ap_table.c.lon_deg + try: + upd = sa.update(self.ap_table).\ + where((self.ap_table.c.state == ApDbRespState.Valid.name) & + (c_lat >= rect.min_lat) & (c_lat <= rect.max_lat) & + (((c_lon >= rect.min_lon) & (c_lon <= rect.max_lon)) | + ((c_lon >= (rect.min_lon - 360)) & + (c_lon <= (rect.max_lon - 360))) | + ((c_lon >= (rect.min_lon + 360)) & + (c_lon <= (rect.max_lon + 360))))).\ + values(state=ApDbRespState.Invalid.name) + async with self._engine.begin() as conn: + await conn.execute(upd) + except sa.exc.SQLAlchemyError as ex: + error(f"Cache database spatial invalidation failed: {ex}") + + async def reset_precomputations(self) -> None: + """ Mark records in precomputation state as invalid """ + assert self._engine + try: + upd = sa.update(self.ap_table).\ + where(self.ap_table.c.state == ApDbRespState.Precomp.name).\ + values(state=ApDbRespState.Invalid.name) + async with self._engine.begin() as conn: + await conn.execute(upd) + except sa.exc.SQLAlchemyError as ex: + error(f"Cache database unprecomputation failed: {ex}") + + async def num_precomputing(self) -> int: + """ Return number of requests currently being precomputed """ + assert self._engine is not None + try: + sel = sa.select([sa.func.count()]).select_from(self.ap_table).\ + where(self.ap_table.c.state == ApDbRespState.Precomp.name) + async with self._engine.begin() as conn: + rp = await conn.execute(sel) + return rp.fetchone()[0] + except sa.exc.SQLAlchemyError as ex: + error(f"Cache database spatial precomputing count query failed: " + f"{ex}") + return 0 # Will never happen. Appeasing MyPy + + async def get_invalid_reqs(self, limit: int) -> List[str]: + """ Return list of invalidated requests, marking them as being + precomputed + + Arguments: + limit -- Maximum number of requests to return + Returns list of requests as strings + """ + assert self._engine is not None + try: + sq = sa.select([self.ap_table.c.serial_number, + self.ap_table.c.rulesets, + self.ap_table.c.cert_ids]).\ + where(self.ap_table.c.state == ApDbRespState.Invalid.name).\ + limit(limit) + upd = sa.update(self.ap_table).\ + values({"state": ApDbRespState.Precomp.name}).\ + where(sa.tuple_(self.ap_table.c.serial_number, + self.ap_table.c.rulesets, + self.ap_table.c.cert_ids).in_(sq)).\ + returning(self.ap_table.c.request) + async with self._engine.begin() as conn: + rp = await conn.execute(upd) + return [row[0] for row in rp] + except sa.exc.SQLAlchemyError as ex: + error(f"Cache database invalidated query failed: {ex}") + return 0 # Will never happen. Appeasing MyPy + + async def get_num_invalid_reqs(self) -> int: + """ Returns number of invalidated records """ + assert self._engine is not None + try: + sel = sa.select([sa.func.count()]).select_from(self.ap_table).\ + where(self.ap_table.c.state == ApDbRespState.Invalid.name) + async with self._engine.begin() as conn: + rp = await conn.execute(sel) + return rp.fetchone()[0] + except sa.exc.SQLAlchemyError as ex: + error(f"Cache database invalidated count query failed: {ex}") + return 0 # Will never happen. Appeasing MyPy + + async def get_cache_size(self) -> int: + """ Returns total number entries in cache (including nonvalid) """ + assert self._engine is not None + try: + sel = sa.select([sa.func.count()]).select_from(self.ap_table) + async with self._engine.begin() as conn: + rp = await conn.execute(sel) + return rp.fetchone()[0] + except sa.exc.SQLAlchemyError as ex: + error(f"Cache database size query failed: {ex}") + return 0 # Will never happen. Appeasing MyPy + + async def delete(self, pk: ApDbPk) -> None: + """ Delete row by primary key """ + try: + d = sa.delete(self.ap_table) + for k, v in pk.dict().items(): + d = d.where(self.ap_table.c[k] == v) + async with self._engine.begin() as conn: + await conn.execute(d) + except sa.exc.SQLAlchemyError as ex: + error(f"Cache database removal failed: {ex}") + + async def get_switch(self, sw: FuncSwitch) -> bool: + """ Gets value of given switch """ + if self.SWITCHES_TABLE_NAME not in self.metadata.tables: + return True + try: + table = self.metadata.tables[self.SWITCHES_TABLE_NAME] + sel = sa.select([table.c.state]).where(table.c.name == sw.name) + async with self._engine.begin() as conn: + rp = await conn.execute(sel) + v = rp.first() + return True if v is None else v[0] + except sa.exc.SQLAlchemyError as ex: + error(f"Error reading switch value for '{sw.name}': {ex}") + return True # Will never happen. Appeasing MyPy + + async def set_switch(self, sw: FuncSwitch, state: bool) -> None: + """ Sets value of given switch """ + error_if(self.SWITCHES_TABLE_NAME not in self.metadata.tables, + f"Table '{self.SWITCHES_TABLE_NAME}' not found in " + f"'{self.db_name}' database") + try: + table = self.metadata.tables[self.SWITCHES_TABLE_NAME] + ins = sa_pg.insert(table).values(name=sw.name, state=state) + ins = ins.on_conflict_do_update(index_elements=["name"], + set_={"state": state}) + async with self._engine.begin() as conn: + await conn.execute(ins) + except sa.exc.SQLAlchemyError as ex: + error(f"Switch setting upsert failed: {ex}") + + def _create_engine(self, dsn) -> Any: + """ Creates asynchronous SqlAlchemy engine """ + try: + parts = urllib.parse.urlsplit(dsn) + except ValueError as ex: + error(f"Invalid database DSN syntax: '{safe_dsn(dsn)}': {ex}") + if self.ASYNC_DRIVER_NAME not in parts: + dsn = \ + urllib.parse.urlunsplit( + parts._replace( + scheme=f"{parts.scheme}+{self.ASYNC_DRIVER_NAME}")) + try: + return sa_async.create_async_engine(dsn) + except sa.exc.SQLAlchemyError as ex: + error(f"Invalid database DSN: '{safe_dsn(dsn)}': {ex}") + return None # Will never happen. Appeasing pylint diff --git a/rcache/rcache_service.py b/rcache/rcache_service.py new file mode 100644 index 0000000..0645682 --- /dev/null +++ b/rcache/rcache_service.py @@ -0,0 +1,479 @@ +""" Implementation of cache service activities """ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# pylint: disable=wrong-import-order, invalid-name, too-many-arguments +# pylint: disable=too-many-instance-attributes, too-few-public-methods + +import aiohttp +import asyncio +import datetime +import http +import json +import logging +import math +import pydantic +from queue import Queue +import time +import traceback +from typing import Any, Dict, Optional, Set, Tuple, Union + +from rcache_common import dp, error_if, get_module_logger +from rcache_db_async import RcacheDbAsync +from rcache_models import AfcReqRespKey, RcacheUpdateReq, ApDbPk, ApDbRecord, \ + FuncSwitch, RatapiAfcConfig, RatapiRulesetIds, RcacheInvalidateReq, \ + LatLonRect, RcacheSpatialInvalidateReq, RcacheStatus + +__all__ = ["RcacheService"] + +# Module logger +LOGGER = get_module_logger() +LOGGER.setLevel(logging.INFO) + +# Default maximum distance between FS ands AP in kilometers +DEFAULT_MAX_MAX_LINK_DISTANCE_KM = 200. + +# Number of degrees per kilometer +DEGREES_PER_KM = 1 / (60 * 1.852) + +# Default length of averaging window +AVERAGING_WINDOW_SIZE = 10 + +# Maximum number of rows to invalidate at a time (None if all) +# Currently used for complete/ruleset invalidation and not for spatial one (as +# there is, probably, no need to) +INVALIDATION_CHUNK_SIZE = 1000 + + +class Ema: + """ Exponential moving average for some value or its rate + + Public attributes: + ema -- Current average value + + Private attributes: + _weight -- Weighting factor (2/(window_length+1) + _is_rate -- True if rate is averaged, False if value + _prev_value -- Previous value + """ + + def __init__(self, win_size: int, is_rate: bool) -> None: + """ Constructor + + Arguments: + win_length -- Window length + is_rate -- True for rate averaging, False for value averaging + """ + self._weight = 2 / (win_size + 1) + self._is_rate = is_rate + self.ema: float = 0 + self._prev_value: float = 0 + + def periodic_update(self, new_value: float) -> None: + """ Update with new value """ + measured_value = new_value + if self._is_rate: + measured_value -= self._prev_value + self._prev_value = new_value + self.ema += self._weight * (measured_value - self.ema) + + +class RcacheService: + """ Manager of all server-side actions + + Private attributes: + _start_time -- When service started + _precompute_quota -- Maximum number of precomputing requests + in flight + _afc_req_url -- REST API URL to send requests for + precomputation. None for no + precomputation + _rulesets_url -- REST API URL for getting list of active + Ruleset IDs. None to use default maximum + AP FS distance + _config_retrieval_url -- REST API URL for retrieving AFC Config by + Ruleset ID. None to use default maximum + AP FS distance + _db -- Database manager + _db_connected_event -- Set after db connected + _update_queue -- Queue for arrived update requests + _invalidation_queue -- Queue for arrived invalidation requests + _precompute_event -- Event that triggers precomputing task + (set on invalidations and update + requests) + _main_tasks -- Set of top level tasks + _precomputer_subtasks -- References that keep individual + precomputer tasks out of oblivion + _updated_count -- Number of processed updates + _precompute_count -- Number of initiated precomputations + _updated_rate_ema -- Average rate of database write + _update_queue_size_ema -- Average length of update queue + _precomputation_rate_ema -- Average rate of initiated precomputations + _all_tasks_running -- True while no tasks crashed + _schedule_lag_ema -- Average scheduling delay + """ + + def __init__(self, rcache_db_dsn: str, precompute_quota: int, + afc_req_url: Optional[str], rulesets_url: Optional[str], + config_retrieval_url: Optional[str]) -> None: + self._start_time = datetime.datetime.now() + self._db = RcacheDbAsync(rcache_db_dsn) + self._db_connected_event = asyncio.Event() + self._afc_req_url = afc_req_url + self._rulesets_url = rulesets_url.rstrip("/") if rulesets_url else None + self._config_retrieval_url = config_retrieval_url.rstrip("/") \ + if config_retrieval_url else None + self._invalidation_queue: \ + Queue[Union[RcacheInvalidateReq, RcacheSpatialInvalidateReq]] = \ + asyncio.Queue() + self._update_queue: Queue[AfcReqRespKey] = asyncio.Queue() + self._precompute_event = asyncio.Event() + self._precompute_event.set() + self._precompute_quota = 0 + self.precompute_quota = precompute_quota + self._main_tasks: Set[asyncio.Task] = set() + for worker in (self._invalidator_worker, self._updater_worker, + self._precomputer_worker, self._averager_worker): + self._main_tasks.add(asyncio.create_task(worker())) + self._precomputer_subtasks: Set[asyncio.Task] = set() + self._updated_count = 0 + self._precompute_count = 0 + self._updated_rate_ema = Ema(win_size=AVERAGING_WINDOW_SIZE, + is_rate=True) + self._update_queue_len_ema = Ema(win_size=AVERAGING_WINDOW_SIZE, + is_rate=False) + self._precomputation_rate_ema = Ema(win_size=AVERAGING_WINDOW_SIZE, + is_rate=True) + self._all_tasks_running = True + self._schedule_lag_ema = Ema(win_size=AVERAGING_WINDOW_SIZE, + is_rate=False) + + async def get_invalidation_enabled(self) -> bool: + """ Current invalidation enabled state """ + return await self._db.get_switch(FuncSwitch.Invalidate) + + async def set_invalidation_enabled(self, value: bool) -> None: + """ Enables/disables invalidation """ + await self._db.set_switch(FuncSwitch.Invalidate, value) + + async def get_precomputation_enabled(self) -> bool: + """ Current precomputation enabled state """ + return await self._db.get_switch(FuncSwitch.Precompute) + + async def set_precomputation_enabled(self, value: bool) -> None: + """ Enables/disables precomputation """ + await self._db.set_switch(FuncSwitch.Precompute, value) + + async def get_update_enabled(self) -> bool: + """ Current update enabled state """ + return await self._db.get_switch(FuncSwitch.Update) + + async def set_update_enabled(self, value: bool) -> None: + """ Enables/disables update """ + await self._db.set_switch(FuncSwitch.Update, value) + + @property + def precompute_quota(self) -> int: + """ Returns current precompute quota """ + return self._precompute_quota + + @precompute_quota.setter + def precompute_quota(self, value: int) -> None: + """ Sets precompute quota """ + error_if(value < 0, f"Precompute quota of {value} is invalid") + self._precompute_quota = value + + def check_db_server(self) -> bool: + """ Check if database server can be connected to """ + return self._db.check_server() + + def healthy(self) -> bool: + """ Service is in healthy status """ + return self._all_tasks_running and \ + self._db_connected_event.is_set() + + async def connect_db(self, create_if_absent=False, recreate_db=False, + recreate_tables=False) -> None: + """ Connect to database """ + if create_if_absent or recreate_db or recreate_tables: + self._db.create_db(recreate_db=recreate_db, + recreate_tables=recreate_db) + await self._db.connect() + err = ApDbRecord.check_db_table(self._db.ap_table) + error_if(err, f"Request cache database has unexpected format: {err}") + self._db_connected_event.set() + + async def shutdown(self) -> None: + """ Shut service down """ + while self._main_tasks: + task = self._main_tasks.pop() + task.cancel() + while self._precomputer_subtasks: + task = self._precomputer_subtasks.pop() + task.cancel() + await self._db.disconnect() + + def update(self, cache_update_req: RcacheUpdateReq) -> None: + """ Enqueue arrived update requests """ + for rrk in cache_update_req.req_resp_keys: + self._update_queue.put_nowait(rrk) + + def invalidate( + self, + invalidation_req: Union[RcacheInvalidateReq, + RcacheSpatialInvalidateReq]) -> None: + """ Enqueue arrived invalidation requests """ + self._invalidation_queue.put_nowait(invalidation_req) + + async def get_status(self) -> RcacheStatus: + """ Returns service status """ + num_invalid_entries = await self._db.get_num_invalid_reqs() \ + if self._db_connected_event.is_set() else -1 + return \ + RcacheStatus( + up_time=datetime.datetime.now() - self._start_time, + db_connected=self._db_connected_event.is_set(), + all_tasks_running=self._all_tasks_running, + invalidation_enabled=await self.get_invalidation_enabled(), + precomputation_enabled=await self.get_precomputation_enabled(), + update_enabled=await self.get_update_enabled(), + precomputation_quota=self._precompute_quota, + num_valid_entries=(max(0, + await self._db.get_cache_size() - + num_invalid_entries)) + if self._db_connected_event.is_set() else -1, + num_invalid_entries=num_invalid_entries, + update_queue_len=self._update_queue.qsize(), + update_count=self._updated_count, + avg_update_write_rate=round(self._updated_rate_ema.ema, 2), + avg_update_queue_len=round(self._update_queue_len_ema.ema, 2), + num_precomputed=self._precompute_count, + active_precomputations=len(self._precomputer_subtasks), + avg_precomputation_rate=round( + self._precomputation_rate_ema.ema, 3), + avg_schedule_lag=round(self._schedule_lag_ema.ema, 3)) + + async def _updater_worker(self) -> None: + """ Cache updater task worker """ + try: + await self._db_connected_event.wait() + while True: + update_bulk: Dict[Tuple[Any, ...], Dict[str, Any]] = {} + rrk = await self._update_queue.get() + while True: + try: + dr = ApDbRecord.from_req_resp_key(rrk) + except pydantic.ValidationError as ex: + LOGGER.error( + f"Invalid format of cache update data: {ex}") + else: + if dr is not None: + row_dict = dr.dict() + update_bulk[self._db.get_ap_pk(row_dict)] = \ + row_dict + if (len(update_bulk) == self._db.max_update_records()) or \ + self._update_queue.empty(): + break + rrk = await self._update_queue.get() + if update_bulk and await self.get_update_enabled(): + await self._db.update_cache(list(update_bulk.values())) + self._updated_count += len(update_bulk) + self._precompute_event.set() + except asyncio.CancelledError: + return + except BaseException as ex: + self._all_tasks_running = False + LOGGER.error(f"Updater task unexpectedly aborted:\n" + f"{''.join(traceback.format_exception(ex))}") + + async def _invalidator_worker(self) -> None: + """ Cache invalidator task worker """ + try: + await self._db_connected_event.wait() + while True: + req = await self._invalidation_queue.get() + while not await self.get_invalidation_enabled(): + await asyncio.sleep(1) + invalid_before = await self._report_invalidation() + if isinstance(req, RcacheInvalidateReq): + if req.ruleset_ids is None: + while await self._db.invalidate( + limit=INVALIDATION_CHUNK_SIZE): + pass + await self._report_invalidation( + "Complete invalidation", invalid_before) + else: + for ruleset_id in req.ruleset_ids: + while await self._db.invalidate( + ruleset_id, limit=INVALIDATION_CHUNK_SIZE): + pass + invalid_before = \ + await self._report_invalidation( + f"AFC Config for ruleset '{ruleset_id}' " + f"invalidation", invalid_before) + else: + assert isinstance(req, RcacheSpatialInvalidateReq) + max_link_distance_km = \ + await self._get_max_max_link_distance_km() + max_link_distance_deg = \ + max_link_distance_km * DEGREES_PER_KM + for rect in req.tiles: + lon_reduction = \ + max(math.cos( + math.radians( + (rect.min_lat + rect.max_lat) / 2)), + 1 / 180) + await self._db.spatial_invalidate( + LatLonRect( + min_lat=rect.min_lat - max_link_distance_deg, + max_lat=rect.max_lat + max_link_distance_deg, + min_lon=rect.min_lon - + max_link_distance_deg / lon_reduction, + max_lon=rect.max_lon + + max_link_distance_deg / lon_reduction)) + invalid_before = \ + await self._report_invalidation( + f"Spatial invalidation for tile " + f"<{rect.short_str()}> with clearance of " + f"{max_link_distance_km}km", + invalid_before) + self._precompute_event.set() + except asyncio.CancelledError: + return + except BaseException as ex: + self._all_tasks_running = False + LOGGER.error(f"Invalidator task unexpectedly aborted:\n" + f"{''.join(traceback.format_exception(ex))}") + + async def _report_invalidation( + self, dsc: Optional[str] = None, + invalid_before: Optional[int] = None) -> int: + """ Make a log record on invalidation, compute invalid count + + Argumentsa: + dsc -- Invalidation description. None to not make log print + invalid_before -- Number of invalid records before operation. Might be + None if dsc is None + Returns number of invalid records after operation + """ + ret = await self._db.get_num_invalid_reqs() + if dsc is not None: + assert invalid_before is not None + LOGGER.info(f"{dsc}: {invalid_before} was invalidated before " + f"operation, {ret} is invalidated after operation, " + f"increase of {ret - invalid_before}") + return ret + + async def _single_precompute_worker(self, req: str) -> None: + """ Single request precomputer subtask worker """ + try: + async with aiohttp.ClientSession() as session: + assert self._afc_req_url is not None + async with session.post(self._afc_req_url, + json=json.loads(req)) as resp: + if resp.ok: + return + await self._db.delete(ApDbPk.from_req(req_str=req)) + except (asyncio.CancelledError, + aiohttp.client_exceptions.ServerDisconnectedError): + # Frankly, it's beyond my understanding why ServerDisconnectedError + # happens before shutdown initiated by uvicorn in Compose + # environment...) + return + except BaseException as ex: + LOGGER.error(f"Precomputation subtask for request '{req}' " + f"unexpectedly aborted:\n" + f"{''.join(traceback.format_exception(ex))}") + + async def _precomputer_worker(self) -> None: + """ Precomputer task worker """ + if self._afc_req_url is None: + return + try: + await self._db_connected_event.wait() + await self._db.reset_precomputations() + while True: + while not await self.get_precomputation_enabled(): + await asyncio.sleep(1) + await self._precompute_event.wait() + self._precompute_event.clear() + remaining_quota = \ + self._precompute_quota - await self._db.num_precomputing() + if remaining_quota <= 0: + continue + invalid_reqs = \ + await self._db.get_invalid_reqs(limit=remaining_quota) + if not invalid_reqs: + continue + self._precompute_event.set() + for req in invalid_reqs: + self._precompute_count += 1 + task = \ + asyncio.create_task( + self._single_precompute_worker(req)) + self._precomputer_subtasks.add(task) + task.add_done_callback(self._precomputer_subtasks.discard) + except asyncio.CancelledError: + return + except BaseException as ex: + self._all_tasks_running = False + LOGGER.error(f"Precomputer task unexpectedly aborted:\n" + f"{''.join(traceback.format_exception(ex))}") + + async def _get_max_max_link_distance_km(self) -> float: + """ Retrieves maximum AP-FS distance in unit of latitude degrees """ + if (self._rulesets_url is not None) and \ + (self._config_retrieval_url is not None): + ret: Optional[float] = None + try: + async with aiohttp.ClientSession() as session: + async with session.get(self._rulesets_url) as resp: + if resp.status != http.HTTPStatus.OK.value: + raise aiohttp.ClientError( + "Can't receive list of active configurations") + rulesets = \ + RatapiRulesetIds.parse_obj(await resp.json()) + for ruleset in rulesets.rulesetId: + async with session.get( + f"{self._config_retrieval_url}/{ruleset}") \ + as resp: + if resp.status != http.HTTPStatus.OK.value: + continue + maxLinkDistance = \ + RatapiAfcConfig.parse_obj( + await resp.json()).maxLinkDistance + if (ret is None) or (maxLinkDistance > ret): + ret = maxLinkDistance + if ret is not None: + return ret + except aiohttp.ClientError as ex: + LOGGER.error(f"Error retrieving maximum maxLinkDistance: {ex}") + except pydantic.ValidationError as ex: + LOGGER.error(f"Error decoding response: {ex}") + LOGGER.error(f"Default maximum maxinkDistance of " + f"{DEFAULT_MAX_MAX_LINK_DISTANCE_KM}km will be used") + return DEFAULT_MAX_MAX_LINK_DISTANCE_KM + + async def _averager_worker(self) -> None: + """ Averager task worker """ + try: + while True: + timetag = time.time() + await asyncio.sleep(1) + self._schedule_lag_ema.periodic_update( + time.time() - timetag - 1) + self._update_queue_len_ema.periodic_update( + self._update_queue.qsize()) + self._updated_rate_ema.periodic_update(self._updated_count) + self._precomputation_rate_ema.periodic_update( + self._precompute_count) + except asyncio.CancelledError: + return + except BaseException as ex: + self._all_tasks_running = False + LOGGER.error(f"Averager task unexpectedly aborted:\n" + f"{''.join(traceback.format_exception(ex))}") diff --git a/rcache/requirements.txt b/rcache/requirements.txt new file mode 100644 index 0000000..3d06e35 --- /dev/null +++ b/rcache/requirements.txt @@ -0,0 +1,11 @@ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +asyncpg +fastapi +tabulate +uvicorn diff --git a/rlanafc.pro b/rlanafc.pro new file mode 100644 index 0000000..baf4e2c --- /dev/null +++ b/rlanafc.pro @@ -0,0 +1,22 @@ +###################################################################### +# Automatically generated by qmake (2.01a) Wed Feb 8 15:29:23 2012 +###################################################################### + +TEMPLATE = app +TARGET = rlanafc +DEPENDPATH += . +INCLUDEPATH += . /usr/include/gdal +CONFIG += debug + +QMAKE_CXXFLAGS_WARN_ON += -Werror=format-extra-args +QMAKE_CXXFLAGS_WARN_ON += -Werror=format +QMAKE_CXXFLAGS_WARN_ON += -Werror=shadow +QMAKE_CXXFLAGS += -std=gnu++11 +QMAKE_LIBS += -lz -lgdal -larmadillo + +DEFINES += QT_NO_DEBUG_OUTPUT + +# Input +HEADERS += $$files(*.h) + +SOURCES += $$files(*.cpp) diff --git a/runcmd.py b/runcmd.py new file mode 100755 index 0000000..adf2672 --- /dev/null +++ b/runcmd.py @@ -0,0 +1,108 @@ +#!/usr/bin/python +''' This program is used to run a command in the background, with +its output redirected to a log file. The program prints periodic +status messages to avoid timeouts of the runner of this program. +The exit code of the subprocess is used as this program's exit.name +''' + +import sys +import optparse +import threading +import os +import subprocess +import time +import datetime + +if sys.version_info[0] >= 3: + # Python 3 specific definitions + echo_outfile = sys.stdout.buffer +else: + # Python 2 specific definitions + echo_outfile = sys.stdout + + +class StatusTimer(threading.Thread): + def __init__(self, timeoutSecs): + threading.Thread.__init__(self) + self.daemon = True # Exit with the parent process + + self._began = datetime.datetime.now() + self._timeout = timeoutSecs + + def run(self): + while True: + time.sleep(self._timeout) + diff = datetime.datetime.now() - self._began + sys.stdout.write('Running for %.1f minutes...\n' % + (diff.seconds / 60.0)) + sys.stdout.flush() + + +class Monitor(threading.Thread): + def __init__(self, stream, outFile): + ''' Process lines from a stream and write to the file. + ''' + threading.Thread.__init__(self) + self.daemon = True # Exit with the parent process + + self._stream = stream + self._file = outFile + + def run(self): + ''' Run within the thread context. ''' + for line in iter(self._stream.readline, b''): + # Avoid duplicate line endings + line = line.rstrip() + # Tee the output per-line + for out in (self._file, echo_outfile): + out.write(line + b'\n') + out.flush() + + +def main(argv): + parser = optparse.OptionParser( + usage='usage: %prog [options] ' + ) + parser.add_option( + '-a', dest='append', action='store_true', default=False, + help='Append to the output file' + ) + + (options, args) = parser.parse_args(argv[1:]) + if len(args) < 2: + parser.print_usage() + return 1 + + outFileName = args[0] + cmdParts = args[1:] + + # Prepare output file + openMode = 'wb' + if options.append: + openMode += '+' + outFile = open(outFileName, openMode) + + if os.name == 'nt': + useShell = True + else: + useShell = False + + # Spawn the child + proc = subprocess.Popen( + cmdParts, shell=useShell, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + close_fds=('posix' in sys.builtin_module_names) + ) + + # Wait for completion with background threads + mon = Monitor(proc.stdout, outFile) + mon.start() + + exitCode = proc.wait() + mon.join() + return exitCode + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/sedacdata.png b/sedacdata.png new file mode 100644 index 0000000..422ee9b Binary files /dev/null and b/sedacdata.png differ diff --git a/selinux/fbrat.fc b/selinux/fbrat.fc new file mode 100644 index 0000000..f7439be --- /dev/null +++ b/selinux/fbrat.fc @@ -0,0 +1,3 @@ +/usr/bin/rat-manage-api -- gen_context(system_u:object_r:fbrat_exec_t) +/usr/bin/afc-engine -- gen_context(system_u:object_r:fbrat_exec_t) +/var/lib/fbrat(/.*)? gen_context(system_u:object_r:fbrat_data_t) diff --git a/selinux/fbrat.if b/selinux/fbrat.if new file mode 100644 index 0000000..399f1a4 --- /dev/null +++ b/selinux/fbrat.if @@ -0,0 +1 @@ +## Policy for Facebook RLAN AFC Tool diff --git a/selinux/fbrat.te b/selinux/fbrat.te new file mode 100644 index 0000000..fafd432 --- /dev/null +++ b/selinux/fbrat.te @@ -0,0 +1,34 @@ +# Versioning for this module, not for RPM packages +policy_module(fbrat, 1.1) + +require { + type fs_t; + type httpd_t; + type init_t; + type unconfined_t; + class file { read getattr open }; +} + + +# HTTP Daemon itself runs under httpd_t domain, this is for management processes +type fbrat_t; +type fbrat_exec_t; +application_domain(fbrat_t, fbrat_exec_t) + +# System configuration types +type fbrat_conf_t; +files_config_file(fbrat_conf_t) + +# Daemon storage data types +type fbrat_data_t; +files_type(fbrat_data_t) + +type_transition unconfined_t fbrat_exec_t : process fbrat_t; +type_transition httpd_t fbrat_exec_t : process fbrat_t; + +manage_dirs_pattern(httpd_t, fbrat_data_t, fbrat_data_t) +manage_files_pattern(httpd_t, fbrat_data_t, fbrat_data_t) +# Run the child process in different domain +domain_auto_trans(httpd_t, fbrat_exec_t, fbrat_t) +manage_dirs_pattern(fbrat_t, fbrat_data_t, fbrat_data_t) +manage_files_pattern(fbrat_t, fbrat_data_t, fbrat_data_t) diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..8d5ae45 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,2 @@ +*.cpp.x +*.h.x diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..b3bf1cb --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,43 @@ +# Allow includes relative to this "src" directory +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) +# Allow includes for generated headers +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +include_directories(${Boost_INCLUDE_DIRS}) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(BUILD_SHARED_LIBS ON) +set(CMAKE_AUTOMOC ON) + +# Helper functions for all target types +include(srcfunctions) +set(TARGET_LIBS "") +set(TARGET_BINS "") +set(TARGET_SBINS "") + +# Build in selected sub-directories +if ("RatapiDebug" IN_LIST OPENAFC_BUILD_TYPE OR + "RatapiRelease" IN_LIST OPENAFC_BUILD_TYPE) + add_subdirectory(ratapi) +endif() +if ("WebDebug" IN_LIST OPENAFC_BUILD_TYPE OR + "WebRelease" IN_LIST OPENAFC_BUILD_TYPE) + add_subdirectory(web) +endif() +if ("EngineDebug" IN_LIST OPENAFC_BUILD_TYPE OR + "EngineRelease" IN_LIST OPENAFC_BUILD_TYPE) + message(STATUS "Make visible") + add_subdirectory(ratcommon) + add_subdirectory(afclogging) + add_subdirectory(afcsql) + if(BUILD_AFCENGINE) + add_subdirectory(afc-engine) + endif(BUILD_AFCENGINE) +endif() +if ("Ulsprocessor" IN_LIST OPENAFC_BUILD_TYPE) + add_subdirectory(coalition_ulsprocessor) + add_subdirectory(ratapi) +endif() + +# Make visible to project scope +set(TARGET_LIBS "${TARGET_LIBS}" PARENT_SCOPE) diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..86073d4 --- /dev/null +++ b/src/README.md @@ -0,0 +1,13 @@ +open-afc/src contains several modules/packages. + +afc-engine: Performs the core computational analysis to determine channel availability. The afc-engine reads a json configuration file and a json request file, and generates a json response file. + +coalition_ulsprocessor: Takes raw FS data in the format provide by the FCC, or corresponding government agency for other countries, and pieces together FS links. + +afclogging: Library used by afc-engine for creating run logs. + +afcsql: Library used by the afc-engine for reading sqlite database files. + +ratapi: Python layer that provides the REST API for administration of the system and responding to requests + +web: see src/web/README.md diff --git a/src/afc-engine-preload/Makefile b/src/afc-engine-preload/Makefile new file mode 100644 index 0000000..d048284 --- /dev/null +++ b/src/afc-engine-preload/Makefile @@ -0,0 +1,17 @@ +# Copyright (C) 2023 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +all: libaep.so + +CXXFLAGS=-Wall -D_GNU_SOURCE -fPIC -DNO_GOOGLE_LINK -O2 +#CXXFLAGS=-Wall -D_GNU_SOURCE -fPIC -DNO_GOOGLE_LINK -O0 -g + +libaep.so: aep.o + g++ -rdynamic -shared -o $@ $^ #-lgoogle_cloud_cpp_storage + +clean: + rm -fv *.so *.o + diff --git a/src/afc-engine-preload/README.md b/src/afc-engine-preload/README.md new file mode 100644 index 0000000..58cc520 --- /dev/null +++ b/src/afc-engine-preload/README.md @@ -0,0 +1,28 @@ +This work is licensed under the OpenAFC Project License, a copy of which is included with this software program. + +
+
+ +# Table of Contents +- [Table of Contents](#table-of-contents) +- [The afc-engine preload library](#the-afc-engine-preload-library) +# The afc-engine preload library +The afc-engine preload library redirects read accesses to the geospatial data. The access can be redirected to other local directory or to remote storage. The redirection is implemented by overwriting file access syscalls. + +The preload library also enables the local cache of geospatial data files. + +This library requeres a file tree info of the redirected directory which is created by src/afc-engine-preload/parse_fs.py script: +``` +src/afc-engine-preload/parse_fs.py +``` + +The library could be configured by the following env vars in the worker docker: +- **AFC_AEP_ENABLE**=any_value - Enable the library if defined. Default: Not defined. +- **AFC_AEP_FILELIST**=path - Path to file tree info file. Default: /aep/list/aep.list +- **AFC_AEP_DEBUG**=number - Log level. 0 - disable, 1 - log time of read operations. Default: 0 +- **AFC_AEP_LOGFILE**=path - Where to write the log. Default: /aep/log/aep.log +- **AFC_AEP_CACHE**=path - Where to store the cache. Default: /aep/cache +- **AFC_AEP_CACHE_MAX_FILE_SIZE**=`size`- Cache files with size less than `size`. Default: 50.000.000 bytes +- **AFC_AEP_CACHE_MAX_SIZE**=`size`- Max cache size. Default: 1.000.000.000 bytes +- **AFC_AEP_REAL_MOUNTPOINT**=path - Redirect read access to there. Default: /mnt/nfs/rat_transfer/3dep/1_arcsec +- **AFC_AEP_ENGINE_MOUNTPOINT**=path - Redirect read access from here. Default: the value of **AFC_AEP_REAL_MOUNTPOINT** var diff --git a/src/afc-engine-preload/aep.cpp b/src/afc-engine-preload/aep.cpp new file mode 100644 index 0000000..cea13de --- /dev/null +++ b/src/afc-engine-preload/aep.cpp @@ -0,0 +1,1672 @@ +/* Copyright (C) 2023 Broadcom. All rights reserved. + * The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate + * that owns the software below. + * This work is licensed under the OpenAFC Project License, a copy of which is + * included with this software program. */ + +#define __DEFINED_struct__IO_FILE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define AEP_PATH_MAX PATH_MAX +#define AEP_FILENAME_MAX FILENAME_MAX + +#define HASH_SIZE USHRT_MAX + +/* debug flags */ +#define DBG_LOG 1 /* statistic log */ +#define DBG_DBG 2 /* debug messages */ +#define DBG_CACHED 8 /* cached file IO */ +#define DBG_ANY 4 /* any file IO */ + +#define aep_assert(cond, format, ...) \ + if (!(cond)) { \ + fprintf(stderr, format " Abort!\n", ##__VA_ARGS__); \ + if (aep_debug) { \ + dprintf(logfile, format " Abort!\n", ##__VA_ARGS__); \ + } \ + abort(); \ + } +#define dbg(format, ...) \ + if (aep_debug & DBG_DBG) { \ + dprintf(logfile, "%d: " format "\n", getpid(), ##__VA_ARGS__); \ + } +#define dbge(format, ...) \ + fprintf(stderr, format " Error!\n", ##__VA_ARGS__); \ + if (aep_debug) { \ + dprintf(logfile, format " Error!\n", ##__VA_ARGS__); \ + } +#define dbgd(format, ...) \ + if (aep_debug & DBG_CACHED) { \ + dprintf(logfile, "data " format "\n", ##__VA_ARGS__); \ + } +#define dbgo(format, ...) \ + if (aep_debug & DBG_ANY) { \ + dprintf(logfile, "orig " format "\n", ##__VA_ARGS__); \ + } +#define dbgl(format, ...) \ + if (aep_debug & DBG_LOG) { \ + dprintf(logfile, format "\n", ##__VA_ARGS__); \ + } + +typedef int (*orig_fcntl_t)(int, int, ...); + +/* from musl-1.2.3/src/dirent/__dirent.h */ +struct __dirstream { + off_t tell; + int fd; + int buf_pos; + int buf_end; + volatile int lock[1]; + /* Any changes to this struct must preserve the property: + * offsetof(struct __dirent, buf) % sizeof(off_t) == 0 */ + char buf[2048]; +}; + +#ifndef __GLIBC__ +/* from musl-1.2.3/src/stat/fstatat.c */ +struct statx { + uint32_t stx_mask; + uint32_t stx_blksize; + uint64_t stx_attributes; + uint32_t stx_nlink; + uint32_t stx_uid; + uint32_t stx_gid; + uint16_t stx_mode; + uint16_t pad1; + uint64_t stx_ino; + uint64_t stx_size; + uint64_t stx_blocks; + uint64_t stx_attributes_mask; + struct { + int64_t tv_sec; + uint32_t tv_nsec; + int32_t pad; + } stx_atime, stx_btime, stx_ctime, stx_mtime; + uint32_t stx_rdev_major; + uint32_t stx_rdev_minor; + uint32_t stx_dev_major; + uint32_t stx_dev_minor; + uint64_t spare[14]; +}; + +/* from musl-1.2.3/src/internal/stdio_impl.h */ +struct _IO_FILE { + unsigned flags; + unsigned char *rpos, *rend; + int (*close)(FILE *); + unsigned char *wend, *wpos; + unsigned char *mustbezero_1; + unsigned char *wbase; + size_t (*read)(FILE *, unsigned char *, size_t); + size_t (*write)(FILE *, const unsigned char *, size_t); + off_t (*seek)(FILE *, off_t, int); + unsigned char *buf; + size_t buf_size; + FILE *prev, *next; + int fd; + int pipe_pid; + long lockcount; + int mode; + volatile int lock; + int lbf; + void *cookie; + off_t off; + char *getln_buf; + void *mustbezero_2; + unsigned char *shend; + off_t shlim, shcnt; + FILE *prev_locked, *next_locked; + struct __locale_struct *locale; +}; +#endif /* __GLIBC__ */ + +typedef struct { + unsigned int read_remote_size; + unsigned int read_remote; + unsigned int read_remote_time; + unsigned int read_cached_size; + unsigned int read_cached; + unsigned int read_cached_time; + unsigned int read_write_size; + unsigned int read_write; + unsigned int read_write_time; +} aep_statistic_t; + +static struct stat def_stat = {.st_dev = 0x72, + .st_ino = 0x6ea7ca04, + .st_nlink = 0x1, + .st_mode = 0, /* S_IFDIR | S_IRWXU | S_IRWXG | S_IRWXO - directory, + S_IFREG | S_IRUSR | S_IRGRP | S_IROTH - file */ + .st_uid = 0x4466, + .st_gid = 0x592, + .st_rdev = 0, + .st_size = 0, + .st_blksize = 0x80000, + .st_blocks = 0, /* st_size / 512 rounded up */ + .st_atim = {0x63b45b04, 0}, + .st_mtim = {0x63b45b04, 0}, + .st_ctim = {0x63b45b04, 0}}; + +static struct statx def_statx = { + .stx_mask = 0x17ff, + .stx_blksize = 0x80000, + .stx_attributes = 0, + .stx_nlink = 1, + .stx_uid = 0x4466, + .stx_gid = 0x592, + .stx_mode = 0, /* S_IFDIR | S_IRWXU | S_IRWXG | S_IRWXO - directory, + S_IFREG | S_IRUSR | S_IRGRP | S_IROTH - file */ + .stx_ino = 0x6ea7ca04, + .stx_size = 0, + .stx_blocks = 0, /* stx_size/512 rounded up */ + .stx_attributes_mask = 0x203000, + .stx_atime = {0x63b45b04, 0}, + .stx_btime = {0x63b45b04, 0}, + .stx_ctime = {0x63b45b04, 0}, + .stx_mtime = {0x63b45b04, 0}, + .stx_rdev_major = 0, + .stx_rdev_minor = 0, + .stx_dev_major = 0, + .stx_dev_minor = 0x72}; + +typedef struct fe_t fe_t; + +typedef struct fe_t { + fe_t *next, *down; + char *name; + int64_t size; +} fe_t; + +typedef struct { + fe_t *fe; + FILE file; + DIR dir; + off_t off; + char *tpath; + struct dirent dirent; + fe_t *readdir_p; +} data_fd_t; + +/* the root "/" entry of file tree */ +static fe_t tree = {}; +/* open files array */ +static std::map data_fds; + +/* Dynamically allocated buffers. Sometime in the future I'll free them */ +static uint8_t *filelist; /* row file tree buffer */ +static fe_t *fes; /* file tree */ + +static aep_statistic_t aepst = {}; + +/* configuration from getenv() */ +static char *cache_path; +static uint64_t max_cached_file_size; +static uint64_t max_cached_size; +static uint32_t aep_debug = 0; +static char *ae_mountpoint = NULL; /* The path in which afc-engine seeking for static data */ +static size_t strlen_ae_mountpoint; +static char *real_mountpoint = NULL; /* The path in which static data really is */ +static bool aep_use_gs = false; +static int logfile = -1; +static volatile int64_t *cache_size; +static volatile int8_t *open_files; +static sem_t *shmem_sem; +static int64_t claimed_size; + +static data_fd_t *fd_get_data_fd(const int fd); +static char *fd_get_dbg_name(const int fd); +static void fd_set_dbg_name(const int fd, const char *tpath); +static void fd_rm(const int fd, bool closeit = false); + +static int download_file_nfs(data_fd_t *data_fd, char *dest); +static ssize_t read_remote_data_nfs(void *destv, size_t size, char *tpath, off_t off); +static int download_file_gs(data_fd_t *data_fd, char *dest); +static ssize_t read_remote_data_gs(void *destv, size_t size, char *tpath, off_t off); +static int init_gs(); +static void reduce_cache(uint64_t size); +static bool fd_is_remote(int fd); + +/* free string allocated by realpath() in is_remote_file() */ +static inline void free_tpath(char *tpath) +{ + if (tpath) { + free(tpath - strlen_ae_mountpoint); + } +} + +static inline void prn_statistic() +{ + if (!(aep_debug & (DBG_LOG | DBG_DBG))) { + return; + } + dprintf(logfile, + "statistics: remoteIO %u/%u/%u cachedIO %u/%u/%u dl %u/%u/%u cs %lu\n", + aepst.read_remote, + aepst.read_remote_size, + aepst.read_remote_time, + aepst.read_cached, + aepst.read_cached_size, + aepst.read_cached_time, + aepst.read_write, + aepst.read_write_size, + aepst.read_write_time, + *cache_size); +} + +static void starttime(struct timeval *tv) +{ + gettimeofday(tv, NULL); +} + +static unsigned int stoptime(struct timeval *tv) +{ + struct timeval tv1; + int us; + + gettimeofday(&tv1, NULL); + us = (tv1.tv_sec - tv->tv_sec) * 1000000 + tv1.tv_usec - tv->tv_usec; + return (unsigned int)us; +} + +static sem_t *semopen(const char *fname) +{ + int i; + sem_t *sem; + char *tmp = strdup(fname); + + for (i = 1; tmp[i]; i++) { + if (tmp[i] == '/') { + tmp[i] = '_'; + } + } + sem = sem_open(tmp, O_CREAT, 0666, 1); + free(tmp); + aep_assert(sem, "sem_open"); + return sem; +} + +static inline FILE *orig_fopen(const char *path, const char *mode) +{ + typedef FILE *(*orig_fopen_t)(const char *, const char *); + orig_fopen_t orig = (orig_fopen_t)dlsym(RTLD_NEXT, "fopen"); + FILE *ret; + + ret = (*orig)(path, mode); + if (ret) { + fd_set_dbg_name(fileno(ret), path); + } + return ret; +} + +static inline size_t orig_fread(void *destv, size_t size, size_t nmemb, FILE *f) +{ + typedef size_t (*orig_fread_t)(void *, size_t, size_t, FILE *); + orig_fread_t orig = (orig_fread_t)dlsym(RTLD_NEXT, "fread"); + size_t ret; + + ret = (*orig)(destv, size, nmemb, f); + return ret; +} + +static inline int orig_fclose(FILE *f) +{ + typedef int (*orig_fclose_t)(FILE *); + orig_fclose_t orig = (orig_fclose_t)dlsym(RTLD_NEXT, "fclose"); + + return (*orig)(f); +} + +static inline int orig_open(const char *pathname, int flags, ...) +{ + typedef int (*orig_open_t)(const char *, int, ...); + int fd; + orig_open_t orig = (orig_open_t)dlsym(RTLD_NEXT, "open"); + + fd = (*orig)(pathname, flags, 0666); + fd_set_dbg_name(fd, pathname); + return fd; +} + +static inline int orig_openat(int dirfd, const char *pathname, int flags, ...) +{ + typedef int (*orig_openat_t)(int, const char *, int); + orig_openat_t orig = (orig_openat_t)dlsym(RTLD_NEXT, "openat"); + int fd; + + fd = (*orig)(dirfd, pathname, flags); + fd_set_dbg_name(fd, pathname); + return fd; +} + +static inline int orig_close(int fd) +{ + typedef int (*orig_close_t)(int); + orig_close_t orig = (orig_close_t)dlsym(RTLD_NEXT, "close"); + + return (*orig)(fd); +} + +static inline int orig_stat(const char *pathname, struct stat *statbuf) +{ + typedef int (*orig_stat_t)(const char *, struct stat *); + orig_stat_t orig = (orig_stat_t)dlsym(RTLD_NEXT, "stat"); + + return (*orig)(pathname, statbuf); +} + +static inline ssize_t orig_read(int fd, void *buf, size_t count) +{ + typedef ssize_t (*orig_read_t)(int, void *, size_t); + ssize_t ret; + orig_read_t orig = (orig_read_t)dlsym(RTLD_NEXT, "read"); + + ret = (*orig)(fd, buf, count); + // dbgo("orig_read(%d, %zu) %zd", fd, count, ret); + return ret; +} + +static inline off_t orig_lseek(int fd, off_t offset, int whence) +{ + typedef off_t (*orig_lseek_t)(int, off_t, int); + orig_lseek_t orig = (orig_lseek_t)dlsym(RTLD_NEXT, "lseek"); + + return (*orig)(fd, offset, whence); +} + +static inline struct dirent *orig_readdir(DIR *dir) +{ + typedef struct dirent *(*orig_readdir_t)(DIR * dir); + orig_readdir_t orig = (orig_readdir_t)dlsym(RTLD_NEXT, "readdir"); + + return (*orig)(dir); +} + +static inline int orig_fstat(int fd, struct stat *statbuf) +{ + typedef int (*orig_fstat_t)(int, struct stat *); + orig_fstat_t orig = (orig_fstat_t)dlsym(RTLD_NEXT, "fstat"); + + return (*orig)(fd, statbuf); +} + +static inline int orig_lstat(const char *pathname, struct stat *statbuf) +{ + typedef int (*orig_lstat_t)(const char *, struct stat *); + orig_lstat_t orig = (orig_lstat_t)dlsym(RTLD_NEXT, "lstat"); + + return (*orig)(pathname, statbuf); +} + +static inline int orig_access(const char *pathname, int mode) +{ + typedef int (*orig_access_t)(const char *, int); + orig_access_t orig = (orig_access_t)dlsym(RTLD_NEXT, "access"); + int ret; + + ret = (*orig)(pathname, mode); + return ret; +} + +static inline void orig_rewind(FILE *stream) +{ + typedef int (*orig_rewind_t)(FILE *); + orig_rewind_t orig = (orig_rewind_t)dlsym(RTLD_NEXT, "rewind"); + + (*orig)(stream); +} + +static inline DIR *orig_opendir(const char *name) +{ + typedef DIR *(*orig_opendir_t)(const char *name); + orig_opendir_t orig = (orig_opendir_t)dlsym(RTLD_NEXT, "opendir"); + DIR *ret; + + ret = (*orig)(name); + if (ret) { + fd_set_dbg_name(dirfd(ret), name); + } + return ret; +} + +static inline int orig_closedir(DIR *dirp) +{ + typedef int (*orig_closedir_t)(DIR *); + orig_closedir_t orig = (orig_closedir_t)dlsym(RTLD_NEXT, "closedir"); + + return (*orig)(dirp); +} + +static inline DIR *orig_fdopendir(int fd) +{ + typedef DIR *(*orig_fdopendir_t)(int); + orig_fdopendir_t orig = (orig_fdopendir_t)dlsym(RTLD_NEXT, "fdopendir"); + + return (*orig)(fd); +} + +static inline int orig_fgetc(FILE *stream) +{ + typedef int (*orig_fgetc_t)(FILE *); + orig_fgetc_t orig = (orig_fgetc_t)dlsym(RTLD_NEXT, "fgetc"); + + return (*orig)(stream); +} + +/* find file in file tree by name. Return file entry pointer or NULL */ +static fe_t *find_fe(char *tpath) +{ + char *c = tpath + 1; /* skip first / */ + fe_t *cfe = &tree; + + while (c < tpath + strlen(tpath)) { + char name[AEP_FILENAME_MAX] = {}; + int name_off = 0; + + while (*c && *c != '/') { + name[name_off] = *c; + name_off++; + c++; + } + c++; /* skip final / */ + name[name_off] = 0; + cfe = cfe->down; + while (cfe) { + if (strcmp(cfe->name, name)) { + cfe = cfe->next; + if (!cfe) { + return NULL; + } + } else { + break; + } + } + } + return cfe; +} + +/* FILE function pointers stubs */ +#ifndef __GLIBC__ +/* never used */ +static size_t f_read(FILE *f, unsigned char *buf, size_t size) +{ + dbgo("FILE->read(%d(%s), %zu)", fileno(f), fd_get_dbg_name(fileno(f)), size); + return 0; +} +/* never used */ +static size_t f_write(FILE *f, const unsigned char *buff, size_t size) +{ + dbgo("FILE->write(%d(%s), %zu)", fileno(f), fd_get_dbg_name(fileno(f)), size); + f->wpos = 0; + return 0; +} + +static off_t f_seek(FILE *f, off_t off, int whence) +{ + data_fd_t *data_fd = fd_get_data_fd(fileno(f)); + dbgd("FILE->seek(%d(%s), %jd, %d)", fileno(f), fd_get_dbg_name(fileno(f)), off, whence); + switch (whence) { + case SEEK_SET: /* 0 */ + data_fd->off = off; + break; + case SEEK_CUR: /* 0 */ + data_fd->off += off; + break; + case SEEK_END: + data_fd->off = data_fd->fe->size + off; + break; + } + dbgd("FILE->seek(%d(%s), %jd, %d) %jd", + fileno(f), + fd_get_dbg_name(fileno(f)), + off, + whence, + data_fd->off); + return data_fd->off; +} + +/* never used */ +static int f_close(FILE *f) +{ + dbgd("FILE->close(%d(%s))", fileno(f), fd_get_dbg_name(fileno(f))); + fd_rm(fileno(f)); + return 0; +} +#endif + +static inline void cache_size_set(int64_t size) +{ + sem_wait(shmem_sem); + *cache_size += size; + sem_post(shmem_sem); +} + +static inline int64_t cache_size_get() +{ + int64_t tmp; + + sem_wait(shmem_sem); + tmp = *cache_size; + sem_post(shmem_sem); + return tmp; +} + +/* 16 bits hash */ +static uint16_t hash_fname(const char *str) +{ + uint8_t cor = 0; /* to do USGS_1_n32w099 and USGS_1_n33w098 differ */ + + uint16_t hash = 0x5555; + str++; /* skip '/' */ + while (*str) { + hash ^= *((uint16_t *)str) + cor; + str += 2; + cor++; + } + return hash; +} + +static uint8_t files_open_set(const char *name, int val) +{ + aep_assert(strcmp(name, "noname"), "files_open_set(noname)"); + uint16_t fno = hash_fname(name); + uint8_t ret; + + sem_wait(shmem_sem); + open_files[fno] += val; + if (open_files[fno] < 0) { + open_files[fno] = 0; + } + ret = open_files[fno]; + sem_post(shmem_sem); + return ret; +} + +static uint8_t files_open_get(const char *name) +{ + aep_assert(strcmp(name, "noname"), "files_open_get(noname)"); + uint16_t fno = hash_fname(name); + uint8_t ret; + + sem_wait(shmem_sem); + ret = open_files[fno]; + sem_post(shmem_sem); + return ret; +} + +static size_t read_data(void *destv, size_t size, data_fd_t *data_fd) +{ + dbg("read_data(%s)", data_fd->tpath); + char fakepath[AEP_PATH_MAX]; + ssize_t ret; + ssize_t (*read_remote_data)(void *destv, size_t size, char *tpath, off_t off) = + aep_use_gs ? read_remote_data_gs : read_remote_data_nfs; + /* define pointer to download file func */ + int (*download_file)(data_fd_t * data_fd, char *dest); + download_file = aep_use_gs ? download_file_gs : download_file_nfs; + struct stat stat; + sem_t *sem; + bool is_cached = false; + + strncpy(fakepath, cache_path, AEP_PATH_MAX); + strncat(fakepath, data_fd->tpath, AEP_PATH_MAX - strlen(fakepath)); + + sem = semopen(data_fd->tpath); + // dbg("read_data %s", data_fd->tpath); + sem_wait(sem); + + /* download whole file to cache if possible */ + if (!orig_stat(fakepath, &stat)) { + if (data_fd->fe->size == stat.st_size) { + is_cached = true; + } + if (!is_cached && data_fd->fe->size <= (int64_t)max_cached_file_size) { + if (data_fd->fe->size + cache_size_get() > (int64_t)max_cached_size) { + reduce_cache(data_fd->fe->size); + } + if (data_fd->fe->size + cache_size_get() < (int64_t)max_cached_size) { + // dbg("download %s", data_fd->tpath); + if (!download_file(data_fd, fakepath)) { + cache_size_set(data_fd->fe->size); + dbg("download %s done, cs %ld", + data_fd->tpath, + *cache_size); + is_cached = true; + } else { + dbg("download %s failed, cs %ld", + data_fd->tpath, + *cache_size); + } + } else { + dbgl("Can't cache %s %lu cs %ld", + data_fd->tpath, + data_fd->fe->size, + *cache_size); + dbg("Can't cache %s %lu cs %ld", + data_fd->tpath, + data_fd->fe->size, + *cache_size); + } + } + } + + if (is_cached) { + int fd; + struct timeval tv; + unsigned int us; + + starttime(&tv); + fd = orig_open(fakepath, O_RDONLY); + aep_assert(fd >= 0, "read_data(%s) open", fakepath); + orig_lseek(fd, data_fd->off, SEEK_SET); + ret = orig_read(fd, destv, size); + aep_assert(ret >= 0, "read_data(%s) read", fakepath); + orig_close(fd); + us = stoptime(&tv); + sem_post(sem); + sem_close(sem); + dbgl("read cached file %s size %zu time %u us cache size %zu", + data_fd->tpath, + ret, + us, + *cache_size); + aepst.read_cached++; + aepst.read_cached_size += ret; + aepst.read_cached_time += us; + } else { + sem_post(sem); + sem_close(sem); + ret = read_remote_data(destv, size, data_fd->tpath, data_fd->off); + aep_assert(ret >= 0, "read_data(%s) read_remote_data", fakepath) + } + // dbg("read_data %s done", data_fd->tpath); + data_fd->off += ret; + dbgd("read_data(%s, %zu) %zd", data_fd->tpath, size, ret); + return ret; +} + +/* create fake file descriptor, FILE* and DIR*. Returns like open() */ +static int fd_add(char *tpath) +{ + int fd; + fe_t *fe; + char fakepath[AEP_PATH_MAX]; + char *p = fakepath; + struct stat statbuf; + data_fd_t *data_fd; + + fe = find_fe(tpath); + if (!fe) { + return -1; + } + dbg("fd_add(%s) size 0x%jx", tpath, fe->size); + + strncpy(fakepath, cache_path, AEP_PATH_MAX); + strncat(fakepath, tpath, AEP_PATH_MAX - strlen(fakepath)); + + /* create cache file */ + if (orig_stat(fakepath, &statbuf)) { + for (p = fakepath; *p; p++) { + if (*p == '/') { + *p = '\0'; + mkdir(fakepath, 0777); + *p = '/'; + } + } + if (fe->size) { /* it's a file, touch it */ + int fd; + + fd = orig_open(fakepath, O_CREAT | O_RDWR); + aep_assert(fd >= 0, "fd_add(%s) touch errno %d", fakepath, errno); + orig_close(fd); + } else { /* is dir */ + mkdir(fakepath, 0777); + } + } + + if (fe->size) { + files_open_set(tpath, 1); + } + fd = orig_open(fakepath, O_RDONLY); + data_fd = &data_fds[fd]; + memset(data_fd, 0, sizeof(data_fd_t)); + aep_assert(!orig_fstat(fd, &statbuf), "fd_add(%s) fstat", tpath); + data_fd->fe = fe; + data_fd->off = 0; +#ifndef __GLIBC__ + data_fd->file.fd = fd; + data_fd->file.read = f_read; + data_fd->file.write = f_write; + data_fd->file.seek = f_seek; + data_fd->file.close = f_close; +#endif + data_fd->readdir_p = NULL; + data_fd->dir.fd = fd; + free_tpath(data_fd->tpath); /* new std::map is zeroed */ + data_fd->tpath = tpath; + dbg("fd_add(%s) %d done", tpath, fd); + return fd; +} + +static bool fd_is_remote(int fd) +{ + data_fd_t *data_fd = fd_get_data_fd(fd); + return data_fd && data_fd->fe; +} + +static void fd_rm(const int fd, bool closeit) +{ + data_fd_t *data_fd = fd_get_data_fd(fd); + + dbg("fd_rm(%d)", fd); + if (!data_fd) { + return; + } + if (data_fd->fe) { + if (data_fd->fe->size) { + files_open_set(data_fd->tpath, -1); + } + if (closeit) { + orig_close(fd); + } + } + free_tpath(data_fd->tpath); + data_fds.erase(fd); + dbg("fd_rm(%d) done", fd); +} + +static inline data_fd_t *fd_get_data_fd(const int fd) +{ + auto search = data_fds.find(fd); + + if (search == data_fds.end()) { + return NULL; + } + return &search->second; +} + +static inline char *fd_get_dbg_name(const int fd) +{ + data_fd_t *data_fd = fd_get_data_fd(fd); + if (data_fd) { + return data_fd->tpath; + } + return (char *)"noname"; +} + +static inline void fd_set_dbg_name(const int fd, const char *tpath) +{ + data_fd_t *data_fd = fd_get_data_fd(fd); + + if ((aep_debug & DBG_ANY) && data_fd) { + strncpy(data_fd->tpath, tpath, AEP_PATH_MAX); + } +} + +static bool is_remote_file(const char *path, char **tpath) +{ + char *rpath = NULL; + + if (!path) { + *tpath = (char *)path; + return false; + } + + rpath = realpath(path, rpath); + if (!rpath) { + *tpath = (char *)path; + return false; + } + + if (strncmp(rpath, ae_mountpoint, strlen_ae_mountpoint) == 0) { + if (strlen(rpath) == strlen_ae_mountpoint || rpath[strlen_ae_mountpoint] == '/') { + *tpath = (char *)rpath + strlen_ae_mountpoint; + dbgd("is_remote_file(%s -> %s)", path, *tpath); + return true; + } else { + free(rpath); + *tpath = (char *)path; + dbgo("is_remote_file(%s)", *tpath); + return false; + } + } + free(rpath); + *tpath = (char *)path; + return false; +} + +static int ftw_reduce_callback(const char *fpath, const struct stat *sb, int typeflag) +{ + if (typeflag == FTW_F && sb->st_size) { + char *tpath = (char *)fpath + strlen(cache_path); + if (!files_open_get(tpath)) { + sem_t *sem; + sem = semopen(tpath); + sem_wait(sem); + aep_assert(!truncate(fpath, 0), "truncate"); + sem_post(sem); + cache_size_set(-sb->st_size); + if (cache_size_get() + claimed_size <= (int64_t)max_cached_size) { + return -1; + } + dbg("truncate(%s) cs %ld", tpath, *cache_size); + } + } + return 0; +} + +static void reduce_cache(uint64_t size) +{ + // dbg("reduce_cache(%lu)", size); + claimed_size = (int64_t)size; + ftw(cache_path, ftw_reduce_callback, 100); + // dbg("reduce_cache(%lu) done", size); +} + +static int ftw_callback(const char *fpath, const struct stat *sb, int typeflag) +{ + *cache_size += sb->st_size; + return 0; +} + +/* This library entrypoint */ +void __attribute__((constructor)) aep_init(void) +{ + int ret; + struct stat statbuf; + int fd; + uint8_t *fl; + fe_t *free_fes; /* next empty file entry from fes array */ + fe_t **stack, *cstack; + fe_t *cfe = NULL; /* last file entry added to tree */ + uint8_t *filelist_end; + uint8_t tab_prev = 0; + uint32_t entries_size; + char *filelist_path; + char *tmp; + int shm_fd; + + /* check env vars */ + tmp = getenv("AFC_AEP_DEBUG"); + aep_debug = tmp ? atoi(tmp) : 0; + if (aep_debug) { + char *logname; + + logname = getenv("AFC_AEP_LOGFILE"); + if (!logname) { + dbge("AFC_AEP_LOGFILE env var is not defined, log disabled"); + aep_debug = 0; + } else { + logfile = orig_open(logname, O_CREAT | O_RDWR | O_APPEND); + if (logfile < 0) { + dbge("Can not open %s, log disabled", logname); + aep_debug = 0; + } + } + } + tmp = getenv("AFC_AEP_REAL_MOUNTPOINT"); + aep_assert(tmp, "AFC_AEP_REAL_MOUNTPOINT env var is not defined"); + real_mountpoint = realpath(tmp, real_mountpoint); + aep_assert(real_mountpoint, "AFC_AEP_REAL_MOUNTPOINT env var path does not exist"); + + tmp = getenv("AFC_AEP_ENGINE_MOUNTPOINT"); + aep_assert(tmp, "AFC_AEP_ENGINE_MOUNTPOINT env var is not defined"); + ae_mountpoint = realpath(tmp, ae_mountpoint); + aep_assert(ae_mountpoint, "AFC_AEP_ENGINE_MOUNTPOINT env var path does not exist"); + strlen_ae_mountpoint = strlen(ae_mountpoint); + + if (getenv("AFC_AEP_GS")) { + aep_use_gs = true; + init_gs(); + } + filelist_path = getenv("AFC_AEP_FILELIST"); + aep_assert(filelist_path, "AFC_AEP_FILELIST env var is not defined"); + tmp = getenv("AFC_AEP_CACHE_MAX_FILE_SIZE"); + aep_assert(tmp, "AFC_AEP_CACHE_MAX_FILE_SIZE env var is not defined"); + max_cached_file_size = atoll(tmp); + tmp = getenv("AFC_AEP_CACHE_MAX_SIZE"); + aep_assert(tmp, "AFC_AEP_CACHE_MAX_SIZE env var is not defined"); + max_cached_size = atoll(tmp); + if (max_cached_file_size > max_cached_size) { + max_cached_file_size = max_cached_size; + } + cache_path = getenv("AFC_AEP_CACHE"); + aep_assert(cache_path, "AFC_AEP_CACHE env var is not defined"); + + /* read filelist */ + ret = orig_stat(filelist_path, &statbuf); + if (ret) { + dbge("Filelist is not found"); + exit(ret); + } + filelist = (uint8_t *)malloc(statbuf.st_size); + if (!filelist) { + dbge("Memory allocation"); + exit(-ENOMEM); + } + fd = orig_open(filelist_path, O_RDONLY); + if (fd < 0) { + dbge("File IO"); + exit(-EIO); + } + if (orig_read(fd, filelist, statbuf.st_size) != statbuf.st_size) { + dbge("File IO"); + exit(-EIO); + } + orig_close(fd); + + fl = filelist; + filelist_end = filelist + statbuf.st_size; + + /* alloc file tree entries */ + entries_size = *((uint32_t *)fl); + fl += sizeof(uint32_t); + entries_size += *((uint32_t *)fl); + fl += sizeof(uint32_t); + entries_size *= sizeof(fe_t); + fes = (fe_t *)calloc(1, entries_size); + if (!fes) { + dbge("Memory allocation"); + exit(-ENOMEM); + } + free_fes = fes; + + stack = (fe_t **)calloc(1, (*((uint8_t *)fl) + 1) * sizeof(fe_t *)); + if (!(stack)) { + dbge("Memory allocation"); + exit(-ENOMEM); + } + fl += sizeof(uint8_t); + stack[0] = &tree; + tree.name = (char *)"root"; /* debug */ + cstack = stack[0]; + + /* fill file tree */ + while (fl < filelist_end) { + uint8_t tab = 0; + char *name; + int64_t size; + + /* read next line */ + while (*fl == '\t') { + tab++; + (fl)++; + } + name = (char *)(fl); + fl += strlen(name) + 1; + + size = *((int64_t *)fl); + fl += sizeof(int64_t); + if (tab != tab_prev) { + if (tab < tab_prev) { + cstack = stack[tab]; + cfe = cstack->down; + while (cfe && cfe->next) { + cfe = cfe->next; + } + } else { + stack[tab] = cfe; + cstack = stack[tab]; + } + tab_prev = tab; + } + if (!cstack->down) { + cstack->down = free_fes; + } else { + cfe->next = free_fes; + } + cfe = free_fes; + free_fes++; + cfe->name = name; + cfe->size = size; + } + free(stack); + + /* share cache size */ + shmem_sem = sem_open("aep_shmem_sem", O_CREAT, 0666, 1); + aep_assert(shmem_sem, "aep_init:sem_open"); + shm_fd = shm_open("aep_shmem", O_RDWR | O_CREAT | O_EXCL, 0666); + // dbg("aep_init"); + sem_wait(shmem_sem); + if (shm_fd < 0) { + // dbg("aep_init cache skip"); + /* O_CREAT | O_EXCL failed, so shared memory object already was initialized */ + shm_fd = shm_open("aep_shmem", O_RDWR, 0666); + aep_assert(shm_fd >= 0, "shm_open"); + cache_size = (int64_t *)mmap(NULL, + sizeof(int64_t) + HASH_SIZE, + PROT_READ | PROT_WRITE, + MAP_SHARED, + shm_fd, + 0); + aep_assert(cache_size, "mmap"); + open_files = (int8_t *)(cache_size + 1); + } else { + // dbg("aep_init recount cache"); + aep_assert(!ftruncate(shm_fd, sizeof(uint64_t) + HASH_SIZE), "aep_init:ftruncate"); + cache_size = (int64_t *)mmap(NULL, + sizeof(int64_t) + HASH_SIZE, + PROT_READ | PROT_WRITE, + MAP_SHARED, + shm_fd, + 0); + aep_assert(cache_size, "mmap"); + open_files = (int8_t *)(cache_size + 1); + memset((void *)cache_size, 0, sizeof(int64_t) + HASH_SIZE); + /* count existing cache size */ + ftw(cache_path, ftw_callback, 100); + } + sem_post(shmem_sem); + + dbg("aep_init done cs %lu", *cache_size); +} + +FILE *fopen(const char *path, const char *mode) +{ + FILE *ret; + char *tpath; + + if (is_remote_file(path, &tpath)) { + int fd; + + fd = fd_add(tpath); + if (fd < 0) { + return NULL; + } + ret = &(fd_get_data_fd(fd)->file); + dbgd("fopen(%s, %s) %d", tpath, mode, fd); + } else { + ret = orig_fopen(path, mode); + if (ret) { + dbgo("fopen(%s, %s) %d", path, mode, fileno(ret)); + fd_rm(fileno(ret)); + } else { + dbgo("fopen(%s, %s) -1", path, mode); + } + } + return ret; +} + +size_t fread(void *destv, size_t size, size_t nmemb, FILE *f) +{ + size_t ret; + + if (fd_is_remote(fileno(f))) { + data_fd_t *data_fd = fd_get_data_fd(fileno(f)); + + ret = read_data(destv, size * nmemb, data_fd); + ret /= size; + // dbgd("fread(%d(%s), %zu * %zu) %zu", fileno(f), fd_get_dbg_name(fileno(f)), size, + // nmemb, ret); + } else { + ret = orig_fread(destv, size, nmemb, f); + // dbgo("fread(%d, %zu * %zu) %zu", fileno(f), size, nmemb, ret); + } + return ret; +} + +int fclose(FILE *f) +{ + int ret; + + dbg("fclose(%p)", f); + + if (fd_is_remote(fileno(f))) { + dbgd("fclose(%d(%s))", fileno(f), fd_get_dbg_name(fileno(f))); + fd_rm(fileno(f), true); + ret = 0; + prn_statistic(); + } else { + dbgo("fclose(%d)", fileno(f)); + ret = orig_fclose(f); + } + + dbg("fclose(%p) %d done", f, ret); + return ret; +} + +int open(const char *pathname, int flags, ...) +{ + int ret; + char *tpath; + + if (is_remote_file(pathname, &tpath)) { + ret = fd_add(tpath); + dbgd("open(%s, %x) %d", tpath, flags, ret); + } else { + ret = orig_open(pathname, flags); + dbgo("open(%s, %x) %d", tpath, flags, ret); + } + return ret; +} + +int openat(int dirfd, const char *pathname, int flags, ...) +{ + int ret; + char *tpath; + + if (is_remote_file(pathname, &tpath)) { + ret = fd_add(tpath); + dbgd("openat(%s, %x) %d", tpath, flags, ret); + } else { + ret = orig_openat(dirfd, pathname, flags); + dbgo("openat(%d, %s, %x) %d", dirfd, tpath, flags, ret); + } + return ret; +} + +int close(int fd) +{ + int ret = 0; + + if (fd_is_remote(fd)) { + dbgd("close(%d(%s))", fd, fd_get_dbg_name(fd)); + fd_rm(fd, true); + } else { + ret = orig_close(fd); + dbgo("close(%d(%s))=%d", fd, fd_get_dbg_name(fd), ret); + } + return ret; +} + +int stat(const char *pathname, struct stat *statbuf) +{ + char *tpath; + int ret; + + if (is_remote_file(pathname, &tpath)) { + int fd; + data_fd_t *data_fd; + + dbgd("stat(%s)", tpath); + + fd = fd_add(tpath); + if (fd < 0) { + return -1; + } + data_fd = fd_get_data_fd(fd); + + memcpy(statbuf, &def_stat, sizeof(struct stat)); + if (data_fd->fe->size) { + statbuf->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + } else { + statbuf->st_mode = S_IFDIR | S_IRWXU | S_IRWXG | S_IRWXO; + } + statbuf->st_size = data_fd->fe->size; + statbuf->st_blocks = (statbuf->st_size >> 9) + (statbuf->st_size & 0x1ff) ? 1 : 0; + dbgd("stat(%s, 0x%lx)", tpath, data_fd->fe->size); + fd_rm(fd, true); + ret = 0; + } else { + ret = orig_stat(pathname, statbuf); + dbgo("stat(%s) %d", tpath, ret); + } + return ret; +} + +int fstat(int fd, struct stat *statbuf) +{ + int ret; + + if (fd_is_remote(fd)) { + data_fd_t *data_fd; + data_fd = fd_get_data_fd(fd); + + dbgd("fstat(%d(%s))", fd, fd_get_dbg_name(fd)); + + memcpy(statbuf, &def_stat, sizeof(struct stat)); + if (data_fd->fe->size) { + statbuf->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + } else { + statbuf->st_mode = S_IFDIR | S_IRWXU | S_IRWXG | S_IRWXO; + } + statbuf->st_size = data_fd->fe->size; + statbuf->st_blocks = (statbuf->st_size >> 9) + (statbuf->st_size & 0x1ff) ? 1 : 0; + ret = 0; + dbgd("fstat(%s, 0x%lx, %s) %d", + fd_get_dbg_name(fd), + data_fd->fe->size, + data_fd->fe->size ? "file" : "dir", + ret); + } else { + ret = orig_fstat(fd, statbuf); + dbgo("fstat(%d) %d", fd, ret); + } + return ret; +} + +int lstat(const char *pathname, struct stat *statbuf) +{ + int ret; + char *tpath; + + if (is_remote_file(pathname, &tpath) && 0) { + int fd; + data_fd_t *data_fd; + + fd = fd_add(tpath); + if (fd < 0) { + return -1; + } + data_fd = fd_get_data_fd(fd); + + memcpy(statbuf, &def_stat, sizeof(struct stat)); + if (data_fd->fe->size) { + statbuf->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + } else { + statbuf->st_mode = S_IFDIR | S_IRWXU | S_IRWXG | S_IRWXO; + } + statbuf->st_size = data_fd->fe->size; + statbuf->st_blocks = (statbuf->st_size >> 9) + (statbuf->st_size & 0x1ff) ? 1 : 0; + ret = 0; + dbgd("lstat(%s, 0x%lx, %s) %d", + tpath, + data_fd->fe->size, + data_fd->fe->size ? "file" : "dir", + ret); + fd_rm(fd, true); + } else { + ret = orig_lstat(pathname, statbuf); + } + return ret; +} + +int access(const char *pathname, int mode) +{ + int ret; + char *tpath; + + if (is_remote_file(pathname, &tpath)) { + fe_t *fe; + + fe = find_fe(tpath); + ret = fe ? 0 : -1; + dbgd("access(%s, %d) %d", tpath, mode, ret); + free_tpath(tpath); + } else { + ret = orig_access(pathname, mode); + dbgo("access(%s, %d) %d", pathname, mode, ret); + } + return ret; +} + +/* the statx wrapper for musl */ +long int syscall(long int __sysno, ...) +{ + typedef int (*orig_syscall_t)(long int, ...); + void *ret; + + if (__sysno == SYS_statx) { + /* __syscall(SYS_statx, fd, path, flag, 0x7ff, &stx); */ + va_list args; + char *path; + int dirfd; + int flags; + unsigned int mask; + struct statx *st; + char *tpath; + + va_start(args, __sysno); + dirfd = va_arg(args, int); + (void)dirfd; + path = va_arg(args, char *); + flags = va_arg(args, int); + (void)flags; + mask = va_arg(args, unsigned int); + (void)mask; + st = va_arg(args, struct statx *); + va_end(args); + if (is_remote_file(path, &tpath)) { + fe_t *fe; + + fe = find_fe(tpath); + if (!fe) { + dbgd("SYS_statx(%s) -1", tpath); + free_tpath(tpath); + return -1; + } + + dbgd("syscall(SYS_statx, dirfd:%d, path:%s, flags:0x%x, mask:0x%x) 0x%lx", + dirfd, + path, + flags, + mask, + fe->size); + memcpy(st, &def_statx, sizeof(struct statx)); + if (fe->size) { + st->stx_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + st->stx_size = fe->size; + st->stx_blocks = (fe->size >> 9) + (fe->size & 0x1ff) ? 1 : 0; + } else { + st->stx_mode = S_IFDIR | S_IRWXU | S_IRWXG | S_IRWXO; + } + free_tpath(tpath); + return 0; + } else { + orig_syscall_t orig = (orig_syscall_t)dlsym(RTLD_NEXT, "syscall"); + void *argsb = __builtin_apply_args(); + + dbgo("SYS_statx(%d, %s)", dirfd, path); +#if __cplusplus + ret = __builtin_apply((void (*)(...))(orig), argsb, 512); +#else + ret = __builtin_apply((void (*)())(orig), argsb, 512); +#endif + __builtin_return(ret); + } + } else { + orig_syscall_t orig = (orig_syscall_t)dlsym(RTLD_NEXT, "syscall"); + void *argsb = __builtin_apply_args(); + + dbgo("syscall(unsupported %ld)", __sysno); +#if __cplusplus + ret = __builtin_apply((void (*)(...))(orig), argsb, 512); +#else + ret = __builtin_apply((void (*)())(orig), argsb, 512); +#endif + __builtin_return(ret); + } +} + +int fcntl(int fd, int cmd, ...) +{ + if (fd_is_remote(fd)) { + aep_assert(cmd == F_SETLK, "fcntl(unsupported cmd=%d)", cmd); + dbgd("fcntl(%s, %d)", fd_get_dbg_name(fd), cmd); + return 0; + } else { + void *args = __builtin_apply_args(); + void *ret; + orig_fcntl_t orig = (orig_fcntl_t)dlsym(RTLD_NEXT, "fcntl"); + + dbgo("fcntl(%d, %d)", fd, cmd); +#if __cplusplus + ret = __builtin_apply((void (*)(...))(*orig), args, 512); +#else + ret = __builtin_apply((void (*)())(*orig), args, 512); +#endif + __builtin_return(ret); + } +} + +ssize_t read(int fd, void *buf, size_t count) +{ + ssize_t ret; + + if (fd_is_remote(fd)) { + data_fd_t *data_fd = fd_get_data_fd(fd); + + ret = read_data(buf, count, data_fd); + // dbgd("read(%d(%s), %zu) %zd", fd, fd_get_dbg_name(fd), count, ret); + } else { + ret = orig_read(fd, buf, count); + // dbgo("read(%d(%s), %zu) %zd", fd, fd_get_dbg_name(fd), count, ret); + } + return ret; +} + +off_t lseek(int fd, off_t offset, int whence) +{ + off_t ret; + + if (fd_is_remote(fd)) { + data_fd_t *data_fd = fd_get_data_fd(fd); + + aep_assert(whence == SEEK_SET, + "lseek(%s, %ld, %d) unsupported whence", + fd_get_dbg_name(fd), + offset, + whence); + data_fd->off = offset; + ret = 0; + dbgd("lseek(%d(%s), %ld, %d) %ld", fd, fd_get_dbg_name(fd), offset, whence, ret); + } else { + ret = orig_lseek(fd, offset, whence); + dbgo("lseek(%d(%s), %ld, %d) %ld", fd, fd_get_dbg_name(fd), offset, whence, ret); + } + return ret; +} + +struct dirent *readdir(DIR *dir) +{ + struct dirent *ret; + + if (fd_is_remote(dirfd(dir))) { + data_fd_t *data_fd = fd_get_data_fd(dirfd(dir)); + + if (!data_fd->readdir_p) { + data_fd->readdir_p = data_fd->fe->down; + } else { + data_fd->readdir_p = data_fd->readdir_p->next; + } + if (!data_fd->readdir_p) { + // dbgd("readdir(%s) NULL", fd_get_dbg_name(dirfd(dir))); + return NULL; + } + + data_fd->dirent.d_type = data_fd->readdir_p->size ? DT_REG : DT_DIR; + strncpy(data_fd->dirent.d_name, data_fd->readdir_p->name, 256); + // dbgd("readdir(%s) %s", fd_get_dbg_name(dirfd(dir)), data_fd->dirent.d_name); + return &data_fd->dirent; + } else { + ret = orig_readdir(dir); + if (ret) { + dbgo("readdir(%d) %s", dirfd(dir), ret->d_name); + } else { + dbgo("readdir(%d) NULL", dirfd(dir)); + } + } + return ret; +} + +void rewind(FILE *stream) +{ + if (fd_is_remote(fileno(stream))) { + dbgd("rewind(%d(%s))", fileno(stream), fd_get_dbg_name(fileno(stream))); + data_fd_t *data_fd = fd_get_data_fd(fileno(stream)); + data_fd->off = 0; +#ifndef __GLIBC__ + data_fd->file.flags &= ~32; /* clear F_ERR */ + data_fd->file.wpos = data_fd->file.wbase = data_fd->file.wend = 0; + data_fd->file.rpos = data_fd->file.rend = 0; +#endif + } else { + orig_rewind(stream); + } +} + +DIR *opendir(const char *name) +{ + DIR *ret; + char *tpath; + + if (is_remote_file(name, &tpath)) { + int fd; + + fd = fd_add(tpath); + ret = &(fd_get_data_fd(fd)->dir); + dbgd("opendir(%s) %d", tpath, fd); + } else { + ret = orig_opendir(name); + dbgo("opendir(%s) %d", name, dirfd(ret)); + } + return ret; +} + +DIR *fdopendir(int fd) +{ + DIR *ret; + + if (fd_is_remote(fd)) { + ret = &(fd_get_data_fd(fd)->dir); + dbgd("fdopendir(%d(%s))", fd, fd_get_data_fd(fd)->tpath); + } else { + ret = orig_fdopendir(fd); + dbgo("fdopendir(%d)", fd); + } + return ret; +} + +int closedir(DIR *dirp) +{ + dbgd("closedir"); + if (fd_is_remote(dirfd(dirp))) { + dbgd("closedir(%d(%s))", dirfd(dirp), fd_get_dbg_name(dirfd(dirp))); + fd_rm(dirfd(dirp), true); + return 0; + } else { + dbgo("closedir(%d)", dirfd(dirp)); + return orig_closedir(dirp); + } +} + +int fgetc(FILE *stream) +{ + int ret; + + if (fd_is_remote(fileno(stream))) { + data_fd_t *data_fd = fd_get_data_fd(fileno(stream)); + char c; + + if (read_data(&c, 1, data_fd) != 1) { + ret = EOF; + } else { + ret = c; + } + dbgd("fgetc(%d(%s)) %d", fileno(stream), fd_get_dbg_name(fileno(stream)), ret); + } else { + ret = orig_fgetc(stream); + dbgo("fgetc(%d(%s)) %d", fileno(stream), fd_get_dbg_name(fileno(stream)), ret); + } + return ret; +} + +/* Google storage static data interface */ +#ifndef NO_GOOGLE_LINK + #include +namespace gcs = ::google::cloud::storage; +static gcs::Client client; +static char *bucket_name; +#endif + +static int init_gs() +{ +#ifndef NO_GOOGLE_LINK + bucket_name = getenv("AFC_AEP_GS_BUCKET_NAME"); + client = gcs::Client(); +#endif + return 0; +} + +static int download_file_gs(data_fd_t *data_fd, char *dest) +{ +#ifndef NO_GOOGLE_LINK + int output; + + if ((output = orig_open(dest, O_CREAT | O_WRONLY)) < 0) { + return -1; + } + + google::cloud::Status status = client.DownloadToFile(bucket_name, data_fd->tpath, dest); + if (!status.ok()) { + return -1; + } + + orig_close(output); +#endif + return 0; +} + +static ssize_t read_remote_data_gs(void *destv, size_t size, char *tpath, off_t off) +{ +#ifndef NO_GOOGLE_LINK + // ObjectReadStream is std::basic_istream + gcs::ObjectReadStream stream = client.ReadObject(bucket_name, + tpath, + gcs::ReadRange(off, off + size)); + stream.read((char *)destv, size); + return stream.tellg(); +#else + return 0; +#endif +} + +/* copy file from nfs mount to local */ +static int download_file_nfs(data_fd_t *data_fd, char *dest) +{ + int input, output, res; + off_t copied = 0; + char realpath[AEP_PATH_MAX]; + struct timeval tv; + unsigned int us; + + strncpy(realpath, real_mountpoint, AEP_PATH_MAX); + strncat(realpath, data_fd->tpath, AEP_PATH_MAX - strlen(realpath)); + + starttime(&tv); + if ((output = orig_open(dest, O_CREAT | O_RDWR)) < 0) { + return -1; + } + + if ((input = orig_open(realpath, O_RDONLY)) < 0) { + return -1; + } + res = sendfile(output, input, &copied, data_fd->fe->size); + + orig_close(input); + + fsync(output); + orig_close(output); + us = stoptime(&tv); + + dbgl("cache file %s size %zu time %u us", realpath, data_fd->fe->size, us); + aepst.read_write++; + aepst.read_write_size += data_fd->fe->size; + aepst.read_write_time += us; + return res == (int)data_fd->fe->size ? 0 : -1; +} + +static ssize_t read_remote_data_nfs(void *destv, size_t size, char *tpath, off_t off) +{ + int fd; + ssize_t ret; + char path[AEP_PATH_MAX]; + struct timeval tv; + unsigned int us; + + strncpy(path, real_mountpoint, AEP_PATH_MAX); + strncat(path, tpath, AEP_PATH_MAX - strlen(path)); + starttime(&tv); + fd = orig_open(path, O_RDONLY); + aep_assert(fd >= 0, "read_data_fs(%s) open", path); + orig_lseek(fd, off, SEEK_SET); + ret = orig_read(fd, destv, size); + orig_close(fd); + us = stoptime(&tv); + dbgd("read_remote_data(%s, %zu) %zu", path, size, ret); + dbgl("read remote file %s size %zu time %u us", path, size, us); + aepst.read_remote++; + aepst.read_remote_size += size; + aepst.read_remote_time += us; + return ret; +} diff --git a/src/afc-engine-preload/parse_fs.py b/src/afc-engine-preload/parse_fs.py new file mode 100755 index 0000000..b97588f --- /dev/null +++ b/src/afc-engine-preload/parse_fs.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# Tool for querying ALS database + +# Copyright (C) 2023 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +import sys +import os + +noofdirs = 0 +nooffiles = 0 +max_size = 0 +max_name_len = 0 +max_tab = 0 + + +def write_obj(name, size, tab, outfile): + # print(("\t"*tab) + "{} {}".format(name, size)) + outfile.write(("\t" * tab).encode('utf-8')) + if tab > 0: + global max_tab + if tab > max_tab: + max_tab = tab + outfile.write(name.encode('utf-8')) + outfile.write("\0".encode('utf-8')) + outfile.write(size.to_bytes(8, byteorder="little", signed=True)) + if size != 0: + global nooffiles + global max_size + nooffiles = nooffiles + 1 + if size > max_size: + max_size = size + else: + global noofdirs + noofdirs = noofdirs + 1 + global max_name_len + if len(name) > max_name_len: + max_name_len = len(name) + + +def sort_key(direntry): + return direntry.name + + +def parse_dir(path, tab, outfile): + if not os.listdir(path): + return + + if tab >= 0: + write_obj(os.path.basename(path), 0, tab, outfile) + + dirs = [] + files = [] + for dire in os.scandir(path): + if dire.is_dir(follow_symlinks=False): + dirs.append(dire) + elif dire.is_file(follow_symlinks=False) and dire.stat(follow_symlinks=False).st_size > 0: + files.append(dire) + dirs.sort(key=sort_key) + files.sort(key=sort_key) + for dire in dirs: + parse_dir(dire.path, tab + 1, outfile) + for dire in files: + write_obj(dire.name, dire.stat().st_size, tab + 1, outfile) + + +if __name__ == '__main__': + if len(sys.argv) <= 2 or not os.path.isdir(sys.argv[1]): + print("usage: {} static_data_path list_path".format(sys.argv[0])) + sys.exit() + with open(sys.argv[2], "wb") as outfile: + outfile.write(noofdirs.to_bytes(4, byteorder="little", signed=False)) + outfile.write(nooffiles.to_bytes(4, byteorder="little", signed=False)) + outfile.write(max_tab.to_bytes(1, byteorder="little", signed=False)) + parse_dir(os.path.normpath(sys.argv[1]), -1, outfile) + with open(sys.argv[2], "r+b") as outfile: + outfile.seek(0) + outfile.write(noofdirs.to_bytes(4, byteorder="little", signed=False)) + outfile.write(nooffiles.to_bytes(4, byteorder="little", signed=False)) + outfile.write(max_tab.to_bytes(1, byteorder="little", signed=False)) + + print("dirs {} files {} max tab {} max size {} max name length {}".format( + noofdirs, nooffiles, max_tab, hex(max_size), max_name_len)) diff --git a/src/afc-engine/AfcDefinitions.h b/src/afc-engine/AfcDefinitions.h new file mode 100644 index 0000000..8b2cb04 --- /dev/null +++ b/src/afc-engine/AfcDefinitions.h @@ -0,0 +1,89 @@ +#ifndef INCLUDE_AFCDEFINITIONS_H +#define INCLUDE_AFCDEFINITIONS_H + +#include +#include +#include +#include + +#include + +#include "MathHelpers.h" + +#include "terrain.h" + +namespace po = boost::program_options; + +typedef typename std::pair LatLon; // Stored as (Lat,Lon) + +typedef typename std::tuple DoubleTriplet; + +typedef typename std::pair + AngleRadius; // (Degrees CW from true noth, Radius in meters) + +const double quietNaN = std::numeric_limits::quiet_NaN(); + +const double meanEarthR_m = 6.371e6; + +// const TerrainClass* terrainObject = new TerrainClass(); + +enum RLANBoundary { + NO_BOUNDARY = 0, // empty default value + ELLIPSE, + LINEAR_POLY, + RADIAL_POLY +}; + +enum RLANType { RLAN_INDOOR, RLAN_OUTDOOR }; + +enum BuildingType { + NO_BUILDING_TYPE, + TRADITIONAL_BUILDING_TYPE, + THERMALLY_EFFICIENCT_BUILDING_TYPE +}; + +enum ChannelColor { + RED, + YELLOW, + GREEN, + + BLACK // In Denied Region +}; + +enum ChannelType { INQUIRED_FREQUENCY, INQUIRED_CHANNEL }; + +class ChannelStruct +{ + public: + ChannelType type; + std::vector freqMHzList; + std::vector> segList; + int index; + int operatingClass; + + int bandwidth(int segIdx) const + { + return freqMHzList[segIdx + 1] - freqMHzList[segIdx]; + } +}; + +class psdFreqRangeClass +{ + public: + std::vector freqMHzList; + std::vector psd_dBm_MHzList; + // psd_dBm_MHzList has size N + // freqMHzList has size N+1 + // psd_dBm_MHzList[i] is the PSD from freqMHzList[i] to freqMHzList[i+1] + // freqMHzList[0] and freqMHzList[N] are the start and stop frequencies of the + // corresponding inquiredFrequencyRange +}; + +inline std::string slurp(const std::ifstream &inStream) +{ + std::stringstream strStream; + strStream << inStream.rdbuf(); + return strStream.str(); +} + +#endif // INCLUDE_AFCDEFINITIONS_H diff --git a/src/afc-engine/AfcManager.cpp b/src/afc-engine/AfcManager.cpp new file mode 100644 index 0000000..f4a2d99 --- /dev/null +++ b/src/afc-engine/AfcManager.cpp @@ -0,0 +1,18484 @@ +// AfcManager.cpp -- Manages I/O and top-level operations for the AFC Engine +#include +#include + +#include "AfcManager.h" +#include "RlanRegion.h" +#include "lininterp.h" + +// "--runtime_opt" masks +// These bits corresponds to RNTM_OPT_... bits in src/ratapi/ratapi/defs.py +// Please keep all these definitions synchronous +#define RUNTIME_OPT_ENABLE_DBG 1 +#define RUNTIME_OPT_ENABLE_GUI 2 +#define RUNTIME_OPT_URL 4 +#define RUNTIME_OPT_ENABLE_SLOW_DBG 16 +#define RUNTIME_OPT_CERT_ID 32 + +extern double qerfi(double q); + +// QJsonArray jsonChannelData(const std::vector &channelList); +// QJsonObject jsonSpectrumData(const std::vector &channelList, const QJsonObject +// &deviceDesc, const double &startFreq); + +std::string padStringFront(const std::string &s, const char &padder, const int &amount) +{ + std::string r = s; + while ((int)(r.length()) < amount) { + r = padder + r; + } + return r; +} + +/** + * Generate a UTC ISO8601-formatted timestamp + * and return as QString + */ +QString ISO8601TimeUTC(const int &dayStep = 0) +{ + time_t rawtime; + struct tm *ptm; + + std::time(&rawtime); + rawtime += dayStep * 24 * 3600; + + ptm = gmtime(&rawtime); + + // "yyyy-mm-ddThh:mm:ssZ" + std::string result = padStringFront(std::to_string(ptm->tm_year + 1900), '0', 4) + "-" + + padStringFront(std::to_string(1 + ptm->tm_mon), '0', 2) + "-" + + padStringFront(std::to_string(ptm->tm_mday), '0', 2) + "T" + + padStringFront(std::to_string(ptm->tm_hour), '0', 2) + ":" + + padStringFront(std::to_string(ptm->tm_min), '0', 2) + ":" + + padStringFront(std::to_string(ptm->tm_sec), '0', 2) + "Z"; + + return QString::fromStdString(result); +} + +namespace +{ + +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "AfcManager") + +// const double fixedEpsDielect = 15; +// const double fixedSgmConductivity = 0.005; +// const double fixedEsNoSurfRef = 301; +// const int fixedRadioClimate = 6; +// const int fixedPolarization = 1; +// const double fixedConfidence = 0.5; +// const double fixedRelevance = 0.5; + +/** + * Encapsulates a XML writer to a file + * All fields are NULL if filename parameter in constructor is not a valid path. + */ +class ZXmlWriter +{ + public: + std::unique_ptr xml_writer; + std::unique_ptr _file; + std::unique_ptr zip_writer; + + ZXmlWriter(const std::string &filename); + ~ZXmlWriter(); +}; + +ZXmlWriter::ZXmlWriter(const std::string &filename) +{ + if (!filename.empty()) { + zip_writer.reset(new ZipWriter(QString::fromStdString(filename))); + _file = zip_writer->openFile("doc.kml"); + xml_writer.reset(new QXmlStreamWriter(_file.get())); + } +} + +ZXmlWriter::~ZXmlWriter() +{ + xml_writer.reset(); + _file.reset(); + zip_writer.reset(); +} +}; // end namespace + +namespace OpClass +{ +// Note: Per 11ax D 8.0, section 27.3..23.2 +// Channel's start frequency is calculated using formula +// Channel center frequency = Channel starting frequency + 5 × nch +// where nch = 1, 2, ...233, is the channel index +// For channel 1, center frequency is (5950 + 5 * 1) = 5955 +// and start frequency = (center frequency - BW/2) = (5955 - 20/2) = 5945 +OpClass GlobalOpClass_131 = {131, // Operating class + 20, // Bandwidth + 5950, // Start frequency. + {1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, + 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, + 97, 101, 105, 109, 113, 117, 121, 125, 129, 133, 137, 141, + 145, 149, 153, 157, 161, 165, 169, 173, 177, 181, 185, 189, + 193, 197, 201, 205, 209, 213, 217, 221, 225, 229, 233}}; + +const OpClass GlobalOpClass_132 = {132, 40, 5950, {3, 11, 19, 27, 35, 43, 51, 59, 67, 75, + 83, 91, 99, 107, 115, 123, 131, 139, 147, 155, + 163, 171, 179, 187, 195, 203, 211, 219, 227}}; + +const OpClass GlobalOpClass_133 = {133, + 80, + 5950, + {7, 23, 39, 55, 71, 87, 103, 119, 135, 151, 167, 183, 199, 215}}; + +const OpClass GlobalOpClass_134 = {134, 160, 5950, {15, 47, 79, 111, 143, 175, 207}}; + +const OpClass GlobalOpClass_137 = {137, 320, 5950, {31, 63, 95, 127, 159, 191}}; + +const OpClass GlobalOpClass_135 = {135, + 80, + 5950, + {7, 23, 39, 55, 71, 87, 103, 119, 135, 151, 167, 183, 199, 215}}; + +const OpClass GlobalOpClass_136 = {136, 20, 5925, {2}}; + +const std::vector USOpClass = {GlobalOpClass_131, + GlobalOpClass_132, + GlobalOpClass_133, + GlobalOpClass_134, + // Opclass 135 is a non contiguous 80+80 channels. + // GlobalOpClass_135, + GlobalOpClass_136, + GlobalOpClass_137}; + +// Hardcode for PSD to only consider 20MHz channels +const std::vector PSDOpClassList = {GlobalOpClass_131, GlobalOpClass_136}; + +} + +static const std::map nlcdCodeNames = { + {0, "Unclassified"}, + {11, "Open Water"}, + {12, "Ice/Snow"}, + {21, "Developed, Open Space"}, + {22, "Developed, Low Intensity"}, + {23, "Developed, Medium Intensity"}, + {24, "Developed, High Intensity"}, + {31, "Barren Land"}, + {41, "Deciduous Forest"}, + {42, "Evergreen Forest"}, + {43, "Mixed Forest"}, + {51, "Alaska Dwarf Scrub"}, + {52, "Shrub/Scrub"}, + {71, "Grassland/Herbaceous"}, + {72, "Alaska Sedge/Herbaceous"}, + {73, "Alaska Lichens"}, + {74, "Alaska Moss"}, + {81, "Pasture/Hay"}, + {82, "Cultivated Crops"}, + {90, "Woody Wetlands"}, + {95, "Emergent Herbaceous Wetlands"}, +}; +static const std::map nlcdLandCatNames = { + {CConst::deciduousTreesNLCDLandCat, "deciduousTreesNLCDLandCat"}, + {CConst::coniferousTreesNLCDLandCat, "coniferousTreesNLCDLandCat"}, + {CConst::highCropFieldsNLCDLandCat, "highCropFieldsNLCDLandCat"}, + {CConst::noClutterNLCDLandCat, "noClutterNLCDLandCat"}, + {CConst::villageCenterNLCDLandCat, "villageCenterNLCDLandCat"}, + {CConst::unknownNLCDLandCat, "unknownNLCDLandCat"}, +}; +static const std::map pathLossModelNames = { + {CConst::unknownPathLossModel, "unknownPathLossModel"}, + {CConst::ITMBldgPathLossModel, "ITMBldgPathLossModel"}, + {CConst::CoalitionOpt6PathLossModel, "CoalitionOpt6PathLossModel"}, + {CConst::FCC6GHzReportAndOrderPathLossModel, "FCC6GHzReportAndOrderPathLossModel"}, + {CConst::CustomPathLossModel, "CustomPathLossModel"}, + {CConst::ISEDDBS06PathLossModel, "ISEDDBS06PathLossModel"}, + {CConst::BrazilPathLossModel, "BrazilPathLossModel"}, + {CConst::OfcomPathLossModel, "OfcomPathLossModel"}, + {CConst::FSPLPathLossModel, "FSPLPathLossModel"}}; +static const std::map propEnvNames = {{CConst::unknownPropEnv, "unknownPropEnv"}, + {CConst::urbanPropEnv, "urbanPropEnv"}, + {CConst::suburbanPropEnv, + "suburbanPropEnv"}, + {CConst::ruralPropEnv, "ruralPropEnv"}, + {CConst::barrenPropEnv, "barrenPropEnv"}}; + +/** GZIP CSV for EIRP computation */ +class EirpGzipCsv : public GzipCsv +{ + public: + ColStr callsign; // ULS Path Callsign + ColInt pathNum; // ULS Path number + ColInt ulsId; // ULS Path ID from DB + ColInt segIdx; // Segment index + ColInt divIdx; // Diversity index + ColDouble scanLat; // Scanpoint latitude + ColDouble scanLon; // Scanpoint longitude + ColDouble scanAgl; // Scanpoint elevation above ground + ColDouble scanAmsl; // Scanpoint elevation above terrain + ColInt scanPtIdx; // Scanpoint index + ColDouble distKm; // Distance from scanpoint to FS RX + ColDouble elevationAngleTx; // Elevation from scanpoint to FS RX + ColInt channel; // Channel number + ColDouble chanStartMhz; // Channel start MHz + ColDouble chanEndMhz; // Channel end MHz + ColDouble chanBwMhz; // Channel bandwidth MHz + ColEnum chanType; // Request type (by freq/by number) + ColDouble eirpLimit; // Resulting EIRP limit dB + ColBool fspl; // Freespace (trial) pathLoss computation + ColDouble pathLossDb; // Path loss dB + ColEnum configPathLossModel; // Configured path loss model + ColStr resultedPathLossModel; // Resulted path poss model + ColDouble buildingPenetrationDb; // Building penetration loss dB + ColDouble offBoresight; // Angle beween RX beam and direction to scanpoint + ColDouble rxGainDb; // RX Gain DB (loss due to antenna diagram) + ColDouble discriminationGainDb; // Discrimination gain + ColEnum txPropEnv; // TX Propagation environment + ColEnum nlcdTx; // Land use at RLAN + ColStr pathClutterTxModel; // Path Clutter TX model + ColDouble pathClutterTxDb; // Path clutter TX dB + ColStr txClutter; // TX Clutter + ColEnum rxPropEnv; // RX Propagation environment + ColEnum nlcdRx; // Land use at FS + ColStr pathClutterRxModel; // Path Clutter RX model + ColDouble pathClutterRxDb; // Path Clutter RX dB + ColStr rxClutter; // RX Clutter + ColDouble nearFieldOffsetDb; // Near field offset dB + ColDouble spectralOverlapLossDb; // Spectral overlap loss dB + ColDouble ulsAntennaFeederLossDb; // FS Antenna feeder loss dB + ColDouble rxPowerDbW; // Intermediate aggregated loss + ColDouble ulsNoiseLevelDbW; // FS Noise level dB + + EirpGzipCsv(const std::string &filename) : + GzipCsv(filename), + callsign(this, "CallSign"), + pathNum(this, "PathNumber"), + ulsId(this, "UlsId"), + segIdx(this, "SegIdx"), + divIdx(this, "DivIdx"), + scanLat(this, "ScanLat"), + scanLon(this, "ScanLon"), + scanAgl(this, "ScanAGL"), + scanAmsl(this, "ScanAMSL"), + scanPtIdx(this, "ScanIdx"), + distKm(this, "DistKm"), + elevationAngleTx(this, "ElevationAngleTx"), + channel(this, "Channel"), + chanStartMhz(this, "ChanStartMhz"), + chanEndMhz(this, "ChanEndMhz"), + chanBwMhz(this, "ChanBwMhz"), + chanType(this, + "ChanType", + {{INQUIRED_FREQUENCY, "ByFreq"}, {INQUIRED_CHANNEL, "ByNumber"}}), + eirpLimit(this, "EIRP"), + fspl(this, "FreeSpace"), + pathLossDb(this, "PathLossDb"), + configPathLossModel(this, "ConfigPathLossModel", pathLossModelNames), + resultedPathLossModel(this, "ResultedPathLossModel"), + buildingPenetrationDb(this, "BuildingPenetrationDb"), + offBoresight(this, "OffBoresightDeg"), + rxGainDb(this, "RxGainDb"), + discriminationGainDb(this, "DiscrGainDb"), + txPropEnv(this, "TxPropEnv", propEnvNames), + nlcdTx(this, "TxLandUse", nlcdLandCatNames), + pathClutterTxModel(this, "TxClutterModel"), + pathClutterTxDb(this, "PathClutterTxDb"), + txClutter(this, "TxClutter"), + rxPropEnv(this, "RxPropEnv", propEnvNames), + nlcdRx(this, "RxLandUse", nlcdLandCatNames), + pathClutterRxModel(this, "RxClutterModel"), + pathClutterRxDb(this, "PathClutterRxDb"), + rxClutter(this, "RxClutter"), + nearFieldOffsetDb(this, "NearFieldOffsetDb"), + spectralOverlapLossDb(this, "SpectralOverlapLossDb"), + ulsAntennaFeederLossDb(this, "UlsAntennaFeederLossDb"), + rxPowerDbW(this, "RxPowerDbW"), + ulsNoiseLevelDbW(this, "UlsNoiseLevel") + + { + } +}; + +/** GZIP CSV for anomaly writer */ +class AnomalyGzipCsv : public GzipCsv +{ + public: + ColInt fsid; + ColStr dbName; + ColStr callsign; + ColDouble rxLatitudeDeg; + ColDouble rxLongitudeDeg; + ColStr anomaly; + + AnomalyGzipCsv(const std::string &filename) : + GzipCsv(filename), + fsid(this, "FSID"), + dbName(this, "DBNAME"), + callsign(this, "CALLSIGN"), + rxLatitudeDeg(this, "RX_LATITUDE"), + rxLongitudeDeg(this, "RX_LONGITUDE"), + anomaly(this, "ANOMALY_DESCRIPTION") + { + } +}; + +class ExcThrParamClass +{ + public: + double rlanDiscriminationGainDB; + double bodyLossDB; + std::string buildingPenetrationModelStr; + double buildingPenetrationCDF; + double buildingPenetrationDB; + double angleOffBoresightDeg; + std::string pathLossModelStr; + double pathLossCDF; + std::string pathClutterTxModelStr; + double pathClutterTxCDF; + double pathClutterTxDB; + std::string txClutterStr; + std::string pathClutterRxModelStr; + double pathClutterRxCDF; + double pathClutterRxDB; + std::string rxClutterStr; + double rxGainDB; + double discriminationGain; + std::string rxAntennaSubModelStr; + double nearFieldOffsetDB; + double spectralOverlapLossDB; + double polarizationLossDB; + double rxAntennaFeederLossDB; + double reflectorD0; + double reflectorD1; + double nearField_xdb; + double nearField_u; + double nearField_eff; +}; + +/** GZIP CSV for excThr */ +class ExThrGzipCsv : public GzipCsv +{ + public: + ColInt fsid; + ColStr region; + ColStr dbName; + ColInt rlanPosnIdx; + ColStr callsign; + ColDouble fsLon; + ColDouble fsLat; + ColDouble fsAgl; + ColDouble fsTerrainHeight; + ColStr fsTerrainSource; + ColStr fsPropEnv; + ColInt numPr; + ColInt divIdx; + ColInt segIdx; + ColDouble segRxLon; + ColDouble segRxLat; + ColDouble refThetaIn; + ColDouble refKs; + ColDouble refQ; + ColDouble refD0; + ColDouble refD1; + ColDouble rlanLon; + ColDouble rlanLat; + ColDouble rlanAgl; + ColDouble rlanTerrainHeight; + ColStr rlanTerrainSource; + ColStr rlanPropEnv; + ColDouble rlanFsDist; + ColDouble rlanFsGroundDist; + ColDouble rlanElevAngle; + ColDouble boresightAngle; + ColDouble rlanTxEirp; + ColStr rlanAntennaModel; + ColDouble rlanAOB; + ColDouble rlanDiscriminationGainDB; + ColDouble bodyLoss; + ColStr rlanClutterCategory; + ColStr fsClutterCategory; + ColStr buildingType; + ColDouble buildingPenetration; + ColStr buildingPenetrationModel; + ColDouble buildingPenetrationCdf; + ColDouble pathLoss; + ColStr pathLossModel; + ColDouble pathLossCdf; + ColDouble pathClutterTx; + ColStr pathClutterTxMode; + ColDouble pathClutterTxCdf; + ColDouble pathClutterRx; + ColStr pathClutterRxMode; + ColDouble pathClutterRxCdf; + ColDouble rlanBandwidth; + ColDouble rlanStartFreq; + ColDouble rlanStopFreq; + ColDouble ulsStartFreq; + ColDouble ulsStopFreq; + ColStr antType; + ColStr antCategory; + ColDouble antGainPeak; + ColStr prType; + ColDouble prEffectiveGain; + ColDouble prDiscrinminationGain; + ColDouble fsGainToRlan; + ColDouble fsNearFieldXdb; + ColDouble fsNearFieldU; + ColDouble fsNearFieldEff; + ColDouble fsNearFieldOffset; + ColDouble spectralOverlapLoss; + ColDouble polarizationLoss; + ColDouble fsRxFeederLoss; + ColDouble fsRxPwr; + ColDouble fsIN; + ColDouble eirpLimit; + ColDouble fsSegDist; + ColDouble ulsLinkDist; // Field from runExclusionZoneAnalysis() and + // runHeatMapAnalysis() + ColDouble rlanCenterFreq; + ColDouble fsTxToRlanDist; + ColDouble pathDifference; + ColDouble ulsWavelength; + ColDouble fresnelIndex; + ColStr comment; // Field from runExclusionZoneAnalysis() + + ExThrGzipCsv(const std::string filename) : + GzipCsv(filename), + fsid(this, "FS_ID"), + region(this, "FS_REGION"), + dbName(this, "DBNAME"), + rlanPosnIdx(this, "RLAN_POSN_IDX"), + callsign(this, "CALLSIGN"), + fsLon(this, "FS_RX_LONGITUDE (deg)"), + fsLat(this, "FS_RX_LATITUDE (deg)"), + fsAgl(this, "FS_RX_HEIGHT_ABOVE_TERRAIN (m)"), + fsTerrainHeight(this, "FS_RX_TERRAIN_HEIGHT (m)"), + fsTerrainSource(this, "FS_RX_TERRAIN_SOURCE"), + fsPropEnv(this, "FS_RX_PROP_ENV"), + numPr(this, "NUM_PASSIVE_REPEATER"), + divIdx(this, "IS_DIVERSITY_LINK"), + segIdx(this, "SEGMENT_IDX"), + segRxLon(this, "SEGMENT_RX_LONGITUDE (deg)"), + segRxLat(this, "SEGMENT_RX_LATITUDE (deg)"), + refThetaIn(this, "PR_REF_THETA_IN (deg)"), + refKs(this, "PR_REF_KS"), + refQ(this, "PR_REF_Q"), + refD0(this, "PR_REF_D0 (dB)"), + refD1(this, "PR_REF_D1 (dB)"), + rlanLon(this, "RLAN_LONGITUDE (deg)"), + rlanLat(this, "RLAN_LATITUDE (deg)"), + rlanAgl(this, "RLAN_HEIGHT_ABOVE_TERRAIN (m)"), + rlanTerrainHeight(this, "RLAN_TERRAIN_HEIGHT (m)"), + rlanTerrainSource(this, "RLAN_TERRAIN_SOURCE"), + rlanPropEnv(this, "RLAN_PROP_ENV"), + rlanFsDist(this, "RLAN_FS_RX_DIST (km)"), + rlanFsGroundDist(this, "RLAN_FS_RX_GROUND_DIST (km)"), + rlanElevAngle(this, "RLAN_FS_RX_ELEVATION_ANGLE (deg)"), + boresightAngle(this, "FS_RX_ANGLE_OFF_BORESIGHT (deg)"), + rlanTxEirp(this, "RLAN_TX_EIRP (dBm)"), + rlanAntennaModel(this, "RLAN_ANTENNA_MODEL"), + rlanAOB(this, "RLAN_ANGLE_OFF_BORESIGHT (deg)"), + rlanDiscriminationGainDB(this, "RLAN_DISCRIMINATION_GAIN (dB)"), + bodyLoss(this, "BODY_LOSS (dB)"), + rlanClutterCategory(this, "RLAN_CLUTTER_CATEGORY"), + fsClutterCategory(this, "FS_CLUTTER_CATEGORY"), + buildingType(this, "BUILDING TYPE"), + buildingPenetration(this, "RLAN_FS_RX_BUILDING_PENETRATION (dB)"), + buildingPenetrationModel(this, "BUILDING_PENETRATION_MODEL"), + buildingPenetrationCdf(this, "BUILDING_PENETRATION_CDF"), + pathLoss(this, "PATH_LOSS (dB)"), + pathLossModel(this, "PATH_LOSS_MODEL"), + pathLossCdf(this, "PATH_LOSS_CDF"), + pathClutterTx(this, "PATH_CLUTTER_TX (DB)"), + pathClutterTxMode(this, "PATH_CLUTTER_TX_MODEL"), + pathClutterTxCdf(this, "PATH_CLUTTER_TX_CDF"), + pathClutterRx(this, "PATH_CLUTTER_RX (DB)"), + pathClutterRxMode(this, "PATH_CLUTTER_RX_MODEL"), + pathClutterRxCdf(this, "PATH_CLUTTER_RX_CDF"), + rlanBandwidth(this, "RLAN BANDWIDTH (MHz)"), + rlanStartFreq(this, "RLAN CHANNEL START FREQ (MHz)"), + rlanStopFreq(this, "RLAN CHANNEL STOP FREQ (MHz)"), + ulsStartFreq(this, "ULS START FREQ (MHz)"), + ulsStopFreq(this, "ULS STOP FREQ (MHz)"), + antType(this, "FS_ANT_TYPE"), + antCategory(this, "FS_ANT_CATEGORY"), + antGainPeak(this, "FS_ANT_GAIN_PEAK (dB)"), + prType(this, "PR_TYPE (dB)"), + prEffectiveGain(this, "PR_EFFECTIVE_GAIN (dB)"), + prDiscrinminationGain(this, "PR_DISCRIMINATION_GAIN (dB)"), + fsGainToRlan(this, "FS_ANT_GAIN_TO_RLAN (dB)"), + fsNearFieldXdb(this, "FS_ANT_NEAR_FIELD_XDB"), + fsNearFieldU(this, "FS_ANT_NEAR_FIELD_U"), + fsNearFieldEff(this, "FS_ANT_NEAR_FIELD_EFF"), + fsNearFieldOffset(this, "FS_ANT_NEAR_FIELD_OFFSET (dB)"), + spectralOverlapLoss(this, "RX_SPECTRAL_OVERLAP_LOSS (dB)"), + polarizationLoss(this, "POLARIZATION_LOSS (dB)"), + fsRxFeederLoss(this, "FS_RX_FEEDER_LOSS (dB)"), + fsRxPwr(this, "FS_RX_PWR (dBW)"), + fsIN(this, "FS I/N (dB)"), + eirpLimit(this, "EIRP_LIMIT (dBm)"), + fsSegDist(this, "FS_SEGMENT_DIST (m)"), + ulsLinkDist(this, "ULS_LINK_DIST (m)"), + rlanCenterFreq(this, "RLAN_CENTER_FREQ (Hz)"), + fsTxToRlanDist(this, "FS_TX_TO_RLAN_DIST (m)"), + pathDifference(this, "PATH_DIFFERENCE (m)"), + ulsWavelength(this, "ULS_WAVELENGTH (mm)"), + fresnelIndex(this, "FRESNEL_INDEX"), + comment(this, "COMMENT") + { + } +}; + +/** GZIP CSV for traces */ +class TraceGzipCsv : public GzipCsv +{ + public: + ColStr ptId; + ColDouble lon; + ColDouble lat; + ColDouble dist; + ColDouble amsl; + ColDouble losAmsl; + ColInt fsid; + ColInt divIdx; + ColInt segIdx; + ColInt scanPtIdx; + ColInt rlanHtIdx; + + TraceGzipCsv(std::string filename) : + GzipCsv(filename), + ptId(this, "PT_ID"), + lon(this, "PT_LON (deg)"), + lat(this, "PT_LAT (deg)"), + dist(this, "GROUND_DIST (Km)"), + amsl(this, "TERRAIN_HEIGHT_AMSL (m)"), + losAmsl(this, "LOS_PATH_HEIGHT_AMSL (m)"), + fsid(this, "FSID"), + divIdx(this, "DIV_IDX"), + segIdx(this, "SEG_IDX"), + scanPtIdx(this, "SCAN_PT_IDX"), + rlanHtIdx(this, "RLAN_HEIGHT_IDX") + { + } +}; + +AfcManager::AfcManager() +{ + _createKmz = false; + _createDebugFiles = false; + _createSlowDebugFiles = false; + _certifiedIndoor = false; + + _dataIf = (AfcDataIf *)NULL; + + _rlanUncertaintyRegionType = RLANBoundary::NO_BOUNDARY; + _rlanLLA = std::make_tuple(quietNaN, quietNaN, quietNaN); + _rlanUncerts_m = std::make_tuple(quietNaN, quietNaN, quietNaN); + _allowScanPtsInUncRegFlag = false; + + _scanRegionMethod = CConst::latLonAlignGridScanRegionMethod; + + _scanres_points_per_degree = -1; + _scanres_xy = quietNaN; + _scanres_ht = quietNaN; + _indoorFixedHeightAMSL = false; + + _maxVerticalUncertainty = quietNaN; + _maxHorizontalUncertaintyDistance = quietNaN; + + _scanPointBelowGroundMethod = CConst::TruncateScanPointBelowGroundMethod; + + _minEIRPIndoor_dBm = quietNaN; + _minEIRPOutdoor_dBm = quietNaN; + _minEIRP_dBm = quietNaN; + _maxEIRP_dBm = quietNaN; + _minPSD_dBmPerMHz = quietNaN; + _reportUnavailPSDdBmPerMHz = quietNaN; + _inquiredFrequencyMaxPSD_dBmPerMHz = quietNaN; + _rlanAzimuthPointing = quietNaN; + _rlanElevationPointing = quietNaN; + + _IoverN_threshold_dB = -6.0; + _bodyLossIndoorDB = 0.0; // Indoor body Loss (dB) + _bodyLossOutdoorDB = 0.0; // Outdoor body Loss (dB) + _polarizationLossDB = 0.0; // Polarization Loss (dB) + _rlanOrientation_deg = + 0.0; // Orientation (deg) of ellipse clockwise from North in [-90, 90] + _rlanType = RLANType::RLAN_OUTDOOR; + _rlanHeightType = CConst::AGLHeightType; + + _buildingType = CConst::noBuildingType; + + _fixedBuildingLossFlag = false; + _fixedBuildingLossValue = 0.0; + + _confidenceBldg2109 = quietNaN; + _confidenceClutter2108 = quietNaN; + _confidenceWinner2LOS = quietNaN; + _confidenceWinner2NLOS = quietNaN; + _confidenceWinner2Combined = quietNaN; + _confidenceITM = quietNaN; + _reliabilityITM = quietNaN; + + _winner2LOSOption = CConst::UnknownLOSOption; + + _channelResponseAlgorithm = CConst::pwrSpectralAlgorithm; + + _winner2UnknownLOSMethod = CConst::PLOSCombineWinner2UnknownLOSMethod; + + _winner2ProbLOSThr = quietNaN; // Winner2 prob LOS threshold, if probLOS exceeds threshold, + // use LOS model, otherwise use NLOS + + _winner2UseGroundDistanceFlag = true; + _fsplUseGroundDistanceFlag = false; + + _propEnvMethod = CConst::unknownPropEnvMethod; + + _rxFeederLossDBIDU = quietNaN; + _rxFeederLossDBODU = quietNaN; + _rxFeederLossDBUnknown = quietNaN; + + _itmEpsDielect = quietNaN; + _itmSgmConductivity = quietNaN; + _itmPolarization = 1; + _itmMinSpacing = 5.0; + _itmMaxNumPts = 1500; + + _exclusionZoneFSID = 0; + _exclusionZoneRLANChanIdx = -1; + _exclusionZoneRLANBWHz = quietNaN; + _exclusionZoneRLANEIRPDBm = quietNaN; + + _heatmapMinLon = quietNaN; + _heatmapMaxLon = quietNaN; + _heatmapMinLat = quietNaN; + _heatmapMaxLat = quietNaN; + _heatmapRLANSpacing = quietNaN; + + _heatmapRLANIndoorEIRPDBm = quietNaN; + _heatmapRLANIndoorHeight = quietNaN; + _heatmapRLANIndoorHeightUncertainty = quietNaN; + + _heatmapRLANOutdoorEIRPDBm = quietNaN; + _heatmapRLANOutdoorHeight = quietNaN; + _heatmapRLANOutdoorHeightUncertainty = quietNaN; + + _applyClutterFSRxFlag = false; + _allowRuralFSClutterFlag = false; + _fsConfidenceClutter2108 = quietNaN; + _maxFsAglHeight = quietNaN; + + _rlanITMTxClutterMethod = CConst::ForceTrueITMClutterMethod; + + _cdsmLOSThr = quietNaN; + + _minRlanHeightAboveTerrain = quietNaN; + + _maxRadius = quietNaN; + _exclusionDist = quietNaN; + + _nearFieldAdjFlag = true; + _passiveRepeaterFlag = true; + _reportErrorRlanHeightLowFlag = false; + _illuminationEfficiency = quietNaN; + _closeInHgtFlag = true; + _closeInHgtLOS = quietNaN; + _closeInDist = quietNaN; + _pathLossClampFSPL = false; + _printSkippedLinksFlag = false; + _roundPSDEIRPFlag = true; + + _wlanMinFreqMHz = -1; + _wlanMaxFreqMHz = -1; + _wlanMinFreq = quietNaN; + _wlanMaxFreq = quietNaN; + + _regionPolygonResolution = quietNaN; + _rainForestPolygon = (PolygonClass *)NULL; + + _densityThrUrban = quietNaN; + _densityThrSuburban = quietNaN; + _densityThrRural = quietNaN; + + _removeMobile = false; + + _filterSimRegionOnly = false; + + _ulsDefaultAntennaType = CConst::F1245AntennaType; + + _visibilityThreshold = + quietNaN; // I/N threshold to determine whether or not an RLAN is visible to an FS + _maxLidarRegionLoadVal = -1; + + _terrainDataModel = (TerrainClass *)NULL; + + _bodyLossDB = 0.0; + + _numRegion = -1; + + _popGrid = (PopGridClass *)NULL; + + _ulsList = new ListClass(0); + + _pathLossModel = CConst::unknownPathLossModel; + + _zbldg2109 = quietNaN; + _zclutter2108 = quietNaN; + _fsZclutter2108 = quietNaN; + _zwinner2LOS = quietNaN; + _zwinner2NLOS = quietNaN; + _zwinner2Combined = quietNaN; + + _rlanRegion = (RlanRegionClass *)NULL; + + _ituData = (ITUDataClass *)NULL; + _nfa = (NFAClass *)NULL; + _prTable = (PRTABLEClass *)NULL; + + _aciFlag = true; + + _rlanAntenna = (AntennaClass *)NULL; + _rlanPointing = Vector3(0.0, 0.0, 0.0); + + _exclusionZoneFSTerrainHeight = quietNaN; + _exclusionZoneHeightAboveTerrain = quietNaN; + + _heatmapIToNDB = (double **)NULL; + _heatmapIsIndoor = (bool **)NULL; + _heatmapNumPtsLon = 0; + _heatmapNumPtsLat = 0; + _heatmapMinIToNDB = quietNaN; + _heatmapMaxIToNDB = quietNaN; + _heatmapIToNThresholdDB = quietNaN; + + _responseCode = CConst::successResponseCode; +} + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::~AfcManager() ****/ +/******************************************************************************************/ +AfcManager::~AfcManager() +{ + clearData(); + if (AfcManager::_dataIf) { + delete AfcManager::_dataIf; + } + delete _ulsList; + + if (_ituData) { + delete _ituData; + } + + if (_nfa) { + delete _nfa; + } + + if (_prTable) { + delete _prTable; + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** sortFunction used for RADIAL_POLY ****/ +/******************************************************************************************/ +bool sortFunction(std::pair p0, std::pair p1) +{ + return (p0.first < p1.first); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::initializeDatabases() ****/ +/******************************************************************************************/ +void AfcManager::initializeDatabases() +{ + if (_responseCode != CConst::successResponseCode) { + return; + } + + /**************************************************************************************/ + /* Read region polygons and confirm that rlan is inside simulation region */ + /**************************************************************************************/ + std::vector regionPolygonFileStrList = split(_regionPolygonFileList, ','); + _numRegion = regionPolygonFileStrList.size(); + + for (int regionIdx = 0; regionIdx < _numRegion; ++regionIdx) { + std::vector polyList = + PolygonClass::readMultiGeometry(regionPolygonFileStrList[regionIdx], + _regionPolygonResolution); + PolygonClass *regionPoly = PolygonClass::combinePolygons(polyList); + regionPoly->name = _regionPolygonFileList[regionIdx]; + + double regionArea = regionPoly->comp_bdy_area(); + + double checkArea = 0.0; + for (int polyIdx = 0; polyIdx < (int)polyList.size(); ++polyIdx) { + PolygonClass *poly = polyList[polyIdx]; + checkArea += poly->comp_bdy_area(); + delete poly; + } + + if (fabs(regionArea - checkArea) > 1.0e-6) { + throw std::runtime_error(ErrStream() + << "ERROR: INVALID region polygon file = " + << regionPolygonFileStrList[regionIdx]); + } + + _regionPolygonList.push_back(regionPoly); + LOGGER_INFO(logger) << "REGION: " << regionPolygonFileStrList[regionIdx] + << " AREA: " << regionArea; + } + + double rlanLatitude, rlanLongitude, rlanHeightInput; + std::tie(rlanLatitude, rlanLongitude, rlanHeightInput) = _rlanLLA; + int xIdx = (int)floor(rlanLongitude / _regionPolygonResolution + 0.5); + int yIdx = (int)floor(rlanLatitude / _regionPolygonResolution + 0.5); + + bool found = false; + for (int polyIdx = 0; (polyIdx < (int)_regionPolygonList.size()) && (!found); ++polyIdx) { + PolygonClass *poly = _regionPolygonList[polyIdx]; + + if (poly->in_bdy_area(xIdx, yIdx)) { + found = true; + } + } + if (!found) { + _responseCode = CConst::invalidValueResponseCode; + _invalidParams << "latitude"; + _invalidParams << "longitude"; + return; + } + /**************************************************************************************/ + + // Following lines are finding the minimum and maximum longitudes and latitudes with the + // 150km rlan range + double minLon, maxLon, minLat, maxLat; + double minLonBldg, maxLonBldg, minLatBldg, maxLatBldg; + + if (_analysisType == "HeatmapAnalysis") { + defineHeatmapColors(); + } + + createChannelList(); + + if (_responseCode != CConst::successResponseCode) { + return; + } + + /**************************************************************************************/ + /* Compute ulsMinFreq, and ulsMaxFreq */ + /**************************************************************************************/ + int chanIdx; + int ulsMinFreqMHz = 0; + int ulsMaxFreqMHz = 0; + for (chanIdx = 0; chanIdx < (int)_channelList.size(); ++chanIdx) { + ChannelStruct *channel = &(_channelList[chanIdx]); + int minFreqMHz; + int maxFreqMHz; + int startFreqMHz = channel->freqMHzList.front(); + int stopFreqMHz = channel->freqMHzList.back(); + if ((channel->type == ChannelType::INQUIRED_CHANNEL) && (_aciFlag)) { + minFreqMHz = 2 * startFreqMHz - stopFreqMHz; + maxFreqMHz = 2 * stopFreqMHz - startFreqMHz; + } else { + minFreqMHz = startFreqMHz; + maxFreqMHz = stopFreqMHz; + } + if ((ulsMinFreqMHz == 0) || (minFreqMHz < ulsMinFreqMHz)) { + ulsMinFreqMHz = minFreqMHz; + } + if ((ulsMaxFreqMHz == 0) || (maxFreqMHz > ulsMaxFreqMHz)) { + ulsMaxFreqMHz = maxFreqMHz; + } + } + + double ulsMinFreq = ulsMinFreqMHz * 1.0e6; + double ulsMaxFreq = ulsMaxFreqMHz * 1.0e6; + /**************************************************************************************/ + + /**************************************************************************************/ + /* Set Path Loss Model Parameters */ + /**************************************************************************************/ + switch (_pathLossModel) { + case CConst::ITMBldgPathLossModel: + _closeInDist = 0.0; // Radius in which close in path loss model is used + break; + case CConst::CoalitionOpt6PathLossModel: + _closeInDist = 1.0e3; // Radius in which close in path loss model is used + break; + case CConst::FCC6GHzReportAndOrderPathLossModel: + _closeInDist = 1.0e3; // Radius in which close in path loss model is used + break; + case CConst::FSPLPathLossModel: + _closeInDist = 0.0; // Radius in which close in path loss model is used + break; + default: + throw std::runtime_error( + ErrStream() + << std::string("ERROR: Path Loss Model set to invalid value \"") + + CConst::strPathLossModelList->type_to_str( + _pathLossModel) + + "\""); + break; + } + + ULSClass::pathLossModel = _pathLossModel; + + /**************************************************************************************/ + + if (_analysisType == "AP-AFC" || _analysisType == "ScanAnalysis" || + _analysisType == "test_itm") { + bool fixedHeightAMSL; + if (_rlanType == RLANType::RLAN_INDOOR) { + fixedHeightAMSL = _indoorFixedHeightAMSL; + } else { + fixedHeightAMSL = false; + } + + double centerLat, centerLon; + + /**************************************************************************************/ + /* Create Rlan Uncertainty Region */ + /**************************************************************************************/ + switch (_rlanUncertaintyRegionType) { + case ELLIPSE: + _rlanRegion = (RlanRegionClass *)new EllipseRlanRegionClass( + _rlanLLA, + _rlanUncerts_m, + _rlanOrientation_deg, + fixedHeightAMSL); + break; + case LINEAR_POLY: + _rlanRegion = (RlanRegionClass *)new PolygonRlanRegionClass( + _rlanLLA, + _rlanUncerts_m, + _rlanLinearPolygon, + LINEAR_POLY, + fixedHeightAMSL); + break; + case RADIAL_POLY: + std::sort(_rlanRadialPolygon.begin(), + _rlanRadialPolygon.end(), + sortFunction); + _rlanRegion = (RlanRegionClass *)new PolygonRlanRegionClass( + _rlanLLA, + _rlanUncerts_m, + _rlanRadialPolygon, + RADIAL_POLY, + fixedHeightAMSL); + break; + default: + throw std::runtime_error(ErrStream() + << "ERROR: INVALID " + "_rlanUncertaintyRegionType = " + << _rlanUncertaintyRegionType); + break; + } + /**************************************************************************************/ + + double rlanRegionSize = _rlanRegion->getMaxDist(); + centerLon = _rlanRegion->getCenterLongitude(); + centerLat = _rlanRegion->getCenterLatitude(); + + minLat = centerLat - + ((_maxRadius + rlanRegionSize) / CConst::earthRadius) * 180.0 / M_PI; + maxLat = centerLat + + ((_maxRadius + rlanRegionSize) / CConst::earthRadius) * 180.0 / M_PI; + + double maxAbsLat = std::max(fabs(minLat), fabs(maxLat)); + minLon = centerLon - ((_maxRadius + rlanRegionSize) / + (CConst::earthRadius * cos(maxAbsLat * M_PI / 180.0))) * + 180.0 / M_PI; + maxLon = centerLon + ((_maxRadius + rlanRegionSize) / + (CConst::earthRadius * cos(maxAbsLat * M_PI / 180.0))) * + 180.0 / M_PI; + + if (_pathLossModel == CConst::FCC6GHzReportAndOrderPathLossModel) { + double maxDistBldg; + if (_rlanITMTxClutterMethod == CConst::BldgDataITMCLutterMethod) { + maxDistBldg = _maxRadius; + } else { + maxDistBldg = _closeInDist; + } + + minLatBldg = centerLat - + ((maxDistBldg + rlanRegionSize) / CConst::earthRadius) * + 180.0 / M_PI; + maxLatBldg = centerLat + + ((maxDistBldg + rlanRegionSize) / CConst::earthRadius) * + 180.0 / M_PI; + + double maxAbsLatBldg = std::max(fabs(minLatBldg), fabs(maxLatBldg)); + minLonBldg = centerLon - + ((maxDistBldg + rlanRegionSize) / + (CConst::earthRadius * cos(maxAbsLatBldg * M_PI / 180.0))) * + 180.0 / M_PI; + maxLonBldg = centerLon + + ((maxDistBldg + rlanRegionSize) / + (CConst::earthRadius * cos(maxAbsLatBldg * M_PI / 180.0))) * + 180.0 / M_PI; + } else { + minLatBldg = minLat; + maxLatBldg = maxLat; + minLonBldg = minLon; + maxLonBldg = maxLon; + } + } else if (_analysisType == "ExclusionZoneAnalysis") { + readULSData(_ulsDatabaseList, + (PopGridClass *)NULL, + 0, + ulsMinFreq, + ulsMaxFreq, + _removeMobile, + CConst::FixedSimulation, + 0.0, + 0.0, + 0.0, + 0.0); + readDeniedRegionData(_deniedRegionFile); + if (_ulsList->getSize() == 0) { + } else if (_ulsList->getSize() > 1) { + } + double centerLat = (*_ulsList)[0]->getRxLatitudeDeg(); + double centerLon = (*_ulsList)[0]->getRxLongitudeDeg(); + + minLat = centerLat - ((_maxRadius) / CConst::earthRadius) * 180.0 / M_PI; + maxLat = centerLat + ((_maxRadius) / CConst::earthRadius) * 180.0 / M_PI; + + double maxAbsLat = std::max(fabs(minLat), fabs(maxLat)); + minLon = centerLon - + ((_maxRadius) / (CConst::earthRadius * cos(maxAbsLat * M_PI / 180.0))) * + 180.0 / M_PI; + maxLon = centerLon + + ((_maxRadius) / (CConst::earthRadius * cos(maxAbsLat * M_PI / 180.0))) * + 180.0 / M_PI; + + if (_pathLossModel == CConst::FCC6GHzReportAndOrderPathLossModel) { + minLatBldg = centerLat - + ((_closeInDist) / CConst::earthRadius) * 180.0 / M_PI; + maxLatBldg = centerLat + + ((_closeInDist) / CConst::earthRadius) * 180.0 / M_PI; + + double maxAbsLatBldg = std::max(fabs(minLatBldg), fabs(maxLatBldg)); + minLonBldg = centerLon - + ((_closeInDist) / + (CConst::earthRadius * cos(maxAbsLatBldg * M_PI / 180.0))) * + 180.0 / M_PI; + maxLonBldg = centerLon + + ((_closeInDist) / + (CConst::earthRadius * cos(maxAbsLatBldg * M_PI / 180.0))) * + 180.0 / M_PI; + } else { + minLatBldg = minLat; + maxLatBldg = maxLat; + minLonBldg = minLon; + maxLonBldg = maxLon; + } + } else if (_analysisType == "HeatmapAnalysis") { + minLat = _heatmapMinLat - (_maxRadius / CConst::earthRadius) * 180.0 / M_PI; + maxLat = _heatmapMaxLat + (_maxRadius / CConst::earthRadius) * 180.0 / M_PI; + + double maxAbsLat = std::max(fabs(minLat), fabs(maxLat)); + minLon = _heatmapMinLon - + (_maxRadius / (CConst::earthRadius * cos(maxAbsLat * M_PI / 180.0))) * + 180.0 / M_PI; + maxLon = _heatmapMaxLon + + (_maxRadius / (CConst::earthRadius * cos(maxAbsLat * M_PI / 180.0))) * + 180.0 / M_PI; + + if (_pathLossModel == CConst::FCC6GHzReportAndOrderPathLossModel) { + minLatBldg = _heatmapMinLat - + (_closeInDist / CConst::earthRadius) * 180.0 / M_PI; + maxLatBldg = _heatmapMaxLat + + (_closeInDist / CConst::earthRadius) * 180.0 / M_PI; + + double maxAbsLatBldg = std::max(fabs(minLatBldg), fabs(maxLatBldg)); + minLonBldg = _heatmapMinLon - + (_closeInDist / + (CConst::earthRadius * cos(maxAbsLatBldg * M_PI / 180.0))) * + 180.0 / M_PI; + maxLonBldg = _heatmapMaxLon + + (_closeInDist / + (CConst::earthRadius * cos(maxAbsLatBldg * M_PI / 180.0))) * + 180.0 / M_PI; + } else { + minLatBldg = minLat; + maxLatBldg = maxLat; + minLonBldg = minLon; + maxLonBldg = maxLon; + } +#if DEBUG_AFC + } else if (_analysisType == "test_winner2") { + // Do nothing +#endif + } else { + throw std::runtime_error(QString("Invalid analysis type: %1") + .arg(QString::fromStdString(_analysisType)) + .toStdString()); + } + + /**************************************************************************************/ + /* Setup Terrain data */ + /**************************************************************************************/ + // The following counters are used to understand the percentage of holes in terrain data + // that exist (mostly for debugging) + UlsMeasurementAnalysis::numInvalidSRTM = + 0; // Define counters for invalid heights returned by SRTM + UlsMeasurementAnalysis::numSRTM = 0; // Define counters for all SRTM calls + + _maxLidarRegionLoadVal = 70; + + _terrainDataModel = new TerrainClass(_lidarDir, + _cdsmDir, + _srtmDir, + _depDir, + _globeDir, + minLat, + minLon, + maxLat, + maxLon, + minLatBldg, + minLonBldg, + maxLatBldg, + maxLonBldg, + _maxLidarRegionLoadVal); + + _terrainDataModel->setSourceName(CConst::HeightSourceEnum::unknownHeightSource, "UNKNOWN"); + _terrainDataModel->setSourceName(CConst::HeightSourceEnum::globalHeightSource, "GLOBE"); + _terrainDataModel->setSourceName(CConst::HeightSourceEnum::depHeightSource, + "3DEP 1 arcsec"); + _terrainDataModel->setSourceName(CConst::HeightSourceEnum::srtmHeightSource, "SRTM"); + if (_useBDesignFlag) { + _terrainDataModel->setSourceName(CConst::HeightSourceEnum::lidarHeightSource, + "B3D-3DEP"); + } else if (_useLiDAR) { + _terrainDataModel->setSourceName(CConst::HeightSourceEnum::lidarHeightSource, + "LiDAR"); + } + + if (_pathLossModel == CConst::ITMBldgPathLossModel) { + if (_terrainDataModel->getNumLidarRegion() == 0) { + throw std::runtime_error(ErrStream() << "Path loss model set to ITM_BLDG, " + "but no building data found within " + "the analysis region."); + } + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Setup ITU data */ + /**************************************************************************************/ + _ituData = new ITUDataClass(_radioClimateFile, _surfRefracFile); + LOGGER_INFO(logger) << "Reading ITU data files: " << _radioClimateFile << " and " + << _surfRefracFile; + /**************************************************************************************/ + + /**************************************************************************************/ + /* Read Near Field Adjustment table */ + /**************************************************************************************/ + if (_nearFieldAdjFlag) { + _nfa = new NFAClass(_nfaTableFile); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Read Passive Repeater table */ + /**************************************************************************************/ + _prTable = new PRTABLEClass(_prTableFile); + /**************************************************************************************/ + + /**************************************************************************************/ + /* Read NLCD data or Polulation Density data depending on propEnvMethod */ + /**************************************************************************************/ + if (_propEnvMethod == CConst::nlcdPointPropEnvMethod) { + /**********************************************************************************/ + /* Setup NLCD data */ + /**********************************************************************************/ + if (_nlcdFile.empty()) { + throw std::runtime_error("AfcManager::initializeDatabases(): _nlcdFile not " + "defined."); + } + std::string nlcdPattern, nlcdDirectory; + auto nlcdFileInfo = QFileInfo(QString::fromStdString(_nlcdFile)); + if (nlcdFileInfo.isDir()) { + nlcdPattern = "*"; + nlcdDirectory = _nlcdFile; + } else { + nlcdPattern = nlcdFileInfo.fileName().toStdString(); + nlcdDirectory = nlcdFileInfo.dir().path().toStdString(); + } + cgNlcd.reset( + new CachedGdal(nlcdDirectory, + "nlcd", + GdalNameMapperDirect::make_unique(nlcdPattern, + nlcdDirectory))); + cgNlcd->setNoData(0); + /**********************************************************************************/ + } else if (_propEnvMethod == CConst::popDensityMapPropEnvMethod) { + if (!(_rainForestFile.empty())) { + std::vector polyList = + PolygonClass::readMultiGeometry(_rainForestFile, + _regionPolygonResolution); + _rainForestPolygon = PolygonClass::combinePolygons(polyList); + + _rainForestPolygon->name = "Rain Forest"; + + for (int polyIdx = 0; polyIdx < (int)polyList.size(); ++polyIdx) { + PolygonClass *poly = polyList[polyIdx]; + delete poly; + } + } + + _popGrid = new PopGridClass(_worldPopulationFile, + _regionPolygonList, + _regionPolygonResolution, + _densityThrUrban, + _densityThrSuburban, + _densityThrRural, + minLat, + minLon, + maxLat, + maxLon); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Read ULS data */ + /**************************************************************************************/ + if (_analysisType == "HeatmapAnalysis" || _analysisType == "AP-AFC" || + _analysisType == "ScanAnalysis") { + readULSData(_ulsDatabaseList, + _popGrid, + 0, + ulsMinFreq, + ulsMaxFreq, + _removeMobile, + CConst::FixedSimulation, + minLat, + maxLat, + minLon, + maxLon); + readDeniedRegionData(_deniedRegionFile); + } else if (_analysisType == "ExclusionZoneAnalysis") { + fixFSTerrain(); +#if DEBUG_AFC + } else if (_analysisType == "test_itm") { + // Do nothing + } else if (_analysisType == "test_winner2") { + // Do nothing +#endif + } else { + throw std::runtime_error(QString("Invalid analysis type: %1") + .arg(QString::fromStdString(_analysisType)) + .toStdString()); + } + /**************************************************************************************/ + + splitFrequencyRanges(); + + /**************************************************************************************/ + /* Convert confidences it gaussian thresholds */ + /**************************************************************************************/ + _zbldg2109 = -qerfi(_confidenceBldg2109); + _zclutter2108 = -qerfi(_confidenceClutter2108); + _fsZclutter2108 = -qerfi(_fsConfidenceClutter2108); + _zwinner2LOS = -qerfi(_confidenceWinner2LOS); + _zwinner2NLOS = -qerfi(_confidenceWinner2NLOS); + _zwinner2Combined = -qerfi(_confidenceWinner2Combined); + /**************************************************************************************/ +} +/**************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::clearData ****/ +/******************************************************************************************/ +void AfcManager::clearData() +{ + clearULSList(); + + for (int antIdx = 0; antIdx < (int)_antennaList.size(); antIdx++) { + delete _antennaList[antIdx]; + } + + for (int drIdx = 0; drIdx < (int)_deniedRegionList.size(); drIdx++) { + delete _deniedRegionList[drIdx]; + } + + if (_popGrid) { + delete _popGrid; + _popGrid = (PopGridClass *)NULL; + } + + if (_heatmapIsIndoor) { + int lonIdx; + for (lonIdx = 0; lonIdx < _heatmapNumPtsLon; ++lonIdx) { + free(_heatmapIsIndoor[lonIdx]); + } + free(_heatmapIsIndoor); + } + + if (_heatmapIToNDB) { + int lonIdx; + for (lonIdx = 0; lonIdx < _heatmapNumPtsLon; ++lonIdx) { + free(_heatmapIToNDB[lonIdx]); + } + free(_heatmapIToNDB); + + _heatmapIToNDB = (double **)NULL; + _heatmapNumPtsLon = 0; + _heatmapNumPtsLat = 0; + } + + if (_nearFieldAdjFlag) { + delete _nfa; + _nfa = (NFAClass *)NULL; + } + + if (_passiveRepeaterFlag) { + delete _prTable; + _prTable = (PRTABLEClass *)NULL; + } + + for (int regionIdx = 0; regionIdx < _numRegion; ++regionIdx) { + delete _regionPolygonList[regionIdx]; + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::clearULSList ****/ +/******************************************************************************************/ +void AfcManager::clearULSList() +{ + int ulsIdx; + ULSClass *uls; + + for (ulsIdx = 0; ulsIdx <= _ulsList->getSize() - 1; ulsIdx++) { + uls = (*_ulsList)[ulsIdx]; + delete uls; + } + _ulsList->reset(); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::importGUIjson ****/ +/******************************************************************************************/ +void AfcManager::importGUIjson(const std::string &inputJSONpath) +{ + // Read input parameters from GUI in JSON file + QJsonDocument jsonDoc; + // Use SearchPaths::forReading("data", ..., true) to ensure that the input file exists + // before reading it in + QString fName = QString(inputJSONpath.c_str()); + QByteArray data; + + if (AfcManager::_dataIf->readFile(fName, data)) { + jsonDoc = QJsonDocument::fromJson(data); + } else { + throw std::runtime_error("AfcManager::importGUIjson(): Failed to open JSON file " + "specifying user's input parameters."); + } + + // Print entirety of imported JSON file to debug log + LOGGER_DEBUG(logger) << "Contents of imported JSON file: " << std::endl + << jsonDoc.toJson().toStdString() << std::endl; + + // Read JSON from file + QJsonObject jsonObj = jsonDoc.object(); + + if (jsonObj.contains("version") && !jsonObj["version"].isUndefined()) { + _guiJsonVersion = jsonObj["version"].toString(); + } else { + _guiJsonVersion = QString("1.4"); + } + + if (_guiJsonVersion == "1.4") { + importGUIjsonVersion1_4(jsonObj); + } else { + LOGGER_WARN(logger) << "VERSION NOT SUPPORTED: GUI JSON FILE \"" << inputJSONpath + << "\": version: " << _guiJsonVersion; + _responseCode = CConst::versionNotSupportedResponseCode; + return; + } + + return; +} + +void AfcManager::importGUIjsonVersion1_4(const QJsonObject &jsonObj) +{ + QString errMsg; + + if ((_analysisType == "AP-AFC") || (_analysisType == "ScanAnalysis") || + (_analysisType == "test_itm") || (_analysisType == "HeatmapAnalysis")) { + QStringList requiredParams; + QStringList optionalParams; + + /**********************************************************************/ + /* AvailableSpectrumInquiryRequestMessage Object (Table 5) */ + /**********************************************************************/ + requiredParams = QStringList() << "availableSpectrumInquiryRequests" + << "version"; + optionalParams = QStringList() << "vendorExtensions"; + for (auto &key : jsonObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase(optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + + QJsonObject requestObj; + if (!requiredParams.contains("availableSpectrumInquiryRequests")) { + QJsonArray requestArray = + jsonObj["availableSpectrumInquiryRequests"].toArray(); + if (requestArray.size() != 1) { + LOGGER_WARN(logger) << "GENERAL FAILURE: afc-engine only processes " + "a single request, " + << requestArray.size() << " requests specified"; + _responseCode = CConst::generalFailureResponseCode; + return; + } + requestObj = requestArray.at(0).toObject(); + } + /**********************************************************************/ + + /**********************************************************************/ + /* AvailableSpectrumInquiryRequest Object (Table 6) */ + /**********************************************************************/ + requiredParams = QStringList() << "requestId" + << "deviceDescriptor" + << "location"; + optionalParams = QStringList() << "inquiredFrequencyRange" + << "inquiredChannels" + << "minDesiredPower" + << "vendorExtensions"; + for (auto &key : requestObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase(optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + + /* The requestId is required for any response so keep it */ + if (!requiredParams.contains("requestId")) { + _requestId = requestObj["requestId"].toString(); + } + + QJsonObject deviceDescriptorObj; + if (!requiredParams.contains("deviceDescriptor")) { + deviceDescriptorObj = requestObj["deviceDescriptor"].toObject(); + } + + QJsonObject locationObj; + if (!requiredParams.contains("location")) { + locationObj = requestObj["location"].toObject(); + } + + QJsonArray inquiredFrequencyRangeArray; + if (!optionalParams.contains("inquiredFrequencyRange")) { + inquiredFrequencyRangeArray = + requestObj["inquiredFrequencyRange"].toArray(); + } + + QJsonArray inquiredChannelsArray; + if (!optionalParams.contains("inquiredChannels")) { + inquiredChannelsArray = requestObj["inquiredChannels"].toArray(); + } + + double minDesiredPower; + if (!optionalParams.contains("minDesiredPower")) { + minDesiredPower = requestObj["minDesiredPower"].toDouble(); + } else { + minDesiredPower = quietNaN; + } + + QJsonArray vendorExtensionArray; + if (!optionalParams.contains("vendorExtensions")) { + vendorExtensionArray = requestObj["vendorExtensions"].toArray(); + } + + /**********************************************************************/ + + /**********************************************************************/ + /* DeviceDescriptor Object (Table 7) */ + /**********************************************************************/ + requiredParams = QStringList() << "serialNumber" + << "certificationId"; + optionalParams = QStringList(); + for (auto &key : deviceDescriptorObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase(optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + + QJsonArray certificationIdArray; + if (!requiredParams.contains("certificationId")) { + certificationIdArray = deviceDescriptorObj["certificationId"].toArray(); + } + + if (!requiredParams.contains("serialNumber")) { + _serialNumber = deviceDescriptorObj["serialNumber"].toString(); + } + + /**********************************************************************/ + + /**********************************************************************/ + /* CertificationID Object (Table 8) */ + /**********************************************************************/ + for (auto certificationIDVal : certificationIdArray) { + auto certificationIDObj = certificationIDVal.toObject(); + requiredParams = QStringList() << "rulesetId" + << "id"; + optionalParams = QStringList(); + for (auto &key : certificationIDObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase(optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + if (!requiredParams.contains("rulesetId")) { + _rulesetId = certificationIdArray.at(0)["rulesetId"].toString(); + } + } + /**********************************************************************/ + + /**********************************************************************/ + /* Location Object (Table 9) */ + /**********************************************************************/ + requiredParams = QStringList() << "elevation"; + optionalParams = QStringList() << "ellipse" + << "linearPolygon" + << "radialPolygon" + << "indoorDeployment"; + for (auto &key : locationObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase(optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + + int hasIndoorDeploymentFlag = (optionalParams.contains("indoorDeployment") ? 0 : 1); + + int hasEllipseFlag = (optionalParams.contains("ellipse") ? 0 : 1); + int hasLinearPolygonFlag = (optionalParams.contains("linearPolygon") ? 0 : 1); + int hasRadialPolygonFlag = (optionalParams.contains("radialPolygon") ? 0 : 1); + + int n = hasEllipseFlag + hasLinearPolygonFlag + hasRadialPolygonFlag; + + if (n != 1) { + LOGGER_WARN(logger) + << "GENERAL FAILURE: location object must contain exactly one " + "instance of ellipse, linearPolygon, or radialPolygon, total of " + << n << " instances found"; + _responseCode = CConst::generalFailureResponseCode; + return; + } + + QJsonObject ellipseObj; + if (hasEllipseFlag) { + ellipseObj = locationObj["ellipse"].toObject(); + } + + QJsonObject linearPolygonObj; + if (hasLinearPolygonFlag) { + linearPolygonObj = locationObj["linearPolygon"].toObject(); + } + + QJsonObject radialPolygonObj; + if (hasRadialPolygonFlag) { + radialPolygonObj = locationObj["radialPolygon"].toObject(); + } + + QJsonObject elevationObj; + if (!requiredParams.contains("elevation")) { + elevationObj = locationObj["elevation"].toObject(); + } + /**********************************************************************/ + + /**********************************************************************/ + /* Ellipse Object (Table 10) */ + /**********************************************************************/ + bool hasCenterFlag = false; + QJsonObject centerObj; + if (hasEllipseFlag) { + requiredParams = QStringList() << "center" + << "majorAxis" + << "minorAxis" + << "orientation"; + optionalParams = QStringList(); + for (auto &key : ellipseObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase(optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + + if (!requiredParams.contains("center")) { + centerObj = ellipseObj["center"].toObject(); + hasCenterFlag = true; + } + } + /**********************************************************************/ + + /**********************************************************************/ + /* LinearPolygon Object (Table 11) */ + /**********************************************************************/ + bool hasOuterBoundaryPointArrayFlag = false; + QJsonArray outerBoundaryPointArray; + if (hasLinearPolygonFlag) { + requiredParams = QStringList() << "outerBoundary"; + optionalParams = QStringList(); + for (auto &key : linearPolygonObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase(optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + + if (!requiredParams.contains("outerBoundary")) { + outerBoundaryPointArray = + linearPolygonObj["outerBoundary"].toArray(); + hasOuterBoundaryPointArrayFlag = true; + } + } + /**********************************************************************/ + + /**********************************************************************/ + /* RadialPolygon Object (Table 12) */ + /**********************************************************************/ + bool hasOuterBoundaryVectorArrayFlag = false; + QJsonArray outerBoundaryVectorArray; + if (hasRadialPolygonFlag) { + requiredParams = QStringList() << "center" + << "outerBoundary"; + optionalParams = QStringList(); + for (auto &key : radialPolygonObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase(optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + + if (!requiredParams.contains("center")) { + centerObj = radialPolygonObj["center"].toObject(); + hasCenterFlag = true; + } + + if (!requiredParams.contains("outerBoundary")) { + outerBoundaryVectorArray = + radialPolygonObj["outerBoundary"].toArray(); + hasOuterBoundaryVectorArrayFlag = true; + } + } + /**********************************************************************/ + + /**********************************************************************/ + /* ElevationPolygon Object (Table 13) */ + /**********************************************************************/ + requiredParams = QStringList() << "height" + << "heightType" + << "verticalUncertainty"; + optionalParams = QStringList(); + for (auto &key : elevationObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase(optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + /**********************************************************************/ + + /**********************************************************************/ + /* Point Object (Table 14) */ + /**********************************************************************/ + if (hasCenterFlag) { + requiredParams = QStringList() << "longitude" + << "latitude"; + optionalParams = QStringList(); + for (auto &key : centerObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase(optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + } + + if (hasOuterBoundaryPointArrayFlag) { + for (auto outerBoundaryPointVal : outerBoundaryPointArray) { + auto outerBoundaryPointObj = outerBoundaryPointVal.toObject(); + requiredParams = QStringList() << "longitude" + << "latitude"; + optionalParams = QStringList(); + for (auto &key : outerBoundaryPointObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase( + optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + } + } + /**********************************************************************/ + + /**********************************************************************/ + /* Vector Object (Table 15) */ + /**********************************************************************/ + if (hasOuterBoundaryVectorArrayFlag) { + for (auto outerBoundaryVectorVal : outerBoundaryVectorArray) { + auto outerBoundaryVectorObj = outerBoundaryVectorVal.toObject(); + requiredParams = QStringList() << "length" + << "angle"; + optionalParams = QStringList(); + for (auto &key : outerBoundaryVectorObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase( + optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + } + } + /**********************************************************************/ + + /**********************************************************************/ + /* FrequencyRange Object (Table 16) */ + /**********************************************************************/ + for (auto inquiredFrequencyRangeVal : inquiredFrequencyRangeArray) { + auto inquiredFrequencyRangeObj = inquiredFrequencyRangeVal.toObject(); + requiredParams = QStringList() << "lowFrequency" + << "highFrequency"; + optionalParams = QStringList(); + for (auto &key : inquiredFrequencyRangeObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase(optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + } + /**********************************************************************/ + + /**********************************************************************/ + /* Channels Object (Table 17) */ + /**********************************************************************/ + for (auto inquiredChannelsVal : inquiredChannelsArray) { + auto inquiredChannelsObj = inquiredChannelsVal.toObject(); + requiredParams = QStringList() << "globalOperatingClass"; + optionalParams = QStringList() << "channelCfi"; + for (auto &key : inquiredChannelsObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase(optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + } + /**********************************************************************/ + + /**********************************************************************/ + /* VendorExtension Object (Table 23) */ + /**********************************************************************/ + for (auto vendorExtensionVal : vendorExtensionArray) { + auto vendorExtensionObj = vendorExtensionVal.toObject(); + requiredParams = QStringList() << "extensionId" + << "parameters"; + optionalParams = QStringList(); + for (auto &key : vendorExtensionObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase(requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase(optionalParams.begin() + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + } + /**********************************************************************/ + + if (_missingParams.size()) { + _responseCode = CConst::missingParamResponseCode; + return; + } else if (_unexpectedParams.size()) { + _responseCode = CConst::unexpectedParamResponseCode; + return; + } + + /**********************************************************************/ + /* Extract values */ + /**********************************************************************/ + if (certificationIdArray.size() == 0) { + _responseCode = CConst::invalidValueResponseCode; + _invalidParams << "certificationId"; + return; + } + + if (hasIndoorDeploymentFlag) { + int indoorDeploymentVal = locationObj["indoorDeployment"].toInt(); + switch (indoorDeploymentVal) { + case 0: /* Unknown */ + if (_certifiedIndoor) { + _rlanType = RLANType::RLAN_INDOOR; + } else { + _rlanType = RLANType::RLAN_OUTDOOR; + } + break; + case 1: /* Indoor */ + if (_rulesetId == + QString::fromStdString("US_47_CFR_PART_15_SUBPART_E")) { + /* US */ + if (_certifiedIndoor) { + _rlanType = RLANType::RLAN_INDOOR; + } else { + LOGGER_INFO(logger) + << _serialNumber.toStdString() + << " indicated as deployed indoor " + << "but is not certified as " + "Indoor. " + << "So it is analyzed as outdoor"; + + _rlanType = RLANType::RLAN_OUTDOOR; + } + } else { + /* Everywhere else including Canada */ + _rlanType = RLANType::RLAN_INDOOR; + } + break; + case 2: + _rlanType = RLANType::RLAN_OUTDOOR; + break; + default: + _invalidParams << "indoorDeployment"; + } + } else { /* not specified, treated as unknown */ + if (_certifiedIndoor) { + _rlanType = RLANType::RLAN_INDOOR; + } else { + _rlanType = RLANType::RLAN_OUTDOOR; + } + } + + if (_rlanType == RLANType::RLAN_INDOOR) { + _bodyLossDB = _bodyLossIndoorDB; + if ((!std::isnan(minDesiredPower)) && + (minDesiredPower > _minEIRPIndoor_dBm)) { + _minEIRP_dBm = minDesiredPower; + } else { + _minEIRP_dBm = _minEIRPIndoor_dBm; + } + } else { + _buildingType = CConst::noBuildingType; + _confidenceBldg2109 = 0.0; + _fixedBuildingLossFlag = false; + _fixedBuildingLossValue = 0.0; + _bodyLossDB = _bodyLossOutdoorDB; + if ((!std::isnan(minDesiredPower)) && + (minDesiredPower > _minEIRPOutdoor_dBm)) { + _minEIRP_dBm = minDesiredPower; + } else { + _minEIRP_dBm = _minEIRPOutdoor_dBm; + } + } + + QString rlanHeightType = elevationObj["heightType"].toString(); + + if (rlanHeightType == "AMSL") { + _rlanHeightType = CConst::AMSLHeightType; + } else if (rlanHeightType == "AGL") { + _rlanHeightType = CConst::AGLHeightType; + } else { + _invalidParams << "heightType"; + } + + double verticalUncertainty = elevationObj["verticalUncertainty"].toDouble(); + if (verticalUncertainty < 0.0) { + _invalidParams << "verticalUncertainty"; + } else if (verticalUncertainty > _maxVerticalUncertainty) { + LOGGER_WARN(logger) + << "GENERAL FAILURE: verticalUncertainty = " << verticalUncertainty + << " exceeds max value of " << _maxVerticalUncertainty; + _invalidParams << "verticalUncertainty"; + } + double centerHeight = elevationObj["height"].toDouble(); + + if (hasEllipseFlag) { + _rlanUncertaintyRegionType = RLANBoundary::ELLIPSE; + double centerLatitude = centerObj["latitude"].toDouble(); + double centerLongitude = centerObj["longitude"].toDouble(); + + double minorAxis = ellipseObj["minorAxis"].toDouble(); + double majorAxis = ellipseObj["majorAxis"].toDouble(); + + double orientation = ellipseObj["orientation"].toDouble(); + + if ((centerLatitude < -90.0) || (centerLatitude > 90.0)) { + _invalidParams << "latitude"; + } + if ((centerLongitude < -180.0) || (centerLongitude > 180.0)) { + _invalidParams << "longitude"; + } + + if (majorAxis < minorAxis) { + _invalidParams << "minorAxis"; + _invalidParams << "majorAxis"; + } else { + if (minorAxis < 0.0) { + _invalidParams << "minorAxis"; + } + if (majorAxis < 0.0) { + LOGGER_WARN(logger) << "GENERAL FAILURE: majorAxis < 0"; + _invalidParams << "majorAxis"; + } else if (2 * majorAxis > _maxHorizontalUncertaintyDistance) { + LOGGER_WARN(logger) + << "GENERAL FAILURE: 2*majorAxis = " + << 2 * majorAxis << " exceeds max value of " + << _maxHorizontalUncertaintyDistance; + _invalidParams << "majorAxis"; + } + } + + if ((orientation < 0.0) || (orientation > 180.0)) { + _invalidParams << "orientation"; + } + + _rlanLLA = std::make_tuple(centerLatitude, centerLongitude, centerHeight); + _rlanUncerts_m = std::make_tuple(minorAxis, majorAxis, verticalUncertainty); + _rlanOrientation_deg = orientation; + } else if (hasLinearPolygonFlag) { + _rlanUncertaintyRegionType = RLANBoundary::LINEAR_POLY; + + if ((outerBoundaryPointArray.size() < 3) || + (outerBoundaryPointArray.size() > 15)) { + _invalidParams << "outerBoundary"; + } + + for (auto outerBoundaryPointVal : outerBoundaryPointArray) { + auto outerBoundaryPointObj = outerBoundaryPointVal.toObject(); + double latitude = outerBoundaryPointObj["latitude"].toDouble(); + double longitude = outerBoundaryPointObj["longitude"].toDouble(); + + if ((latitude < -90.0) || (latitude > 90.0)) { + _invalidParams << "latitude"; + } + if ((longitude < -180.0) || (longitude > 180.0)) { + _invalidParams << "longitude"; + } + + _rlanLinearPolygon.push_back(std::make_pair(latitude, longitude)); + } + + double centerLongitude; + double centerLatitude; + + // Average LON/LAT of vertices + double sumLon = 0.0; + double sumLat = 0.0; + int i; + for (i = 0; i < (int)_rlanLinearPolygon.size(); i++) { + sumLon += _rlanLinearPolygon[i].second; + sumLat += _rlanLinearPolygon[i].first; + } + centerLongitude = sumLon / _rlanLinearPolygon.size(); + centerLatitude = sumLat / _rlanLinearPolygon.size(); + + double cosLat = cos(centerLatitude * M_PI / 180.0); + double maxHorizDistSq = 0.0; + for (i = 0; i < (int)_rlanLinearPolygon.size(); i++) { + double lon_i = _rlanLinearPolygon[i].second; + double lat_i = _rlanLinearPolygon[i].first; + for (int j = i + 1; j < (int)_rlanLinearPolygon.size() + 1; j++) { + double jj = j % _rlanLinearPolygon.size(); + double lon_j = _rlanLinearPolygon[jj].second; + double lat_j = _rlanLinearPolygon[jj].first; + double deltaX = (lon_j - lon_i) * (M_PI / 180.0) * cosLat * + CConst::earthRadius; + double deltaY = (lat_j - lat_i) * (M_PI / 180.0) * + CConst::earthRadius; + double distSq = deltaX * deltaX + deltaY * deltaY; + if (distSq > maxHorizDistSq) { + maxHorizDistSq = distSq; + } + } + } + + if (maxHorizDistSq > + _maxHorizontalUncertaintyDistance * _maxHorizontalUncertaintyDistance) { + LOGGER_WARN(logger) + << "GENERAL FAILURE: Linear polygon contains vertices with " + "separation distance that exceeds max value of " + << _maxHorizontalUncertaintyDistance; + _invalidParams << "outerBoundary"; + } + + _rlanLLA = std::make_tuple(centerLatitude, centerLongitude, centerHeight); + _rlanUncerts_m = std::make_tuple(quietNaN, quietNaN, verticalUncertainty); + } else if (hasRadialPolygonFlag) { + _rlanUncertaintyRegionType = RLANBoundary::RADIAL_POLY; + + if ((outerBoundaryVectorArray.size() < 3) || + (outerBoundaryVectorArray.size() > 15)) { + _invalidParams << "outerBoundary"; + } + + for (auto outerBoundaryVectorVal : outerBoundaryVectorArray) { + auto outerBoundaryVectorObj = outerBoundaryVectorVal.toObject(); + double angle = outerBoundaryVectorObj["angle"].toDouble(); + double length = outerBoundaryVectorObj["length"].toDouble(); + + if (length < 0.0) { + _invalidParams << "length"; + } + if ((angle < 0.0) || (angle > 360.0)) { + _invalidParams << "angle"; + } + + _rlanRadialPolygon.push_back(std::make_pair(angle, length)); + } + + double centerLatitude = centerObj["latitude"].toDouble(); + double centerLongitude = centerObj["longitude"].toDouble(); + + double maxHorizDistSq = 0.0; + for (int i = 0; i < (int)_rlanRadialPolygon.size(); i++) { + double angle_i = _rlanRadialPolygon[i].second; + double length_i = _rlanRadialPolygon[i].first; + double x_i = length_i * sin(angle_i * M_PI / 360.0); + double y_i = length_i * cos(angle_i * M_PI / 360.0); + for (int j = i + 1; j < (int)_rlanRadialPolygon.size() + 1; j++) { + double jj = j % _rlanRadialPolygon.size(); + double angle_j = _rlanRadialPolygon[jj].second; + double length_j = _rlanRadialPolygon[jj].first; + double x_j = length_j * sin(angle_j * M_PI / 360.0); + double y_j = length_j * cos(angle_j * M_PI / 360.0); + + double deltaX = x_j - x_i; + double deltaY = y_j - y_i; + double distSq = deltaX * deltaX + deltaY * deltaY; + if (distSq > maxHorizDistSq) { + maxHorizDistSq = distSq; + } + } + } + + if (maxHorizDistSq > + _maxHorizontalUncertaintyDistance * _maxHorizontalUncertaintyDistance) { + LOGGER_WARN(logger) + << "GENERAL FAILURE: Radial polygon contains vertices with " + "separation distance that exceeds max value of " + << _maxHorizontalUncertaintyDistance; + _invalidParams << "outerBoundary"; + } + + _rlanLLA = std::make_tuple(centerLatitude, centerLongitude, centerHeight); + _rlanUncerts_m = std::make_tuple(quietNaN, quietNaN, verticalUncertainty); + } + + for (auto inquiredChannelsVal : inquiredChannelsArray) { + auto inquiredChannelsObj = inquiredChannelsVal.toObject(); + auto chanClass = std::make_pair>( + inquiredChannelsObj["globalOperatingClass"].toInt(), + std::vector()); + if (inquiredChannelsObj.contains("channelCfi")) { + for (const QJsonValue &chanIdx : + inquiredChannelsObj["channelCfi"].toArray()) + chanClass.second.push_back(chanIdx.toInt()); + } + // TODO: are we handling the case where they want all? + + _inquiredChannels.push_back(chanClass); + } + + for (auto inquiredFrequencyRangeVal : inquiredFrequencyRangeArray) { + auto inquiredFrequencyRangeObj = inquiredFrequencyRangeVal.toObject(); + _inquiredFrequencyRangesMHz.push_back( + std::make_pair(inquiredFrequencyRangeObj["lowFrequency"].toInt(), + inquiredFrequencyRangeObj["highFrequency"].toInt())); + } + + bool hasRLANAntenna = false; + for (auto vendorExtensionVal : vendorExtensionArray) { + auto vendorExtensionObj = vendorExtensionVal.toObject(); + auto parametersObj = vendorExtensionObj["parameters"].toObject(); + if (parametersObj.contains("type") && + !parametersObj["type"].isUndefined()) { + std::string type = parametersObj["type"].toString().toStdString(); + if (type == "rlanAntenna") { + if (hasRLANAntenna) { + LOGGER_WARN(logger) << "GENERAL FAILURE: multiple " + "RLAN antennas specified"; + _responseCode = CConst::generalFailureResponseCode; + return; + } + std::string antennaName = parametersObj["antennaModel"] + .toString() + .toStdString(); + _rlanAzimuthPointing = + parametersObj["azimuthPointing"].toDouble(); + _rlanElevationPointing = + parametersObj["elevationPointing"].toDouble(); + int numAntennaAOB = parametersObj["numAOB"].toInt(); + QJsonArray gainArray; + gainArray = parametersObj["discriminationGainDB"].toArray(); + + if (gainArray.size() != numAntennaAOB) { + LOGGER_WARN(logger) + << "GENERAL FAILURE: numAntennaAOB = " + << numAntennaAOB + << " discriminationGainDB has " + << gainArray.size() << " elements"; + _responseCode = CConst::generalFailureResponseCode; + return; + } + + std::vector> sampledData; + std::tuple pt; + for (int aobIdx = 0; aobIdx < numAntennaAOB; ++aobIdx) { + std::get<0>(pt) = (((double)aobIdx) / + (numAntennaAOB - 1)) * + M_PI; + std::get<1>(pt) = gainArray.at(aobIdx).toDouble(); + sampledData.push_back(pt); + } + _rlanAntenna = + new AntennaClass(CConst::antennaLUT_Boresight, + antennaName.c_str()); + LinInterpClass *gainTable = new LinInterpClass(sampledData); + _rlanAntenna->setBoresightGainTable(gainTable); + + hasRLANAntenna = true; + } else if (type == "heatmap") { + requiredParams = QStringList() << "IndoorOutdoorStr" + << "MinLon" + << "MaxLon" + << "MinLat" + << "MaxLat" + << "RLANSpacing" + << "inquiredChannel" + << "analysis" + << "type" + << "fsIdType"; + optionalParams = QStringList() + << "RLANOutdoorEIRPDBm" + << "RLANOutdoorHeight" + << "RLANOutdoorHeightType" + << "RLANOutdoorHeightUncertainty" + << "RLANIndoorEIRPDBm" + << "RLANIndoorHeight" + << "RLANIndoorHeightType" + << "RLANIndoorHeightUncertainty" + << "fsId"; + + for (auto &key : parametersObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase( + requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase( + optionalParams.begin() + + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + + if (_missingParams.size()) { + _responseCode = CConst::missingParamResponseCode; + return; + } else if (_unexpectedParams.size()) { + _responseCode = CConst::unexpectedParamResponseCode; + return; + } + + _heatmapMinLon = parametersObj["MinLon"].toDouble(); + _heatmapMaxLon = parametersObj["MaxLon"].toDouble(); + _heatmapMinLat = parametersObj["MinLat"].toDouble(); + _heatmapMaxLat = parametersObj["MaxLat"].toDouble(); + _heatmapRLANSpacing = + parametersObj["RLANSpacing"].toDouble(); + _heatmapIndoorOutdoorStr = parametersObj["IndoorOutdoorStr"] + .toString() + .toStdString(); + + bool outdoorFlag = false; + bool indoorFlag = false; + if (_heatmapIndoorOutdoorStr == "Outdoor") { + outdoorFlag = true; + } else if (_heatmapIndoorOutdoorStr == "Indoor") { + indoorFlag = true; + } else if (_heatmapIndoorOutdoorStr == "Database") { + outdoorFlag = true; + indoorFlag = true; + } else { + LOGGER_WARN(logger) << "GENERAL FAILURE: heatmap " + "IndoorOutdoorStr = " + << _heatmapIndoorOutdoorStr + << " illegal value"; + _responseCode = CConst::generalFailureResponseCode; + return; + } + + _heatmapAnalysisStr = + parametersObj["analysis"].toString().toStdString(); + + bool itonFlag = false; + if (_heatmapAnalysisStr == "iton") { + itonFlag = true; + } else if (_heatmapAnalysisStr == "availability") { + itonFlag = false; + } else { + LOGGER_WARN(logger) + << "GENERAL FAILURE: heatmap analysis = " + << _heatmapAnalysisStr << " illegal value"; + _responseCode = CConst::generalFailureResponseCode; + return; + } + + if (indoorFlag) { + if (!optionalParams.contains("RLANIndoorHeightTyp" + "e")) { + _heatmapRLANIndoorHeightType = + parametersObj["RLANIndoorHeightTyp" + "e"] + .toString() + .toStdString(); + } else { + _missingParams << "RLANIndoorHeightType"; + } + if (!optionalParams.contains("RLANIndoorHeight")) { + _heatmapRLANIndoorHeight = + parametersObj["RLANIndoorHeight"] + .toDouble(); + } else { + LOGGER_WARN(logger) + << "GENERAL FAILURE: heatmap " + "RLANIndoorHeight not specified"; + _responseCode = + CConst::generalFailureResponseCode; + return; + } + if (!optionalParams.contains("RLANIndoorHeightUncer" + "tainty")) { + _heatmapRLANIndoorHeightUncertainty = + parametersObj["RLANIndoorHeightUnce" + "rtainty"] + .toDouble(); + } else { + LOGGER_WARN(logger) + << "GENERAL FAILURE: heatmap " + "RLANIndoorHeightUncertainty " + "not specified"; + _responseCode = + CConst::generalFailureResponseCode; + return; + } + + if (itonFlag) { + if (!optionalParams.contains("RLANIndoorEIR" + "PDBm")) { + _heatmapRLANIndoorEIRPDBm = + parametersObj["RLANIndoorEI" + "RPDBm"] + .toDouble(); + } else { + LOGGER_WARN(logger) + << "GENERAL FAILURE: " + "heatmap " + "RLANIndoorEIRPDBm not " + "specified"; + _responseCode = CConst:: + generalFailureResponseCode; + return; + } + } + } + + if (outdoorFlag) { + if (!optionalParams.contains("RLANOutdoorHeightTyp" + "e")) { + _heatmapRLANOutdoorHeightType = + parametersObj["RLANOutdoorHeightTyp" + "e"] + .toString() + .toStdString(); + } else { + LOGGER_WARN(logger) + << "GENERAL FAILURE: heatmap " + "RLANOutdoorHeightType not " + "specified"; + _responseCode = + CConst::generalFailureResponseCode; + return; + } + if (!optionalParams.contains("RLANOutdoorHeight")) { + _heatmapRLANOutdoorHeight = + parametersObj["RLANOutdoorHeight"] + .toDouble(); + } else { + LOGGER_WARN(logger) + << "GENERAL FAILURE: heatmap " + "RLANOutdoorHeight not " + "specified"; + _responseCode = + CConst::generalFailureResponseCode; + return; + } + if (!optionalParams.contains("RLANOutdoorHeightUnce" + "rtainty")) { + _heatmapRLANOutdoorHeightUncertainty = + parametersObj["RLANOutdoorHeightUnc" + "ertainty"] + .toDouble(); + } else { + LOGGER_WARN(logger) + << "GENERAL FAILURE: heatmap " + "RLANOutdoorHeightUncertainty " + "not specified"; + _responseCode = + CConst::generalFailureResponseCode; + return; + } + + if (itonFlag) { + if (!optionalParams.contains("RLANOutdoorEI" + "RPDBm")) { + _heatmapRLANOutdoorEIRPDBm = + parametersObj["RLANOutdoorE" + "IRPDBm"] + .toDouble(); + } else { + LOGGER_WARN(logger) + << "GENERAL FAILURE: " + "heatmap " + "RLANOutdoorEIRPDBm not " + "specified"; + _responseCode = CConst:: + generalFailureResponseCode; + return; + } + } + } + + std::string fsidTypeStr = + parametersObj["fsIdType"].toString().toStdString(); + + _heatmapFSID = -1; + if (fsidTypeStr == "All") { + } else if (fsidTypeStr == "Single") { + if (!optionalParams.contains("fsId")) { + _heatmapFSID = + parametersObj["fsId"].toInt(); + } else { + LOGGER_WARN(logger) + << "GENERAL FAILURE: heatmap fsId " + "not specified"; + _responseCode = + CConst::generalFailureResponseCode; + return; + } + } else { + LOGGER_WARN(logger) + << "GENERAL FAILURE: heatmap fsIdType = " + << fsidTypeStr << " illegal value"; + _responseCode = CConst::generalFailureResponseCode; + return; + } + + auto inquiredChannelObj = + parametersObj["inquiredChannel"].toObject(); + + requiredParams = QStringList() << "globalOperatingClass" + << "channelCfi"; + optionalParams = QStringList(); + + for (auto &key : inquiredChannelObj.keys()) { + int rIdx = requiredParams.indexOf(key); + if (rIdx != -1) { + requiredParams.erase( + requiredParams.begin() + rIdx); + } else { + int oIdx = optionalParams.indexOf(key); + if (oIdx != -1) { + optionalParams.erase( + optionalParams.begin() + + oIdx); + } else { + _unexpectedParams << key; + } + } + } + _missingParams << requiredParams; + + if (_missingParams.size()) { + _responseCode = CConst::missingParamResponseCode; + return; + } else if (_unexpectedParams.size()) { + _responseCode = CConst::unexpectedParamResponseCode; + return; + } + + _inquiredFrequencyRangesMHz.clear(); + _inquiredChannels.clear(); + + auto chanClass = std::make_pair>( + inquiredChannelObj["globalOperatingClass"].toInt(), + std::vector()); + chanClass.second.push_back( + inquiredChannelObj["channelCfi"].toInt()); + + _inquiredChannels.push_back(chanClass); + + _analysisType = "HeatmapAnalysis"; + + _mapDataGeoJsonFile = "mapData.json.gz"; + } + } + + std::string extensionId = + vendorExtensionObj["extensionId"].toString().toStdString(); + if (extensionId == "openAfc.overrideAfcConfig") { + _ulsDatabaseList.clear(); + std::string dbfile = + SearchPaths::forReading( + "data", + parametersObj["fsDatabaseFile"].toString(), + true) + .toStdString(); + _ulsDatabaseList.push_back(std::make_tuple("FSDATA", dbfile)); + } + } + + for (auto chanClass : _inquiredChannels) { + LOGGER_INFO(logger) + << (chanClass.second.empty() ? + "ALL" : + std::to_string(chanClass.second.size())) + << " channels requested in operating class " << chanClass.first; + } + LOGGER_INFO(logger) << _inquiredChannels.size() << " operating class(es) requested"; + LOGGER_INFO(logger) + << _inquiredFrequencyRangesMHz.size() << " frequency range(s) requested"; + + if (_inquiredChannels.size() + _inquiredFrequencyRangesMHz.size() == 0) { + LOGGER_WARN(logger) << "GENERAL FAILURE: must specify either " + "inquiredChannels or inquiredFrequencies"; + _responseCode = CConst::generalFailureResponseCode; + return; + } + + if (_invalidParams.size()) { + _responseCode = CConst::invalidValueResponseCode; + } +#if DEBUG_AFC + } else if (_analysisType == "test_winner2") { + // Do nothing +#endif + } else { + throw std::runtime_error(QString("Invalid analysis type for version 1.1: %1") + .arg(QString::fromStdString(_analysisType)) + .toStdString()); + } + + return; +} + +// Support command line interface with AFC Engine +void AfcManager::setCmdLineParams(std::string &inputFilePath, + std::string &configFilePath, + std::string &outputFilePath, + std::string &tempDir, + std::string &logLevel, + int argc, + char **argv) +{ + // Declare the supported options + po::options_description optDescript {"Allowed options"}; + // Create command line options + optDescript.add_options()("help,h", + "Use input-file-path, config-file-path, or output-file-path.")( + "request-type,r", + po::value()->default_value("AP-AFC"), + "set request-type (AP-AFC, HeatmapAnalysis, ExclusionZoneAnalysis)")( + "state-root,s", + po::value()->default_value("/var/lib/fbrat"), + "set fbrat state root directory")("mnt-path,s", + po::value()->default_value("/mnt/" + "nfs"), + "set share with GeoData and config data")( + "input-file-path,i", + po::value()->default_value("inputFile.json"), + "set input-file-path level")("config-file-path,c", + po::value()->default_value("configFile." + "json"), + "set config-file-path level")( + "output-file-path,o", + po::value()->default_value("outputFile.json"), + "set output-file-path level")("temp-dir,t", + po::value()->default_value(""), + "set temp-dir level")( + "log-level,l", + po::value()->default_value("debug"), + "set log-level")("runtime_opt,u", + po::value()->default_value(3), + "bit 0: create 'fast' debug files; bit 1: create kmz and progress " + "files; bit 2: interpret file pathes as URLs; bit 4: create " + "'slow' debug files"); + + po::variables_map cmdLineArgs; + po::store(po::parse_command_line(argc, argv, optDescript), + cmdLineArgs); // ac and av are parameters passed into main + po::notify(cmdLineArgs); + + // Check whether "help" argument was specified + if (cmdLineArgs.count("help")) { + std::cout << optDescript << std::endl; + exit(0); // Terminate program, indicating it completed successfully + } + // Check whether "request-type(r)" was specified + if (cmdLineArgs.count("request-type")) { + _analysisType = cmdLineArgs["request-type"].as(); + } else // Must be specified + { + throw std::runtime_error("AfcManager::setCmdLineParams(): request-type(r) command " + "line argument was not set."); + } + // Check whether "state-root(s)" was specified + if (cmdLineArgs.count("state-root")) { + _stateRoot = cmdLineArgs["state-root"].as(); + } else // Must be specified + { + throw std::runtime_error("AfcManager::setCmdLineParams(): state-root(s) command " + "line argument was not set."); + } + // Check whether "mnt-path(s)" was specified + if (cmdLineArgs.count("mnt-path")) { + _mntPath = cmdLineArgs["mnt-path"].as(); + } else // Must be specified + { + throw std::runtime_error("AfcManager::setCmdLineParams(): mnt-path(s) command line " + "argument was not set."); + } + // Check whether "input-file-path(i)" was specified + if (cmdLineArgs.count("input-file-path")) { + inputFilePath = cmdLineArgs["input-file-path"].as(); + } else { + throw std::runtime_error("AfcManager::setCmdLineParams(): input-file-path(i) " + "command line argument was not set."); + } + // Check whether "config-file-path(c)" was specified + if (cmdLineArgs.count("config-file-path")) { // This is a work in progress + configFilePath = cmdLineArgs["config-file-path"].as(); + } else { + throw std::runtime_error("AfcManager::setCmdLineParams(): config-file-path(c) " + "command line argument was not set."); + } + // Check whether "output-file-path(o)" was specified + if (cmdLineArgs.count("output-file-path")) { + outputFilePath = cmdLineArgs["output-file-path"].as(); + } else { + throw std::runtime_error("AfcManager::setCmdLineParams(): output-file-path(o) " + "command line argument was not set."); + } + // Check whether "temp-dir(t)" was specified + if (cmdLineArgs.count("temp-dir")) { + tempDir = cmdLineArgs["temp-dir"].as(); + } else { + throw std::runtime_error("AfcManager::setCmdLineParams(): temp-dir command line " + "argument was not set."); + } + // Check whether "log-level(l)" was specified + if (cmdLineArgs.count("log-level")) { + logLevel = cmdLineArgs["log-level"].as(); + } else { + throw std::runtime_error("AfcManager::setCmdLineParams(): log-level command line " + "argument was not set."); + } + if (cmdLineArgs.count("runtime_opt")) { + uint32_t tmp = cmdLineArgs["runtime_opt"].as(); + if (tmp & RUNTIME_OPT_ENABLE_DBG) { + AfcManager::_createDebugFiles = true; + } + if (tmp & RUNTIME_OPT_ENABLE_GUI) { + AfcManager::_createKmz = true; + } + + if (tmp & RUNTIME_OPT_CERT_ID) { + AfcManager::_certifiedIndoor = true; + } + AfcManager::_dataIf = new AfcDataIf(tmp & RUNTIME_OPT_URL); + if (tmp & RUNTIME_OPT_ENABLE_SLOW_DBG) { + AfcManager::_createSlowDebugFiles = true; + } + } +} + +void AfcManager::importConfigAFCjson(const std::string &inputJSONpath, const std::string &tempDir) +{ + QString errMsg; + + if (_responseCode != CConst::successResponseCode) { + return; + } + + // read json file in + QJsonDocument jsonDoc; + QString fName = QString(inputJSONpath.c_str()); + QByteArray data; + if (AfcManager::_dataIf->readFile(fName, data)) { + jsonDoc = QJsonDocument::fromJson(data); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): Failed to open JSON " + "file specifying configuration parameters."); + } + + // Print raw contents of input JSON file + LOGGER_DEBUG(logger) << "Raw contents of input JSON file provided by the GUI: " + << jsonDoc.toJson().toStdString(); + + // Read JSON from file + QJsonObject jsonObj = jsonDoc.object(); + + /**********************************************************************/ + /* Setup Paths */ + /**********************************************************************/ + if (jsonObj.contains("globeDir") && !jsonObj["globeDir"].isUndefined()) { + _globeDir = SearchPaths::forReading("data", jsonObj["globeDir"].toString(), true) + .toStdString(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): globeDir is missing."); + } + + if (jsonObj.contains("srtmDir") && !jsonObj["srtmDir"].isUndefined()) { + _srtmDir = SearchPaths::forReading("data", jsonObj["srtmDir"].toString(), true) + .toStdString(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): srtmDir is missing."); + } + + if (jsonObj.contains("cdsmDir") && !jsonObj["cdsmDir"].isUndefined()) { + if (jsonObj["cdsmDir"].toString().isEmpty()) { + _cdsmDir = ""; + } else { + _cdsmDir = SearchPaths::forReading("data", + jsonObj["cdsmDir"].toString(), + true) + .toStdString(); + } + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): cdsmDir is missing."); + } + + if (jsonObj.contains("depDir") && !jsonObj["depDir"].isUndefined()) { + _depDir = SearchPaths::forReading("data", jsonObj["depDir"].toString(), true) + .toStdString(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): depDir is missing."); + } + + if (jsonObj.contains("lidarDir") && !jsonObj["lidarDir"].isUndefined()) { + if (jsonObj["lidarDir"].toString().isEmpty()) { + _lidarDir = ""; + } else { + _lidarDir = SearchPaths::forReading("data", + jsonObj["lidarDir"].toString(), + true) + .toStdString(); + } + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): lidarDir is missing."); + } + + if (jsonObj.contains("regionDir") && !jsonObj["regionDir"].isUndefined()) { + _regionDir = jsonObj["regionDir"].toString().toStdString(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): regionDir is " + "missing."); + } + + if (jsonObj.contains("worldPopulationFile") && + !jsonObj["worldPopulationFile"].isUndefined()) { + _worldPopulationFile = + SearchPaths::forReading("data", + jsonObj["worldPopulationFile"].toString(), + true) + .toStdString(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): worldPopulationFile " + "is missing."); + } + + if (jsonObj.contains("nlcdFile") && !jsonObj["nlcdFile"].isUndefined()) { + _nlcdFile = SearchPaths::forReading("data", jsonObj["nlcdFile"].toString(), true) + .toStdString(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): nlcdFile is missing."); + } + + if (jsonObj.contains("rainForestFile") && !jsonObj["rainForestFile"].isUndefined()) { + if (jsonObj["rainForestFile"].toString().isEmpty()) { + _rainForestFile = ""; + } else { + _rainForestFile = + SearchPaths::forReading("data", + jsonObj["rainForestFile"].toString(), + true) + .toStdString(); + } + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): rainForestFile is " + "missing."); + } + + if (jsonObj.contains("nfaTableFile") && !jsonObj["nfaTableFile"].isUndefined()) { + if (jsonObj["nfaTableFile"].toString().isEmpty()) { + _nfaTableFile = ""; + } else { + _nfaTableFile = SearchPaths::forReading("data", + jsonObj["nfaTableFile"].toString(), + true) + .toStdString(); + } + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): nfaTableFile is " + "missing."); + } + + if (jsonObj.contains("prTableFile") && !jsonObj["prTableFile"].isUndefined()) { + if (jsonObj["prTableFile"].toString().isEmpty()) { + _prTableFile = ""; + } else { + _prTableFile = SearchPaths::forReading("data", + jsonObj["prTableFile"].toString(), + true) + .toStdString(); + } + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): prTableFile is " + "missing."); + } + + if (jsonObj.contains("radioClimateFile") && !jsonObj["radioClimateFile"].isUndefined()) { + _radioClimateFile = SearchPaths::forReading("data", + jsonObj["radioClimateFile"].toString(), + true) + .toStdString(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): radioClimateFile is " + "missing."); + } + + if (jsonObj.contains("surfRefracFile") && !jsonObj["surfRefracFile"].isUndefined()) { + _surfRefracFile = SearchPaths::forReading("data", + jsonObj["surfRefracFile"].toString(), + true) + .toStdString(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): surfRefracFile is " + "missing."); + } + + if (jsonObj.contains("fsDatabaseFileList") && + !jsonObj["fsDatabaseFileList"].isUndefined()) { + QJsonArray fsDatabaseFileArray = jsonObj["fsDatabaseFileList"].toArray(); + for (QJsonValue fsDatabaseFileVal : fsDatabaseFileArray) { + QJsonObject fsDatabaseFileObj = fsDatabaseFileVal.toObject(); + std::string name = fsDatabaseFileObj["name"].toString().toStdString(); + std::string dbfile = SearchPaths::forReading( + "data", + fsDatabaseFileObj["fsDatabaseFile"].toString(), + true) + .toStdString(); + _ulsDatabaseList.push_back(std::make_tuple(name, dbfile)); + } + } else if (jsonObj.contains("fsDatabaseFile") && !jsonObj["fsDatabaseFile"].isUndefined()) { + std::string dbfile = SearchPaths::forReading("data", + jsonObj["fsDatabaseFile"].toString(), + true) + .toStdString(); + _ulsDatabaseList.push_back(std::make_tuple("FSDATA", dbfile)); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): fsDatabaseFile not " + "specified."); + } + + /**********************************************************************/ + + /**********************************************************************/ + /* Create _allowableFreqBandList */ + /**********************************************************************/ + QJsonArray freqBandArray = jsonObj["freqBands"].toArray(); + for (QJsonValue freqBandVal : freqBandArray) { + QJsonObject freqBandObj = freqBandVal.toObject(); + std::string name = freqBandObj["name"].toString().toStdString(); + int startFreqMHz = freqBandObj["startFreqMHz"].toInt(); + int stopFreqMHz = freqBandObj["stopFreqMHz"].toInt(); + + if (stopFreqMHz <= startFreqMHz) { + errMsg = QString("ERROR: Freq Band %1 Invalid, startFreqMHz = %2, " + "stopFreqMHz = %3. Require startFreqMHz < stopFreqMHz") + .arg(QString::fromStdString(name)) + .arg(startFreqMHz) + .arg(stopFreqMHz); + throw std::runtime_error(errMsg.toStdString()); + } else if ((stopFreqMHz <= _wlanMinFreqMHz) || (startFreqMHz >= _wlanMaxFreqMHz)) { + errMsg = QString("ERROR: Freq Band %1 Invalid, startFreqMHz = %2, " + "stopFreqMHz = %3. Has no overlap with band [%4,%5].") + .arg(QString::fromStdString(name)) + .arg(startFreqMHz) + .arg(stopFreqMHz) + .arg(_wlanMinFreqMHz) + .arg(_wlanMaxFreqMHz); + throw std::runtime_error(errMsg.toStdString()); + } + + _allowableFreqBandList.push_back(FreqBandClass(name, startFreqMHz, stopFreqMHz)); + } + if (!_allowableFreqBandList.size()) { + throw std::runtime_error("AfcManager::importConfigAFCjson(): ERROR: no allowable " + "frequency bands defined."); + } + /**********************************************************************/ + + // These are created for ease of copying + QJsonObject antenna = jsonObj["antenna"].toObject(); + QJsonObject buildingLoss = jsonObj["buildingPenetrationLoss"].toObject(); + QJsonObject propModel = jsonObj["propagationModel"].toObject(); + + // Input parameters stored in the AfcManager object + if (jsonObj.contains("regionStr") && !jsonObj["regionStr"].isUndefined()) { + _regionStr = jsonObj["regionStr"].toString().toStdString(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): regionStr is " + "missing."); + } + + _regionPolygonFileList = SearchPaths::forReading("data", + QString::fromStdString(_regionDir + "/" + + _regionStr) + + ".kml", + true) + .toStdString(); + + if (jsonObj.contains("cdsmLOSThr") && !jsonObj["cdsmLOSThr"].isUndefined()) { + _cdsmLOSThr = jsonObj["cdsmLOSThr"].toDouble(); + } else { + _cdsmLOSThr = 0.5; + } + + if (jsonObj.contains("fsAnalysisListFile") && + !jsonObj["fsAnalysisListFile"].isUndefined()) { + _fsAnalysisListFile = QDir(QString::fromStdString(tempDir)) + .filePath(jsonObj["fsAnalysisListFile"].toString()) + .toStdString(); + } else { + _fsAnalysisListFile = QDir(QString::fromStdString(tempDir)) + .filePath("fs_analysis_list.csv") + .toStdString(); + } + + // *********************************** + // If this flag is set, indoor rlan's have a fixed AMSL height over the uncertainty region + // (with no height uncertainty). By default, this flag is false in which case the AGL height + // is fixed over the uncertainty region (with no height uncertainty). + // *********************************** + if (jsonObj.contains("indoorFixedHeightAMSL") && + !jsonObj["indoorFixedHeightAMSL"].isUndefined()) { + _indoorFixedHeightAMSL = jsonObj["indoorFixedHeightAMSL"].toBool(); + } else { + _indoorFixedHeightAMSL = false; + } + + // *********************************** + // + // *********************************** + if (jsonObj.contains("scanPointBelowGroundMethod") && + !jsonObj["scanPointBelowGroundMethod"].isUndefined()) { + std::string scanPointBelowGroundMethodStr = + jsonObj["scanPointBelowGroundMethod"].toString().toStdString(); + if (scanPointBelowGroundMethodStr == "discard") { + _scanPointBelowGroundMethod = CConst::DiscardScanPointBelowGroundMethod; + } else if (scanPointBelowGroundMethodStr == "truncate") { + _scanPointBelowGroundMethod = CConst::TruncateScanPointBelowGroundMethod; + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): Invalid " + "scanPointBelowGroundMethod specified."); + } + } else { + _scanPointBelowGroundMethod = CConst::TruncateScanPointBelowGroundMethod; + } + + // ITM Parameters + _itmEpsDielect = jsonObj["ITMParameters"].toObject()["dielectricConst"].toDouble(); + _itmSgmConductivity = jsonObj["ITMParameters"].toObject()["conductivity"].toDouble(); + _itmPolarization = + jsonObj["ITMParameters"].toObject()["polarization"].toString().toStdString() == + "Vertical"; + + // *********************************** + // More ITM parameters + // *********************************** + _itmMinSpacing = jsonObj["ITMParameters"].toObject()["minSpacing"].toDouble(); + _itmMaxNumPts = jsonObj["ITMParameters"].toObject()["maxPoints"].toInt(); + // *********************************** + + QJsonObject uncertaintyObj = jsonObj["APUncertainty"].toObject(); + + if (uncertaintyObj.contains("scanMethod") && !uncertaintyObj["scanMethod"].isUndefined()) { + std::string scanMethodStr = uncertaintyObj["scanMethod"].toString().toStdString(); + if (scanMethodStr == "xyAlignRegionNorthEast") { + _scanRegionMethod = CConst::xyAlignRegionNorthEastScanRegionMethod; + } else if (scanMethodStr == "xyAlignRegionMajorMinor") { + _scanRegionMethod = CConst::xyAlignRegionMajorMinorScanRegionMethod; + } else if (scanMethodStr == "latLonAlignGrid") { + _scanRegionMethod = CConst::latLonAlignGridScanRegionMethod; + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): Invalid " + "scanPointBelowGroundMethod specified."); + } + } else { + _scanRegionMethod = CConst::latLonAlignGridScanRegionMethod; + } + + // AP uncertainty scanning resolution + _scanres_xy = -1.0; + _scanres_points_per_degree = -1; + switch (_scanRegionMethod) { + case CConst::xyAlignRegionNorthEastScanRegionMethod: + case CConst::xyAlignRegionMajorMinorScanRegionMethod: + if (uncertaintyObj.contains("horizontal") && + !uncertaintyObj["horizontal"].isUndefined()) { + _scanres_xy = uncertaintyObj["horizontal"].toDouble(); + } else { + _scanres_xy = 30.0; + } + break; + case CConst::latLonAlignGridScanRegionMethod: + if (uncertaintyObj.contains("points_per_degree") && + !uncertaintyObj["points_per_degree"].isUndefined()) { + _scanres_points_per_degree = + uncertaintyObj["points_per_degree"].toInt(); + } else { + _scanres_points_per_degree = 3600; // Default 1 arcsec + } + break; + default: + CORE_DUMP; + break; + } + + if (uncertaintyObj.contains("height") && !uncertaintyObj["height"].isUndefined()) { + _scanres_ht = uncertaintyObj["height"].toDouble(); + } else { + _scanres_ht = 5.0; + } + + if (uncertaintyObj.contains("maxVerticalUncertainty") && + !uncertaintyObj["maxVerticalUncertainty"].isUndefined()) { + _maxVerticalUncertainty = uncertaintyObj["maxVerticalUncertainty"].toDouble(); + } else { + _maxVerticalUncertainty = 50.0; + } + + if (uncertaintyObj.contains("maxHorizontalUncertaintyDistance") && + !uncertaintyObj["maxHorizontalUncertaintyDistance"].isUndefined()) { + _maxHorizontalUncertaintyDistance = + uncertaintyObj["maxHorizontalUncertaintyDistance"].toDouble(); + } else { + _maxHorizontalUncertaintyDistance = 650.0; + } + + if (jsonObj.contains("minEIRPIndoor") && !jsonObj["minEIRPIndoor"].isUndefined()) { + _minEIRPIndoor_dBm = jsonObj["minEIRPIndoor"].toDouble(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): minEIRPIndoor is " + "missing."); + } + + if (jsonObj.contains("minEIRPOutdoor") && !jsonObj["minEIRPOutdoor"].isUndefined()) { + _minEIRPOutdoor_dBm = jsonObj["minEIRPOutdoor"].toDouble(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): minEIRPOutdoor is " + "missing."); + } + + _maxEIRP_dBm = jsonObj["maxEIRP"].toDouble(); + + if (jsonObj.contains("minPSD") && !jsonObj["minPSD"].isUndefined()) { + _minPSD_dBmPerMHz = jsonObj["minPSD"].toDouble(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): minPSD is missing."); + } + + bool reportUnavailableSpectrumFlag; + if (jsonObj.contains("reportUnavailableSpectrum") && + !jsonObj["reportUnavailableSpectrum"].isUndefined()) { + reportUnavailableSpectrumFlag = jsonObj["reportUnavailableSpectrum"].toBool(); + } else { + reportUnavailableSpectrumFlag = false; + } + + if (reportUnavailableSpectrumFlag) { + if (jsonObj.contains("reportUnavailPSDdBPerMHz") && + !jsonObj["reportUnavailPSDdBPerMHz"].isUndefined()) { + _reportUnavailPSDdBmPerMHz = jsonObj["reportUnavailPSDdBPerMHz"].toDouble(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): " + "reportUnavailableSpectrum set but " + "reportUnavailPSDdBPerMHz is missing."); + } + } else { + _reportUnavailPSDdBmPerMHz = quietNaN; + } + + if (jsonObj.contains("inquiredFrequencyMaxPSD_dBmPerMHz") && + !jsonObj["inquiredFrequencyMaxPSD_dBmPerMHz"].isUndefined()) { + _inquiredFrequencyMaxPSD_dBmPerMHz = + jsonObj["inquiredFrequencyMaxPSD_dBmPerMHz"].toInt(); + } else { + _inquiredFrequencyMaxPSD_dBmPerMHz = + _maxEIRP_dBm - 10.0 * log10(20.0); // Default maxEIRP over 20 MHz + } + + _IoverN_threshold_dB = jsonObj["threshold"].toDouble(); + _maxRadius = jsonObj["maxLinkDistance"].toDouble() * 1000.0; // convert from km to m + _bodyLossIndoorDB = jsonObj["bodyLoss"].toObject()["valueIndoor"].toDouble(); + _bodyLossOutdoorDB = jsonObj["bodyLoss"].toObject()["valueOutdoor"].toDouble(); + _polarizationLossDB = jsonObj["polarizationMismatchLoss"].toObject()["value"].toDouble(); + _buildingLossModel = (buildingLoss["kind"]).toString(); + + int validFlag; + if (jsonObj.contains("propagationEnv") && !jsonObj["propagationEnv"].isUndefined()) { + _propEnvMethod = (CConst::PropEnvMethodEnum) + CConst::strPropEnvMethodList->str_to_type( + jsonObj["propagationEnv"].toString().toStdString(), + validFlag, + 0); + if (!validFlag) { + throw std::runtime_error("ERROR: Invalid propagationEnv"); + } + } else { + _propEnvMethod = CConst::nlcdPointPropEnvMethod; + } + + if (jsonObj.contains("densityThrUrban") && !jsonObj["densityThrUrban"].isUndefined()) { + _densityThrUrban = jsonObj["densityThrUrban"].toDouble(); + } else { + _densityThrUrban = 486.75e-6; + } + + if (jsonObj.contains("densityThrSuburban") && + !jsonObj["densityThrSuburban"].isUndefined()) { + _densityThrSuburban = jsonObj["densityThrSuburban"].toDouble(); + } else { + _densityThrSuburban = 211.205e-6; + } + + if (jsonObj.contains("densityThrRural") && !jsonObj["densityThrRural"].isUndefined()) { + _densityThrRural = jsonObj["densityThrRural"].toDouble(); + } else { + _densityThrRural = 57.1965e-6; + } + // *********************************** + + // *********************************** + // Feeder loss parameters + // *********************************** + QJsonObject receiverFeederLossObj = jsonObj["receiverFeederLoss"].toObject(); + + if (receiverFeederLossObj.contains("IDU") && !receiverFeederLossObj["IDU"].isUndefined()) { + _rxFeederLossDBIDU = receiverFeederLossObj["IDU"].toDouble(); + } else { + _rxFeederLossDBIDU = 3.0; + } + if (receiverFeederLossObj.contains("ODU") && !receiverFeederLossObj["ODU"].isUndefined()) { + _rxFeederLossDBODU = receiverFeederLossObj["ODU"].toDouble(); + } else { + _rxFeederLossDBODU = 0.0; + } + if (receiverFeederLossObj.contains("UNKNOWN") && + !receiverFeederLossObj["UNKNOWN"].isUndefined()) { + _rxFeederLossDBUnknown = receiverFeederLossObj["UNKNOWN"].toDouble(); + } else { + _rxFeederLossDBUnknown = 0.0; + } + // *********************************** + + // *********************************** + // Noise Figure parameters + // *********************************** + auto ulsRecieverNoise = jsonObj["fsReceiverNoise"].toObject(); + + QJsonArray freqList = ulsRecieverNoise["freqList"].toArray(); + for (QJsonValue freqVal : freqList) { + double freqValHz = freqVal.toDouble() * 1.0e6; // Convert MHz to Hz + _noisePSDFreqList.push_back(freqValHz); + } + + QJsonArray noisePSDList = ulsRecieverNoise["noiseFloorList"].toArray(); + for (QJsonValue noisePSDVal : noisePSDList) { + double noisePSDValdBWPerHz = noisePSDVal.toDouble() - + 90.0; // Convert dbM/MHz to dBW/Hz + _noisePSDList.push_back(noisePSDValdBWPerHz); + } + + if (_noisePSDList.size() != _noisePSDFreqList.size() + 1) { + throw std::runtime_error("AfcManager::importConfigAFCjson(): Invalid " + "fsReceiverNoise specification."); + } + for (int i = 1; i < (int)_noisePSDFreqList.size(); ++i) { + if (!(_noisePSDFreqList[i] > _noisePSDFreqList[i - 1])) { + throw std::runtime_error("AfcManager::importConfigAFCjson(): " + "fsReceiverNoise freqList not monotonically " + "increasing."); + } + } + + // *********************************** + + // *********************************** + // apply clutter bool + // *********************************** + if (jsonObj.contains("clutterAtFS") && !jsonObj["clutterAtFS"].isUndefined()) { + _applyClutterFSRxFlag = jsonObj["clutterAtFS"].toBool(); + } else { + _applyClutterFSRxFlag = false; + } + + if (jsonObj.contains("allowRuralFSClutter") && + !jsonObj["allowRuralFSClutter"].isUndefined()) { + _allowRuralFSClutterFlag = jsonObj["allowRuralFSClutter"].toBool(); + } else { + _allowRuralFSClutterFlag = false; + } + + if (jsonObj.contains("fsClutterModel") && !jsonObj["fsClutterModel"].isUndefined()) { + QJsonObject fsClutterModel = jsonObj["fsClutterModel"].toObject(); + if (fsClutterModel.contains("p2108Confidence") && + !fsClutterModel["p2108Confidence"].isUndefined()) { + _fsConfidenceClutter2108 = fsClutterModel["p2108Confidence"].toDouble() / + 100.0; + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): " + "fsClutterModel[p2108Confidence] missing."); + } + if (fsClutterModel.contains("maxFsAglHeight") && + !fsClutterModel["maxFsAglHeight"].isUndefined()) { + _maxFsAglHeight = fsClutterModel["maxFsAglHeight"].toDouble(); + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): " + "fsClutterModel[maxFsAglHeight] missing."); + } + } else { + throw std::runtime_error("AfcManager::importConfigAFCjson(): fsClutterModel " + "missing."); + } + + if (jsonObj.contains("rlanITMTxClutterMethod") && + !jsonObj["rlanITMTxClutterMethod"].isUndefined()) { + _rlanITMTxClutterMethod = + (CConst::ITMClutterMethodEnum)CConst::strITMClutterMethodList->str_to_type( + jsonObj["rlanITMTxClutterMethod"].toString().toStdString(), + validFlag, + 0); + if (!validFlag) { + throw std::runtime_error("ERROR: Invalid rlanITMTxClutterMethod"); + } + } else { + _rlanITMTxClutterMethod = CConst::ForceTrueITMClutterMethod; + } + // *********************************** + + if (jsonObj.contains("ulsDefaultAntennaType") && + !jsonObj["ulsDefaultAntennaType"].isUndefined()) { + _ulsDefaultAntennaType = + (CConst::ULSAntennaTypeEnum)CConst::strULSAntennaTypeList->str_to_type( + jsonObj["ulsDefaultAntennaType"].toString().toStdString(), + validFlag, + 0); + if (!validFlag) { + throw std::runtime_error("ERROR: Invalid ulsDefaultAntennaType"); + } + } + + // *********************************** + // As of Feb-2022, it was agreed to include all possible Mobile links (e.g. those marked as + // Mobile or TP radio service) in UNII-5 and UNII-7 for AFC analysis. + // *********************************** + if (jsonObj.contains("removeMobile") && !jsonObj["removeMobile"].isUndefined()) { + _removeMobile = jsonObj["removeMobile"].toBool(); + } else { + _removeMobile = false; + } + + // Check what the building penetration type is (right ehre it is ITU-R P.2109) + if (buildingLoss["kind"].toString() == "ITU-R Rec. P.2109") { + _fixedBuildingLossFlag = false; + + if (buildingLoss["buildingType"].toString() == "Traditional") { + _buildingType = CConst::traditionalBuildingType; + } else if (buildingLoss["buildingType"].toString() == "Efficient") { + _buildingType = CConst::thermallyEfficientBuildingType; + } + _confidenceBldg2109 = buildingLoss["confidence"].toDouble() / 100.0; + + // User uses a fixed value for building loss + } else if (buildingLoss["kind"].toString() == "Fixed Value") { + _fixedBuildingLossFlag = true; + _fixedBuildingLossValue = buildingLoss["value"].toDouble(); + _buildingType = CConst::noBuildingType; + _confidenceBldg2109 = 0.0; + } else { + throw std::runtime_error("ERROR: Invalid buildingLoss[\"kind\"]"); + } + + /**************************************************************************************/ + /* Path Loss Model Parmeters ****/ + /**************************************************************************************/ + _pathLossClampFSPL = false; + + // Set pathLossModel + _pathLossModel = (CConst::PathLossModelEnum)CConst::strPathLossModelList->str_to_type( + propModel["kind"].toString().toStdString(), + validFlag, + 0); + if (!validFlag) { + throw std::runtime_error("ERROR: Invalid propagationModel[\"kind\"]"); + } + + if ((_pathLossModel == CConst::CustomPathLossModel) || + (_pathLossModel == CConst::ISEDDBS06PathLossModel) || + (_pathLossModel == CConst::BrazilPathLossModel) || + (_pathLossModel == CConst::OfcomPathLossModel)) { + _pathLossModel = CConst::FCC6GHzReportAndOrderPathLossModel; + } + + switch (_pathLossModel) { + case CConst::unknownPathLossModel: + throw std::runtime_error("ERROR: Invalid propagationModel[\"kind\"]"); + break; + case CConst::FSPLPathLossModel: + // FSPL + break; + case CConst::ITMBldgPathLossModel: + // ITM model using Building data as terrain + // GUI gets these values as percentiles from 0-100, convert to probabilities + // 0-1 + _winner2ProbLOSThr = propModel["win2ProbLosThreshold"].toDouble() / 100.0; + _confidenceITM = propModel["itmConfidence"].toDouble() / 100.0; + _confidenceClutter2108 = propModel["p2108Confidence"].toDouble() / 100.0; + _useBDesignFlag = propModel["buildingSource"].toString() == "B-Design3D"; + _useLiDAR = propModel["buildingSource"].toString() == "LiDAR"; + _use3DEP = true; + _winner2LOSOption = CConst::UnknownLOSOption; + _winner2UnknownLOSMethod = CConst::PLOSThresholdWinner2UnknownLOSMethod; + break; + case CConst::CoalitionOpt6PathLossModel: + // No buildings, Winner II, ITM, P.456 Clutter + // GUI gets these values as percentiles from 0-100, convert to probabilities + // 0-1 + _winner2ProbLOSThr = propModel["win2ProbLosThreshold"].toDouble() / 100.0; + _confidenceITM = propModel["itmConfidence"].toDouble() / 100.0; + _confidenceClutter2108 = propModel["p2108Confidence"].toDouble() / 100.0; + + _use3DEP = propModel["terrainSource"].toString() == "3DEP (30m)"; + + _winner2LOSOption = CConst::UnknownLOSOption; + _winner2UnknownLOSMethod = CConst::PLOSThresholdWinner2UnknownLOSMethod; + break; + case CConst::FCC6GHzReportAndOrderPathLossModel: + // FCC_6GHZ_REPORT_AND_ORDER path loss model + // GUI gets these values as percentiles from 0-100, convert to probabilities + // 0-1 + _winner2ProbLOSThr = quietNaN; + _confidenceITM = propModel["itmConfidence"].toDouble() / 100.0; + _confidenceClutter2108 = propModel["p2108Confidence"].toDouble() / 100.0; + + _useBDesignFlag = propModel["buildingSource"].toString() == "B-Design3D"; + _useLiDAR = propModel["buildingSource"].toString() == "LiDAR"; + + // In the GUI, force this to be true for US and CA. + _use3DEP = propModel["terrainSource"].toString() == "3DEP (30m)"; + + _winner2LOSOption = ((_useBDesignFlag || _useLiDAR) ? + CConst::BldgDataReqTxRxLOSOption : + (!_cdsmDir.empty()) ? CConst::CdsmLOSOption : + CConst::UnknownLOSOption); + _winner2UnknownLOSMethod = CConst::PLOSCombineWinner2UnknownLOSMethod; + + _pathLossClampFSPL = true; + break; + default: + CORE_DUMP; + break; + } + + if (_use3DEP) { + if (_depDir.empty()) { + throw std::runtime_error("AfcManager::importConfigAFCjson(): Specified " + "path loss model requires 3DEP data, but depDir " + "is empty."); + } + } else { + if (!_depDir.empty()) { + LOGGER_WARN(logger) << "3DEP data defined, but selected path loss model " + "doesn't use 3DEP data. No 3DEP data will be used"; + _depDir = ""; + } + } + + if (_useBDesignFlag || _useLiDAR) { + if (_lidarDir.empty()) { + throw std::runtime_error("AfcManager::importConfigAFCjson(): Specified " + "path loss model requires building data, but " + "lidarDir is empty."); + } + } else { + if (!_lidarDir.empty()) { + LOGGER_WARN(logger) + << "Building data defined, but selected path loss model doesn't " + "use building data. No building data will be used"; + + _lidarDir = ""; + } + } + + if (propModel.contains("win2Confidence") && !propModel["win2Confidence"].isUndefined()) { + throw std::runtime_error("AfcManager::importConfigAFCjson(): Outdated afc_config, " + "win2Confidence not supported parameter."); + } + + if (propModel.contains("win2ConfidenceCombined") && + !propModel["win2ConfidenceCombined"].isUndefined()) { + _confidenceWinner2Combined = propModel["win2ConfidenceCombined"].toDouble() / 100.0; + } else { + _confidenceWinner2Combined = 0.5; + } + + if (propModel.contains("win2ConfidenceLOS") && + !propModel["win2ConfidenceLOS"].isUndefined()) { + _confidenceWinner2LOS = propModel["win2ConfidenceLOS"].toDouble() / 100.0; + } else { + _confidenceWinner2LOS = 0.5; + } + + if (propModel.contains("win2ConfidenceNLOS") && + !propModel["win2ConfidenceNLOS"].isUndefined()) { + _confidenceWinner2NLOS = propModel["win2ConfidenceNLOS"].toDouble() / 100.0; + } else { + _confidenceWinner2NLOS = 0.5; + } + + if (propModel.contains("win2UseGroundDistance") && + !propModel["win2UseGroundDistance"].isUndefined()) { + _winner2UseGroundDistanceFlag = propModel["win2UseGroundDistance"].toBool(); + } else { + _winner2UseGroundDistanceFlag = true; + } + + // Whether or not to force LOS when mobile height above closeInHgtLOS for close in model", + // RLAN height above which prob of LOS = 100% for close in model", + if (propModel.contains("winner2HgtFlag") && !propModel["winner2HgtFlag"].isUndefined()) { + _closeInHgtFlag = propModel["winner2HgtFlag"].toBool(); + } else { + _closeInHgtFlag = true; + } + + if (propModel.contains("winner2HgtLOS") && !propModel["winner2HgtLOS"].isUndefined()) { + _closeInHgtLOS = propModel["winner2HgtLOS"].toDouble(); + } else { + _closeInHgtLOS = 0.5; + } + + if (propModel.contains("fsplUseGroundDistance") && + !propModel["fsplUseGroundDistance"].isUndefined()) { + _fsplUseGroundDistanceFlag = propModel["fsplUseGroundDistance"].toBool(); + } else { + _fsplUseGroundDistanceFlag = false; + } + + if (propModel.contains("itmReliability") && !propModel["itmReliability"].isUndefined()) { + _reliabilityITM = propModel["itmReliability"].toDouble() / 100.0; + } else { + _reliabilityITM = 0.5; + } + + if (propModel.contains("winner2LOSOption") && + !propModel["winner2LOSOption"].isUndefined()) { + _winner2LOSOption = (CConst::LOSOptionEnum)CConst::strLOSOptionList->str_to_type( + propModel["winner2LOSOption"].toString().toStdString(), + validFlag, + 1); + } + + // *********************************** + // If this flag is set, map data geojson is generated + // *********************************** + _mapDataGeoJsonFile = ""; + if (jsonObj.contains("enableMapInVirtualAp") && + !jsonObj["enableMapInVirtualAp"].isUndefined() && AfcManager::_createKmz) { + if (jsonObj["enableMapInVirtualAp"].toBool()) { + _mapDataGeoJsonFile = "mapData.json.gz"; + } + } + + if (jsonObj.contains("channelResponseAlgorithm") && + !jsonObj["channelResponseAlgorithm"].isUndefined()) { + std::string strval = jsonObj["channelResponseAlgorithm"].toString().toStdString(); + _channelResponseAlgorithm = + (CConst::SpectralAlgorithmEnum) + CConst::strSpectralAlgorithmList->str_to_type(strval, validFlag, 1); + } else { + _channelResponseAlgorithm = CConst::pwrSpectralAlgorithm; + } + + if (jsonObj.contains("visibilityThreshold") && + !jsonObj["visibilityThreshold"].isUndefined()) { + _visibilityThreshold = jsonObj["visibilityThreshold"].toDouble(); + } else { + _visibilityThreshold = -10000.0; + } + + if (jsonObj.contains("printSkippedLinksFlag") && + !jsonObj["printSkippedLinksFlag"].isUndefined()) { + _printSkippedLinksFlag = jsonObj["printSkippedLinksFlag"].toBool(); + } else { + _printSkippedLinksFlag = false; + } + + if (jsonObj.contains("roundPSDEIRPFlag") && !jsonObj["roundPSDEIRPFlag"].isUndefined()) { + _roundPSDEIRPFlag = jsonObj["roundPSDEIRPFlag"].toBool(); + } else { + _roundPSDEIRPFlag = true; + } + + if (jsonObj.contains("allowScanPtsInUncReg") && + !jsonObj["allowScanPtsInUncReg"].isUndefined()) { + _allowScanPtsInUncRegFlag = jsonObj["allowScanPtsInUncReg"].toBool(); + } else { + _allowScanPtsInUncRegFlag = false; + } + + // *********************************** + // If this flag is set, FS RX antenna will have near field adjustment applied to antenna + // gain + // *********************************** + if (jsonObj.contains("nearFieldAdjFlag") && !jsonObj["nearFieldAdjFlag"].isUndefined()) { + _nearFieldAdjFlag = jsonObj["nearFieldAdjFlag"].toBool(); + } else { + _nearFieldAdjFlag = true; + } + + if (_nearFieldAdjFlag) { + if (_nfaTableFile.empty()) { + throw std::runtime_error("AfcManager::importConfigAFCjson(): " + "nearFieldAdjFlag set to true, but nfaTableFile " + "not specified."); + } + } else { + if (!_nfaTableFile.empty()) { + LOGGER_WARN(logger) << "nearFieldAdjFlag set to false, but nfaTableFile " + "has been specified. Ignoring nfaTableFile."; + _nfaTableFile = ""; + } + } + // *********************************** + + // *********************************** + // If this flag is set, compute passive repeaters, otherwise ignore passive repeaters + // *********************************** + if (jsonObj.contains("passiveRepeaterFlag") && + !jsonObj["passiveRepeaterFlag"].isUndefined()) { + _passiveRepeaterFlag = jsonObj["passiveRepeaterFlag"].toBool(); + } else { + _passiveRepeaterFlag = true; + } + + if (_passiveRepeaterFlag) { + if (_prTableFile.empty()) { + throw std::runtime_error("AfcManager::importConfigAFCjson(): " + "passiveRepeaterFlag set to true, but prTableFile " + "not specified."); + } + } else { + if (!_prTableFile.empty()) { + LOGGER_WARN(logger) << "passiveRepeaterFlag set to false, but prTableFile " + "has been specified. Ignoring prTableFile."; + _prTableFile = ""; + } + } + // *********************************** + + // *********************************** + // If this flag is set, report an error when all scan points are below + // _minRlanHeightAboveTerrain + // *********************************** + if (jsonObj.contains("reportErrorRlanHeightLowFlag") && + !jsonObj["reportErrorRlanHeightLowFlag"].isUndefined()) { + _reportErrorRlanHeightLowFlag = jsonObj["reportErrorRlanHeightLowFlag"].toBool(); + } else { + _reportErrorRlanHeightLowFlag = false; + } + // *********************************** + + // *********************************** + // If this flag is set, include ACI in channel I/N calculations + // *********************************** + if (jsonObj.contains("aciFlag") && !jsonObj["aciFlag"].isUndefined()) { + _aciFlag = jsonObj["aciFlag"].toBool(); + } else { + _aciFlag = true; + } + // *********************************** + + if (jsonObj.contains("deniedRegionFile") && !jsonObj["deniedRegionFile"].isUndefined()) { + _deniedRegionFile = SearchPaths::forReading("data", + jsonObj["deniedRegionFile"].toString(), + true) + .toStdString(); + } +} + +QJsonArray generateStatusMessages(const std::vector &messages) +{ + auto array = QJsonArray(); + for (auto &m : messages) + array.append(QJsonValue(QString::fromStdString(m))); + return array; +} + +void AfcManager::addBuildingDatabaseTiles(OGRLayer *layer) +{ + // add building database raster boundary if loaded + if (_terrainDataModel->getNumLidarRegion()) { + LOGGER_DEBUG(logger) << "adding raster bounds"; + for (QRectF b : _terrainDataModel->getBounds()) { + LOGGER_DEBUG(logger) << "adding tile"; + // Instantiate cone object + std::unique_ptr boxFeature( + OGRFeature::CreateFeature(layer->GetLayerDefn())); + + // Intantiate unique-pointers to OGRPolygon and OGRLinearRing for storing + // the beam coverage + GdalHelpers::GeomUniquePtr boundBox( + GdalHelpers::createGeometry< + OGRPolygon>()); // Use GdalHelpers.h templates to have + // unique pointers create these on the heap + GdalHelpers::GeomUniquePtr boxPoints( + GdalHelpers::createGeometry()); + + // Create OGRPoints to store the coordinates of the beam triangle + // ***IMPORTANT NOTE: Coordinates stored as (Lon,Lat) here, required by + // geoJSON specifications + GdalHelpers::GeomUniquePtr topLeft( + GdalHelpers::createGeometry()); + topLeft->setY(b.top()); + topLeft->setX(b.left()); // Must set points manually since cannot construct + // while pointing at with unique pointer + GdalHelpers::GeomUniquePtr topRight( + GdalHelpers::createGeometry()); + topRight->setY(b.top()); + topRight->setX(b.right()); + GdalHelpers::GeomUniquePtr botLeft( + GdalHelpers::createGeometry()); + botLeft->setY(b.bottom()); + botLeft->setX(b.left()); + GdalHelpers::GeomUniquePtr botRight( + GdalHelpers::createGeometry()); + botRight->setY(b.bottom()); + botRight->setX(b.right()); + + // Adding the polygon vertices to a OGRLinearRing object for geoJSON export + boxPoints->addPoint(topLeft.get()); // Using .get() gives access to the + // pointer without giving up ownership + boxPoints->addPoint(topRight.get()); + boxPoints->addPoint(botRight.get()); + boxPoints->addPoint(botLeft.get()); + boxPoints->addPoint(topLeft.get()); + boundBox->addRingDirectly( + boxPoints.release()); // Points unique-pointer to null and gives up + // ownership of exteriorOfCone to beamCone + + // Add properties to the geoJSON features + boxFeature->SetField("kind", "BLDB"); // BLDB = building bounds + + // Add geometry to feature + boxFeature->SetGeometryDirectly(boundBox.release()); + + if (layer->CreateFeature(boxFeature.release()) != OGRERR_NONE) { + throw std::runtime_error("Could not add bound feature in layer of " + "output data source"); + } + } + } +} + +void AfcManager::addDeniedRegions(OGRLayer *layer) +{ + // add denied regions + LOGGER_DEBUG(logger) << "adding denied regions"; + int drIdx; + for (drIdx = 0; drIdx < (int)_deniedRegionList.size(); ++drIdx) { + DeniedRegionClass *dr = _deniedRegionList[drIdx]; + + DeniedRegionClass::TypeEnum drType = dr->getType(); + + std::string pfx; + switch (drType) { + case DeniedRegionClass::RASType: + pfx = "RAS_"; + break; + case DeniedRegionClass::userSpecifiedType: + pfx = "USER_SPEC_"; + break; + default: + CORE_DUMP; + break; + } + std::string name = pfx + std::to_string(dr->getID()); + + LOGGER_DEBUG(logger) << "adding denied region " << name; + + double maxRLANHeightAGL; + if (_analysisType == "HeatmapAnalysis") { + maxRLANHeightAGL = _heatmapMaxRLANHeightAGL; + } else { + maxRLANHeightAGL = _rlanRegion->getMaxHeightAGL(); + } + + int numPtsCircle = 32; + double rectLonStart, rectLonStop, rectLatStart, rectLatStop; + double circleRadius, longitudeCenter, latitudeCenter; + double drTerrainHeight, drBldgHeight, drHeightAGL; + Vector3 drCenterPosn; + Vector3 drUpVec; + Vector3 drEastVec; + Vector3 drNorthVec; + QString dr_coords; + MultibandRasterClass::HeightResult drLidarHeightResult; + CConst::HeightSourceEnum drHeightSource; + DeniedRegionClass::GeometryEnum drGeometry = dr->getGeometry(); + int numGeom; + bool rectFlag; + switch (drGeometry) { + case DeniedRegionClass::rectGeometry: + case DeniedRegionClass::rect2Geometry: + numGeom = ((RectDeniedRegionClass *)dr)->getNumRect(); + rectFlag = true; + break; + case DeniedRegionClass::circleGeometry: + case DeniedRegionClass::horizonDistGeometry: + numGeom = 1; + rectFlag = false; + break; + default: + CORE_DUMP; + break; + } + for (int geomIdx = 0; geomIdx < numGeom; ++geomIdx) { + GdalHelpers::GeomUniquePtr ptList( + GdalHelpers::createGeometry()); + if (rectFlag) { + std::tie(rectLonStart, rectLonStop, rectLatStart, rectLatStop) = + ((RectDeniedRegionClass *)dr)->getRect(geomIdx); + GdalHelpers::GeomUniquePtr topLeft( + GdalHelpers::createGeometry()); + topLeft->setY(rectLatStop); + topLeft->setX(rectLonStart); + GdalHelpers::GeomUniquePtr topRight( + GdalHelpers::createGeometry()); + topRight->setY(rectLatStop); + topRight->setX(rectLonStop); + GdalHelpers::GeomUniquePtr botLeft( + GdalHelpers::createGeometry()); + botLeft->setY(rectLatStart); + botLeft->setX(rectLonStart); + GdalHelpers::GeomUniquePtr botRight( + GdalHelpers::createGeometry()); + botRight->setY(rectLatStart); + botRight->setX(rectLonStop); + + ptList->addPoint( + topLeft.get()); // Using .get() gives access to the pointer + // without giving up ownership + ptList->addPoint(topRight.get()); + ptList->addPoint(botRight.get()); + ptList->addPoint(botLeft.get()); + ptList->addPoint(topLeft.get()); + } else { + OGRPoint *ogrPtList[numPtsCircle]; + circleRadius = ((CircleDeniedRegionClass *)dr) + ->computeRadius(maxRLANHeightAGL); + longitudeCenter = + ((CircleDeniedRegionClass *)dr)->getLongitudeCenter(); + latitudeCenter = + ((CircleDeniedRegionClass *)dr)->getLatitudeCenter(); + drHeightAGL = dr->getHeightAGL(); + _terrainDataModel->getTerrainHeight(longitudeCenter, + latitudeCenter, + drTerrainHeight, + drBldgHeight, + drLidarHeightResult, + drHeightSource); + drCenterPosn = EcefModel::geodeticToEcef( + latitudeCenter, + longitudeCenter, + (drTerrainHeight + drHeightAGL) / 1000.0); + drUpVec = drCenterPosn.normalized(); + drEastVec = (Vector3(-drUpVec.y(), drUpVec.x(), 0.0)).normalized(); + drNorthVec = drUpVec.cross(drEastVec); + for (int ptIdx = 0; ptIdx < numPtsCircle; ++ptIdx) { + ogrPtList[ptIdx] = GdalHelpers::createGeometry(); + double phi = 2 * M_PI * ptIdx / numPtsCircle; + Vector3 circlePtPosn = drCenterPosn + + (circleRadius / 1000) * + (drEastVec * cos(phi) + + drNorthVec * sin(phi)); + + GeodeticCoord circlePtPosnGeodetic = + EcefModel::ecefToGeodetic(circlePtPosn); + + ogrPtList[ptIdx]->setX(circlePtPosnGeodetic.longitudeDeg); + ogrPtList[ptIdx]->setY(circlePtPosnGeodetic.latitudeDeg); + + ptList->addPoint(ogrPtList[ptIdx]); + } + ptList->addPoint(ogrPtList[0]); + } + + // Intantiate unique-pointers to OGRPolygon and OGRLinearRing for denied + // region + GdalHelpers::GeomUniquePtr boundBox( + GdalHelpers::createGeometry()); + // Use GdalHelpers.h templates to have unique pointers create these on the + // heap + + // Adding the polygon vertices to a OGRLinearRing object for geoJSON export + boundBox->addRingDirectly(ptList.release()); + + // Add properties to the geoJSON features + std::unique_ptr regionFeature( + OGRFeature::CreateFeature(layer->GetLayerDefn())); + regionFeature->SetField("kind", "DR"); // DR = denied region + + // Add geometry to feature + regionFeature->SetGeometryDirectly(boundBox.release()); + + if (layer->CreateFeature(regionFeature.release()) != OGRERR_NONE) { + throw std::runtime_error("Could not add denied region feature in " + "layer of output data source"); + } + } + } +} + +QJsonDocument AfcManager::generateRatAfcJson() +{ + std::vector psdFreqRangeList; + computeInquiredFreqRangesPSD(psdFreqRangeList); + + // compute availableSpectrumInfo + QJsonArray spectrumInfos = QJsonArray(); + for (auto &freqRange : psdFreqRangeList) { + for (int i = 0; i < (int)freqRange.psd_dBm_MHzList.size(); i++) { + if (!std::isnan(freqRange.psd_dBm_MHzList.at(i))) { + spectrumInfos.append(QJsonObject { + {"frequencyRange", + QJsonObject {{"lowFrequency", freqRange.freqMHzList.at(i)}, + {"highFrequency", + freqRange.freqMHzList.at(i + 1)}}}, + {"maxPsd", freqRange.psd_dBm_MHzList.at(i)}}); + } + } + } + + QJsonArray channelInfos = QJsonArray(); + QJsonArray blackChannelInfos = QJsonArray(); + QJsonArray redChannelInfos = QJsonArray(); + if (_responseCode == CConst::successResponseCode) { + for (const auto &group : _inquiredChannels) { + auto operatingClass = group.first; + auto indexArray = QJsonArray(); + auto blackIndexArray = QJsonArray(); + auto redIndexArray = QJsonArray(); + auto eirpArray = QJsonArray(); + + for (const auto &chan : _channelList) { + if ((chan.type == ChannelType::INQUIRED_CHANNEL) && + (chan.operatingClass == operatingClass)) { + ChannelColor chanColor = std::get<2>(chan.segList[0]); + if (chanColor == BLACK) { + blackIndexArray.append(chan.index); + } else if (chanColor == RED) { + redIndexArray.append(chan.index); + } else { + indexArray.append(chan.index); + double eirpVal = + std::min(std::get<0>(chan.segList[0]), + std::get<1>(chan.segList[0])); + if (_roundPSDEIRPFlag) { + // EIRP value rounded down to nearest + // multiple of 0.1 dB + eirpVal = std::floor(eirpVal * 10) / 10.0; + } + eirpArray.append(eirpVal); + } + } + } + + auto channelGroup = QJsonObject {{"globalOperatingClass", + QJsonValue(operatingClass)}, + {"channelCfi", indexArray}, + {"maxEirp", eirpArray}}; + auto blackChannelGroup = QJsonObject {{"globalOperatingClass", + QJsonValue(operatingClass)}, + {"channelCfi", blackIndexArray}}; + auto redChannelGroup = QJsonObject {{"globalOperatingClass", + QJsonValue(operatingClass)}, + {"channelCfi", redIndexArray}}; + channelInfos.append(channelGroup); + blackChannelInfos.append(blackChannelGroup); + redChannelInfos.append(redChannelGroup); + } + } + + std::string shortDescription; + + switch (_responseCode) { + case CConst::generalFailureResponseCode: + shortDescription = "General Failure"; + break; + case CConst::successResponseCode: + shortDescription = "Success"; + break; + case CConst::versionNotSupportedResponseCode: + shortDescription = "Version Not Supported"; + break; + case CConst::deviceDisallowedResponseCode: + shortDescription = "Device Disallowed"; + break; + case CConst::missingParamResponseCode: + shortDescription = "Missing Param"; + break; + case CConst::invalidValueResponseCode: + shortDescription = "Invalid Value"; + break; + case CConst::unexpectedParamResponseCode: + shortDescription = "Unexpected Param"; + break; + case CConst::unsupportedSpectrumResponseCode: + shortDescription = "Unsupported Spectrum"; + break; + + default: + CORE_DUMP; + break; + } + + bool hasSupplementalInfoFlag = false; + if (_responseCode == CConst::missingParamResponseCode) { + hasSupplementalInfoFlag = true; + } else if (_responseCode == CConst::invalidValueResponseCode) { + hasSupplementalInfoFlag = true; + } else if (_responseCode == CConst::unexpectedParamResponseCode) { + hasSupplementalInfoFlag = true; + } + + QJsonObject responseObj; + responseObj.insert("responseCode", _responseCode); + responseObj.insert("shortDescription", shortDescription.c_str()); + + if (hasSupplementalInfoFlag) { + QJsonObject paramObj; + QJsonArray paramsArray; + + if (_missingParams.size()) { + paramsArray = QJsonArray(); + for (auto ¶m : _missingParams) { + paramsArray.append(param); + } + paramObj.insert("missingParams", paramsArray); + } + + if (_invalidParams.size()) { + paramsArray = QJsonArray(); + for (auto ¶m : _invalidParams) { + paramsArray.append(param); + } + paramObj.insert("invalidParams", paramsArray); + } + + if (_unexpectedParams.size()) { + paramsArray = QJsonArray(); + for (auto ¶m : _unexpectedParams) { + paramsArray.append(param); + } + paramObj.insert("unexpectedParams", paramsArray); + } + + responseObj.insert("supplementalInfo", paramObj); + } + + QJsonObject availableSpectrumInquiryResponse; + availableSpectrumInquiryResponse.insert("requestId", _requestId); + availableSpectrumInquiryResponse.insert("rulesetId", _rulesetId); + if (channelInfos.size()) { + availableSpectrumInquiryResponse.insert("availableChannelInfo", channelInfos); + } + if (spectrumInfos.size()) { + availableSpectrumInquiryResponse.insert("availableFrequencyInfo", spectrumInfos); + } + if (_responseCode == CConst::successResponseCode) { + availableSpectrumInquiryResponse.insert("availabilityExpireTime", + ISO8601TimeUTC(1)); + } + + QJsonObject extensionParameterObj; + if (blackChannelInfos.size()) { + extensionParameterObj.insert("blackChannelInfo", blackChannelInfos); + } + if (redChannelInfos.size()) { + extensionParameterObj.insert("redChannelInfo", redChannelInfos); + } + + QJsonObject vendorExtensionObj; + vendorExtensionObj.insert("extensionId", "openAfc.redBlackData"); + vendorExtensionObj.insert("parameters", extensionParameterObj); + + QJsonArray vendorExtensionArray; + vendorExtensionArray.append(vendorExtensionObj); + + availableSpectrumInquiryResponse.insert("vendorExtensions", vendorExtensionArray); + + availableSpectrumInquiryResponse.insert("response", responseObj); + + QJsonObject responses {{"version", _guiJsonVersion}, + {"availableSpectrumInquiryResponses", + QJsonArray {availableSpectrumInquiryResponse}}}; + + return QJsonDocument(responses); +} + +QJsonDocument AfcManager::generateExclusionZoneJson() +{ + QTemporaryDir tempDir; + if (!tempDir.isValid()) { + throw std::runtime_error("AfcManager::generateExclusionZone(): Failed to create a " + "temporary directory to store output of GeoJSON driver"); + } + + const QString tempOutFileName = "output.tmp"; + const QString tempOutFilePath = tempDir.filePath(tempOutFileName); + + GDALDataset *dataSet; + OGRLayer *exclusionLayer = createGeoJSONLayer(tempOutFilePath.toStdString().c_str(), + &dataSet); + + OGRFieldDefn objKind("kind", OFTString); + OGRFieldDefn fsid("FSID", OFTInteger); + OGRFieldDefn terrainHeight("terrainHeight", OFTReal); + OGRFieldDefn height("height", OFTReal); + OGRFieldDefn lat("lat", OFTReal); + OGRFieldDefn lon("lon", OFTReal); + objKind.SetWidth(64); + fsid.SetWidth(32); + terrainHeight.SetWidth(32); + height.SetWidth(32); + lat.SetWidth(32); + lon.SetWidth(32); + + if (exclusionLayer->CreateField(&objKind) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::generateExclusionZone(): Could not create " + "'kind' field in layer of the output data source"); + } + if (exclusionLayer->CreateField(&fsid) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::generateExclusionZone(): Could not create " + "'fsid' field in layer of the output data source"); + } + if (exclusionLayer->CreateField(&terrainHeight) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::generateExclusionZone(): Could not create " + "'terrainHeight' field in layer of the output data " + "source"); + } + if (exclusionLayer->CreateField(&height) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::generateExclusionZone(): Could not create " + "'height' field in layer of the output data source"); + } + if (exclusionLayer->CreateField(&lat) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::generateExclusionZone(): Could not create " + "'lat' field in layer of the output data source"); + } + if (exclusionLayer->CreateField(&lon) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::generateExclusionZone(): Could not create " + "'lon' field in layer of the output data source"); + } + + // Instantiate polygon object + std::unique_ptr exclusionZoneFeature( + OGRFeature::CreateFeature(exclusionLayer->GetLayerDefn())); + + // Intantiate unique-pointers to OGRPolygon and OGRLinearRing for storing the beam coverage + GdalHelpers::GeomUniquePtr exZone( + GdalHelpers::createGeometry()); // Use GdalHelpers.h templates to have + // unique pointers create these on the + // heap + GdalHelpers::GeomUniquePtr exteriorOfZone( + GdalHelpers::createGeometry()); + + int ulsIdx; + ULSClass *uls = findULSID(_exclusionZoneFSID, 0, ulsIdx); + // Add properties to the geoJSON features + exclusionZoneFeature->SetField("FSID", _exclusionZoneFSID); + exclusionZoneFeature->SetField("kind", "ZONE"); + exclusionZoneFeature->SetField("terrainHeight", _exclusionZoneFSTerrainHeight); + exclusionZoneFeature->SetField("height", _exclusionZoneHeightAboveTerrain); + exclusionZoneFeature->SetField("lat", uls->getRxLatitudeDeg()); + exclusionZoneFeature->SetField("lon", uls->getRxLongitudeDeg()); + + // Must set points manually since cannot construct while pointing at with unique pointer + GdalHelpers::GeomUniquePtr startPoint(GdalHelpers::createGeometry()); + startPoint->setX(_exclusionZone.back().first); // Lon + startPoint->setY(_exclusionZone.back().second); // Lat + exteriorOfZone->addPoint(startPoint.get()); // Using .get() gives access to the pointer + // without giving up ownership + + for (const auto &point : _exclusionZone) { + // Create OGRPoints to store the coordinates of the beam triangle + // ***IMPORTANT NOTE: Coordinates stored as (Lon,Lat) here, required by geoJSON + // specifications + GdalHelpers::GeomUniquePtr posPoint( + GdalHelpers::createGeometry()); + posPoint->setX(point.first); + posPoint->setY(point.second); + + // Adding the polygon vertices to a OGRLinearRing object for geoJSON export + exteriorOfZone->addPoint(posPoint.get()); + } + + // Add exterior boundary of cone's polygon + exZone->addRingDirectly( + exteriorOfZone.release()); // Points unique-pointer to null and gives up ownership + // of exteriorOfCone to beamCone + + // Add geometry to feature + exclusionZoneFeature->SetGeometryDirectly(exZone.release()); + + if (exclusionLayer->CreateFeature(exclusionZoneFeature.release()) != OGRERR_NONE) { + throw std::runtime_error("Could not add cone feature in layer of output data " + "source"); + } + + // Allocation clean-up + GDALClose(dataSet); // Remove the reference to the dataset + + // Create file to be written to (creates a file, a json object, and a json document in order + // to store) + std::ifstream tempFileStream(tempOutFilePath.toStdString(), std::ifstream::in); + const std::string geoJsonCollection = slurp(tempFileStream); + + QJsonDocument geoJsonDoc = QJsonDocument::fromJson( + QString::fromStdString(geoJsonCollection).toUtf8()); + QJsonObject geoJsonObj = geoJsonDoc.object(); + + QJsonObject analysisJsonObj = {{"geoJson", geoJsonObj}, + {"statusMessageList", + generateStatusMessages(statusMessageList)}}; + + return QJsonDocument(analysisJsonObj); +} + +OGRLayer *AfcManager::createGeoJSONLayer(const char *tmpPath, GDALDataset **dataSet) +{ + GDALDriver *driver = (GDALDriver *)GDALGetDriverByName("GeoJSON"); + if (!driver) { + throw std::runtime_error("GDALGetDriverByName() error"); + } + + /* create empty dataset */ + *dataSet = driver->Create(tmpPath, 0, 0, 0, GDT_Unknown, NULL); + if (!(*dataSet)) { + throw std::runtime_error("GDALOpenEx() error"); + } + + OGRSpatialReference + spatialRefWGS84; // Set the desired spatial reference (WGS84 in this case) + spatialRefWGS84.SetWellKnownGeogCS("WGS84"); + + OGRLayer *layer = (*dataSet)->CreateLayer("Temp_Output", + &spatialRefWGS84, + wkbPolygon, + NULL); + if (!layer) { + throw std::runtime_error("GDALDataset::CreateLayer() error"); + } + + return layer; +} + +void AfcManager::addHeatmap(OGRLayer *layer) +{ + // add heatmap + LOGGER_DEBUG(logger) << "adding heatmap"; + + bool itonFlag = (_heatmapAnalysisStr == "iton"); + + std::string valueStr = (itonFlag ? "ItoN" : "eirpLimit"); + + OGRFieldDefn objFill("fill", OFTString); + OGRFieldDefn objOpacity("fill-opacity", OFTReal); + OGRFieldDefn valueField(valueStr.c_str(), OFTReal); + OGRFieldDefn indoor("indoor", OFTString); + objFill.SetWidth(8); + objOpacity.SetWidth(32); + valueField.SetWidth(32); + indoor.SetWidth(32); + + if (layer->CreateField(&objFill) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::addHeatmap(): Could not create 'fill' field " + "in layer of the output data source"); + } + if (layer->CreateField(&objOpacity) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::addHeatmap(): Could not create " + "'fill-opacity' field in layer of the output data source"); + } + if (layer->CreateField(&valueField) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::addHeatmap(): Could not create value field " + "in layer of the output data source"); + } + if (layer->CreateField(&indoor) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::addHeatmap(): Could not create 'indoor' " + "field in layer of the output data source"); + } + + double latDel = 0.5 * (_heatmapMaxLat - _heatmapMinLat) / + _heatmapNumPtsLat; // distance from center point to top/bot side of square + double lonDel = + 0.5 * (_heatmapMaxLon - _heatmapMinLon) / + _heatmapNumPtsLon; // distance from center point to left/right side of square + LOGGER_DEBUG(logger) << "generating heatmap: " << _heatmapNumPtsLon << "x" + << _heatmapNumPtsLat; + for (int lonIdx = 0; lonIdx < _heatmapNumPtsLon; lonIdx++) { + for (int latIdx = 0; latIdx < _heatmapNumPtsLat; latIdx++) { + double lon = (_heatmapMinLon * (2 * _heatmapNumPtsLon - 2 * lonIdx - 1) + + _heatmapMaxLon * (2 * lonIdx + 1)) / + (2 * _heatmapNumPtsLon); + double lat = (_heatmapMinLat * (2 * _heatmapNumPtsLat - 2 * latIdx - 1) + + _heatmapMaxLat * (2 * latIdx + 1)) / + (2 * _heatmapNumPtsLat); + // Instantiate polygon object + std::unique_ptr heatmapFeature( + OGRFeature::CreateFeature(layer->GetLayerDefn())); + + // Intantiate unique-pointers to OGRPolygon and OGRLinearRing for storing + // the beam coverage + GdalHelpers::GeomUniquePtr mapZone( + GdalHelpers::createGeometry< + OGRPolygon>()); // Use GdalHelpers.h templates to have + // unique pointers create these on the heap + GdalHelpers::GeomUniquePtr mapExt( + GdalHelpers::createGeometry()); + + _minEIRP_dBm = (_heatmapIsIndoor[lonIdx][latIdx] ? _minEIRPIndoor_dBm : + _minEIRPOutdoor_dBm); + + std::string color = getHeatmapColor(_heatmapIToNDB[lonIdx][latIdx], + _heatmapIsIndoor[lonIdx][latIdx], + true); + + double value; + double showValueFlag = true; + if (itonFlag) { + value = _heatmapIToNDB[lonIdx][latIdx]; + } else { + value = _maxEIRP_dBm + _IoverN_threshold_dB - + _heatmapIToNDB[lonIdx][latIdx]; + if (value > _maxEIRP_dBm) { + value = _maxEIRP_dBm; + } else if (value < _minEIRP_dBm) { + value = _minEIRP_dBm; + showValueFlag = false; + } + } + + // Add properties to the geoJSON features + heatmapFeature->SetField("kind", "HMAP"); + heatmapFeature->SetField("fill", color.c_str()); + heatmapFeature->SetField("fill-opacity", 0.5); + if (showValueFlag) { + heatmapFeature->SetField(valueStr.c_str(), value); + } + heatmapFeature->SetField("indoor", + _heatmapIsIndoor[lonIdx][latIdx] ? "Y" : "N"); + + // Create OGRPoints to store the coordinates of the heatmap box + // ***IMPORTANT NOTE: Coordinates stored as (Lon,Lat) here, required by + // geoJSON specifications + GdalHelpers::GeomUniquePtr tl( + GdalHelpers::createGeometry()); + tl->setX(lon - lonDel); + tl->setY(lat + latDel); + // Adding the polygon vertices to a OGRLinearRing object for geoJSON export + mapExt->addPoint(tl.get()); + + GdalHelpers::GeomUniquePtr tr( + GdalHelpers::createGeometry()); + tr->setX(lon + lonDel); + tr->setY(lat + latDel); + // Adding the polygon vertices to a OGRLinearRing object for geoJSON export + mapExt->addPoint(tr.get()); + + GdalHelpers::GeomUniquePtr br( + GdalHelpers::createGeometry()); + br->setX(lon + lonDel); + br->setY(lat - latDel); + // Adding the polygon vertices to a OGRLinearRing object for geoJSON export + mapExt->addPoint(br.get()); + + GdalHelpers::GeomUniquePtr bl( + GdalHelpers::createGeometry()); + bl->setX(lon - lonDel); + bl->setY(lat - latDel); + // Adding the polygon vertices to a OGRLinearRing object for geoJSON export + mapExt->addPoint(bl.get()); + + // add end point again + mapExt->addPoint(tl.get()); + + // Add exterior boundary of cone's polygon + mapZone->addRingDirectly( + mapExt.release()); // Points unique-pointer to null and gives up + // ownership of exteriorOfCone to beamCone + + // Add geometry to feature + heatmapFeature->SetGeometryDirectly(mapZone.release()); + + if (layer->CreateFeature(heatmapFeature.release()) != OGRERR_NONE) { + throw std::runtime_error("Could not add heat map tile feature in " + "layer of output data source"); + } + } + } +} + +void AfcManager::exportGUIjson(const QString &exportJsonPath, const std::string &tempDir) +{ + QJsonDocument outputDocument; + if (_analysisType == "APAnalysis") { + // if the request type is PAWS then we only return the spectrum data + // outputDocument = QJsonDocument(jsonSpectrumData(_channelList, _deviceDesc, + // _wlanMinFreq)); + + // Write PAWS outputs to JSON file + // QFile outputAnalysisFile(exportJsonPath); + // outputAnalysisFile.open(QFile::WriteOnly); + // outputAnalysisFile.write(outputDocument.toJson()); + // outputAnalysisFile.close(); + // LOGGER_DEBUG(logger) << "PAWS response saved to the following JSON file: " << + // outputAnalysisFile.fileName().toStdString(); return; // Short circuit since + // export is complete now + } else if ((_analysisType == "AP-AFC") || (_analysisType == "HeatmapAnalysis")) { + // temporarily return PAWS until we write new generator function + // outputDocument = QJsonDocument(jsonSpectrumData(_rlanBWList, _numChan, + // _channelData, _deviceDesc, _wlanMinFreq)); + outputDocument = generateRatAfcJson(); + + if ((_responseCode == CConst::successResponseCode) && + (!_mapDataGeoJsonFile.empty())) { + generateMapDataGeoJson(tempDir); + } + } else if (_analysisType == "ExclusionZoneAnalysis") { + outputDocument = generateExclusionZoneJson(); + } else if (_analysisType == "ScanAnalysis") { + outputDocument = QJsonDocument(); +#if DEBUG_AFC + } else if (_analysisType == "test_itm") { + // Do nothing + } else if (_analysisType == "test_winner2") { + // Do nothing +#endif + } else { + throw std::runtime_error(ErrStream() << "ERROR: Unrecognized analysis type = \"" + << _analysisType << "\""); + } + + // Write analysis outputs to JSON file + QByteArray data = outputDocument.toJson(); + if (!AfcManager::_dataIf->gzipAndWriteFile(exportJsonPath, data)) { + throw std::runtime_error("Error writing output file"); + } + LOGGER_DEBUG(logger) << "Output file written to " << exportJsonPath.toStdString(); +} + +void AfcManager::generateMapDataGeoJson(const std::string &tempDir) +{ + QJsonDocument outputDocument; + + QTemporaryDir geoTempDir; + if (!geoTempDir.isValid()) { + throw std::runtime_error("AfcManager::generateMapDataGeoJson(): Failed to create a " + "temporary directory to store output of GeoJSON driver"); + } + + const QString tempOutFileName = "output.tmp"; + const QString tempOutFilePath = geoTempDir.filePath(tempOutFileName); + + GDALDataset *dataSet; + OGRLayer *coneLayer = createGeoJSONLayer(tempOutFilePath.toStdString().c_str(), &dataSet); + + OGRFieldDefn objKind("kind", OFTString); + OGRFieldDefn dbNameField("DBNAME", OFTString); + OGRFieldDefn fsidField("FSID", OFTInteger); + OGRFieldDefn segmentField("SEGMENT", OFTInteger); + OGRFieldDefn startFreq("startFreq", OFTReal); + OGRFieldDefn stopFreq("stopFreq", OFTReal); + objKind.SetWidth(64); /* fsLonField.SetWidth(64); fsLatField.SetWidth(64);*/ + fsidField.SetWidth(32); /* fsLonField.SetWidth(64); fsLatField.SetWidth(64);*/ + segmentField.SetWidth(32); /* fsLonField.SetWidth(64); fsLatField.SetWidth(64);*/ + startFreq.SetWidth(32); + stopFreq.SetWidth(32); + + if (coneLayer->CreateField(&objKind) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::generateMapDataGeoJson(): Could not create " + "'kind' field in layer of the output data source"); + } + if (coneLayer->CreateField(&dbNameField) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::generateMapDataGeoJson(): Could not create " + "'DBNAME' field in layer of the output data source"); + } + if (coneLayer->CreateField(&fsidField) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::generateMapDataGeoJson(): Could not create " + "'FSID' field in layer of the output data source"); + } + if (coneLayer->CreateField(&segmentField) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::generateMapDataGeoJson(): Could not create " + "'SEGMENT' field in layer of the output data source"); + } + if (coneLayer->CreateField(&startFreq) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::generateMapDataGeoJson(): Could not create " + "'startFreq' field in layer of the output data source"); + } + if (coneLayer->CreateField(&stopFreq) != OGRERR_NONE) { + throw std::runtime_error("AfcManager::generateMapDataGeoJson(): Could not create " + "'stopFreq' field in layer of the output data source"); + } + + // Calculate the cones in iterative loop + LatLon FSLatLonVal, posPointLatLon, negPointLatLon; + for (const auto &ulsIdx : _ulsIdxList) { + ULSClass *uls = (*_ulsList)[ulsIdx]; + + // Grab FSID for storing with coverage polygon + int FSID = uls->getID(); + + std::string dbName = std::get<0>(_ulsDatabaseList[uls->getDBIdx()]); + + int numPR = uls->getNumPR(); + for (int segIdx = 0; segIdx < numPR + 1; ++segIdx) { + // Instantiate cone object + std::unique_ptr coneFeature( + OGRFeature::CreateFeature(coneLayer->GetLayerDefn())); + + Vector3 ulsTxPosn = (segIdx == 0 ? uls->getTxPosition() : + uls->getPR(segIdx - 1).positionTx); + double ulsTxLongitude = (segIdx == 0 ? uls->getTxLongitudeDeg() : + uls->getPR(segIdx - 1).longitudeDeg); + double ulsTxLatitude = (segIdx == 0 ? uls->getTxLatitudeDeg() : + uls->getPR(segIdx - 1).latitudeDeg); + // double ulsTxHeight = (segIdx == 0 ? uls->getTxHeightAMSL() : + // uls->getPR(segIdx-1).heightAMSLTx); + + Vector3 ulsRxPosn = (segIdx == numPR ? uls->getRxPosition() : + uls->getPR(segIdx).positionRx); + double ulsRxLongitude = (segIdx == numPR ? uls->getRxLongitudeDeg() : + uls->getPR(segIdx).longitudeDeg); + double ulsRxLatitude = (segIdx == numPR ? uls->getRxLatitudeDeg() : + uls->getPR(segIdx).latitudeDeg); + // double ulsRxHeight = (segIdx == numPR ? uls->getRxHeightAMSL() : + // uls->getPR(segIdx).heightAMSLRx); + + bool txLocFlag = (!std::isnan(ulsTxPosn.x())) && + (!std::isnan(ulsTxPosn.y())) && + (!std::isnan(ulsTxPosn.z())); + + double linkDistKm; + if (!txLocFlag) { + linkDistKm = 1.0; + Vector3 segPointing = (segIdx == numPR ? + uls->getAntennaPointing() : + uls->getPR(segIdx).pointing); + ulsTxPosn = ulsRxPosn + linkDistKm * segPointing; + + GeodeticCoord ulsTxPosnGeodetic = EcefModel::ecefToGeodetic( + ulsTxPosn); + ulsTxLongitude = ulsTxPosnGeodetic.longitudeDeg; + ulsTxLatitude = ulsTxPosnGeodetic.latitudeDeg; + } else { + linkDistKm = (ulsTxPosn - ulsRxPosn).len(); + } + + LatLon ulsRxLatLonVal = std::make_pair(ulsRxLatitude, ulsRxLongitude); + LatLon ulsTxLatLonVal = std::make_pair(ulsTxLatitude, ulsTxLongitude); + + // Compute the beam coordinates and store into DoublePairs + std::tie(FSLatLonVal, posPointLatLon, negPointLatLon) = + computeBeamConeLatLon(uls, ulsRxLatLonVal, ulsTxLatLonVal); + + // Intantiate unique-pointers to OGRPolygon and OGRLinearRing for storing + // the beam coverage + GdalHelpers::GeomUniquePtr beamCone( + GdalHelpers::createGeometry< + OGRPolygon>()); // Use GdalHelpers.h templates to have + // unique pointers create these on the heap + GdalHelpers::GeomUniquePtr exteriorOfCone( + GdalHelpers::createGeometry()); + + // Create OGRPoints to store the coordinates of the beam triangle + // ***IMPORTANT NOTE: Coordinates stored as (Lon,Lat) here, required by + // geoJSON specifications + GdalHelpers::GeomUniquePtr FSPoint( + GdalHelpers::createGeometry()); + FSPoint->setX(FSLatLonVal.second); + FSPoint->setY( + FSLatLonVal + .first); // Must set points manually since cannot construct + // while pointing at with unique pointer + GdalHelpers::GeomUniquePtr posPoint( + GdalHelpers::createGeometry()); + posPoint->setX(posPointLatLon.second); + posPoint->setY(posPointLatLon.first); + GdalHelpers::GeomUniquePtr negPoint( + GdalHelpers::createGeometry()); + negPoint->setX(negPointLatLon.second); + negPoint->setY(negPointLatLon.first); + + // Adding the polygon vertices to a OGRLinearRing object for geoJSON export + exteriorOfCone->addPoint( + FSPoint.get()); // Using .get() gives access to the pointer without + // giving up ownership + exteriorOfCone->addPoint(posPoint.get()); + exteriorOfCone->addPoint(negPoint.get()); + exteriorOfCone->addPoint( + FSPoint.get()); // Adds this again just so that the polygon closes + // where it starts (at FS location) + + // Add exterior boundary of cone's polygon + beamCone->addRingDirectly( + exteriorOfCone + .release()); // Points unique-pointer to null and gives up + // ownership of exteriorOfCone to beamCone + + // Add properties to the geoJSON features + coneFeature->SetField("FSID", FSID); + coneFeature->SetField("SEGMENT", segIdx); + coneFeature->SetField("DBNAME", dbName.c_str()); + coneFeature->SetField("kind", "FS"); + coneFeature->SetField("startFreq", uls->getStartFreq() / 1.0e6); + coneFeature->SetField("stopFreq", uls->getStopFreq() / 1.0e6); + /* coneFeature->SetField("FS Lon", FSLatLonVal.second); + * coneFeature->SetField("FS lat", FSLatLonVal.first);*/ + + // Add geometry to feature + coneFeature->SetGeometryDirectly(beamCone.release()); + + if (coneLayer->CreateFeature(coneFeature.release()) != OGRERR_NONE) { + throw std::runtime_error("Could not add cone feature in layer of " + "output data source"); + } + } + } + if (_rlanAntenna) { + // Instantiate cone object + std::unique_ptr coneFeature( + OGRFeature::CreateFeature(coneLayer->GetLayerDefn())); + + double rlanAntennaArrowLength = 1000.0; + Vector3 centerPosn; + double centerLon, centerLat; + if (_analysisType == "HeatmapAnalysis") { + centerPosn = _heatmapRLANCenterPosn; + centerLon = _heatmapRLANCenterLon; + centerLat = _heatmapRLANCenterLat; + } else { + centerPosn = _rlanRegion->getCenterPosn(); + centerLon = _rlanRegion->getCenterLongitude(); + centerLat = _rlanRegion->getCenterLatitude(); + } + Vector3 ptgPosn = centerPosn + (rlanAntennaArrowLength / 1000.0) * _rlanPointing; + + GeodeticCoord ptgPosnGeodetic = EcefModel::ecefToGeodetic(ptgPosn); + + LatLon rlanLatLonVal = std::make_pair(centerLat, centerLon); + LatLon ptgLatLonVal = std::make_pair(ptgPosnGeodetic.latitudeDeg, + ptgPosnGeodetic.longitudeDeg); + + double cosVal = cos(_rlanRegion->getCenterLatitude() * M_PI / 180.0); + double cvgTheta = 2.0 * M_PI / 180.0; + double deltaLat = ptgLatLonVal.first - rlanLatLonVal.first; + double deltaLon = ptgLatLonVal.second - rlanLatLonVal.second; + posPointLatLon = std::make_pair(rlanLatLonVal.first + deltaLat * cos(cvgTheta) + + deltaLon * cosVal * sin(cvgTheta), + rlanLatLonVal.second + + (deltaLon * cosVal * cos(cvgTheta) - + deltaLat * sin(cvgTheta)) / + cosVal); + negPointLatLon = std::make_pair(rlanLatLonVal.first + deltaLat * cos(cvgTheta) - + deltaLon * cosVal * sin(cvgTheta), + rlanLatLonVal.second + + (deltaLon * cosVal * cos(cvgTheta) + + deltaLat * sin(cvgTheta)) / + cosVal); + + // Intantiate unique-pointers to OGRPolygon and OGRLinearRing for storing the beam + // coverage + GdalHelpers::GeomUniquePtr beamCone( + GdalHelpers::createGeometry()); // Use GdalHelpers.h templates + // to have unique pointers + // create these on the heap + GdalHelpers::GeomUniquePtr exteriorOfCone( + GdalHelpers::createGeometry()); + + // Create OGRPoints to store the coordinates of the beam triangle + // ***IMPORTANT NOTE: Coordinates stored as (Lon,Lat) here, required by geoJSON + // specifications + GdalHelpers::GeomUniquePtr rlanPoint( + GdalHelpers::createGeometry()); + rlanPoint->setX(rlanLatLonVal.second); + rlanPoint->setY( + rlanLatLonVal.first); // Must set points manually since cannot construct + // while pointing at with unique pointer + GdalHelpers::GeomUniquePtr posPoint( + GdalHelpers::createGeometry()); + posPoint->setX(posPointLatLon.second); + posPoint->setY(posPointLatLon.first); + GdalHelpers::GeomUniquePtr negPoint( + GdalHelpers::createGeometry()); + negPoint->setX(negPointLatLon.second); + negPoint->setY(negPointLatLon.first); + + // Adding the polygon vertices to a OGRLinearRing object for geoJSON export + exteriorOfCone->addPoint(rlanPoint.get()); // Using .get() gives access to the + // pointer without giving up ownership + exteriorOfCone->addPoint(posPoint.get()); + exteriorOfCone->addPoint(negPoint.get()); + exteriorOfCone->addPoint( + rlanPoint.get()); // Adds this again just so that the polygon closes where + // it starts (at FS location) + + // Add exterior boundary of cone's polygon + beamCone->addRingDirectly( + exteriorOfCone.release()); // Points unique-pointer to null and gives up + // ownership of exteriorOfCone to beamCone + + // Add properties to the geoJSON features + coneFeature->SetField("FSID", 0); + coneFeature->SetField("SEGMENT", 0); + coneFeature->SetField("DBNAME", "0"); + coneFeature->SetField("kind", "RLAN"); + coneFeature->SetField("startFreq", 0.0); + coneFeature->SetField("stopFreq", 0.0); + + // Add geometry to feature + coneFeature->SetGeometryDirectly(beamCone.release()); + + if (coneLayer->CreateFeature(coneFeature.release()) != OGRERR_NONE) { + throw std::runtime_error("Could not add cone feature in layer of output " + "data source"); + } + } + + // add building database raster boundary if loaded + addBuildingDatabaseTiles(coneLayer); + + // add denied regions + addDeniedRegions(coneLayer); + + if (_analysisType == "HeatmapAnalysis") { + addHeatmap(coneLayer); + } + + // Allocation clean-up + GDALClose(dataSet); // Remove the reference to the dataset + + // Create file to be written to (creates a file, a json object, and a json document in order + // to store) + std::ifstream tempFileStream(tempOutFilePath.toStdString(), std::ifstream::in); + const std::string geoJsonCollection = slurp(tempFileStream); + + QJsonDocument geoJsonDoc = QJsonDocument::fromJson( + QString::fromStdString(geoJsonCollection).toUtf8()); + QJsonObject geoJsonObj = geoJsonDoc.object(); + + QJsonObject analysisJsonObj = {{"geoJson", geoJsonObj}}; + + outputDocument = QJsonDocument(analysisJsonObj); + + // Write map data GEOJSON file + std::string fullPathMapDataFile = QDir(QString::fromStdString(tempDir)) + .filePath(QString::fromStdString( + _mapDataGeoJsonFile)) + .toStdString(); + auto mapDataFile = FileHelpers::open(QString::fromStdString(fullPathMapDataFile), + QIODevice::WriteOnly); + auto gzip_writer = new GzipStream(mapDataFile.get()); + if (!gzip_writer->open(QIODevice::WriteOnly)) { + throw std::runtime_error("Gzip failed to open."); + } + gzip_writer->write(outputDocument.toJson()); + gzip_writer->close(); + LOGGER_DEBUG(logger) << "Output file written to " << mapDataFile->fileName().toStdString(); +} + +#if 0 +// Convert channel data into a valid JSON Object +// represents the available channels for an analysis +// returns a QJson array with channel data contained +QJsonArray jsonChannelData(const std::vector &channelList) +{ + // get a list of the bandwidths used + auto initialPair = std::make_pair(channelList.front().bandwidth(), std::vector()); + auto rlanBWList = std::vector>> { initialPair }; + for (const ChannelStruct &channel : channelList) + { + bool found = false; + for (auto& band : rlanBWList) + { + if (channel.bandwidth() == band.first) + { + band.second.push_back(channel); + found = true; + break; + } + } + if (!found) + { + auto newPair = std::make_pair(channel.bandwidth(), std::vector { channel }); + rlanBWList.push_back(newPair); + } + } + + QJsonArray array = QJsonArray(); + + std::map channelNameStart; + channelNameStart[20] = 1; + channelNameStart[40] = 3; + channelNameStart[80] = 7; + channelNameStart[160] = 15; + channelNameStart[320] = 31; + std::map channelNameStep; + channelNameStep[20] = 4; + channelNameStep[40] = 8; + channelNameStep[80] = 16; + channelNameStep[160] = 32; + channelNameStep[320] = 32; + + for (const auto &channelGroup : rlanBWList) + { + QJsonObject rowObj = QJsonObject(); + rowObj.insert(QString("channelWidth"), QJsonValue(channelGroup.first)); + QJsonArray channels = QJsonArray(); + + LOGGER_DEBUG(logger) << "Adding Channel Width: " << channelGroup.first << " MHz" << '\n' + << "with " << channelGroup.second.size() << " channels"; + for (int chanIdx = 0; chanIdx < (int) channelGroup.second.size(); chanIdx++) + { + ChannelStruct props = channelGroup.second.at(chanIdx); + + std::string color; + switch (props.availability) + { + case GREEN: + color = "green"; + break; + case YELLOW: + color = "yellow"; + break; + case RED: + color = "red"; + break; + case BLACK: + color = "black"; + break; + default: + throw std::invalid_argument("Invalid channel color"); + } + + channels.push_back(QJsonObject{ + {"color", QJsonValue(color.c_str())}, + {"maxEIRP", QJsonValue(props.eirpLimit_dBm)}, + {"name", QJsonValue(channelNameStart.at(channelGroup.first) + channelNameStep.at(channelGroup.first) * chanIdx)}}); + } + + rowObj.insert(QString("channels"), channels); + array.push_back(rowObj); + } + return array; +} +#endif + +#if 0 +// Converts spectrum data into a valid PAWS response +QJsonObject jsonSpectrumData(const std::vector &channelList, const QJsonObject &deviceDesc, const double &startFreq) +{ + // get a list of the bandwidths used and group the channels + auto initialPair = std::make_pair(channelList.front().bandwidth(), std::vector()); + auto rlanBWList = std::vector>> { initialPair }; + for (const ChannelStruct &channel : channelList) + { + bool found = false; + for (auto& band : rlanBWList) + { + if (channel.bandwidth() == band.first) + { + band.second.push_back(channel); + found = true; + break; + } + } + if (!found) + { + auto newPair = std::make_pair(channel.bandwidth(), std::vector { channel }); + rlanBWList.push_back(newPair); + } + } + + QJsonArray spectra = QJsonArray(); + + // iterate over bandwidths (rows) + for (const auto &bandwidth : rlanBWList) + { + QJsonArray parentProfiles = QJsonArray(); + QJsonArray profiles = QJsonArray(); + // iterate over channels that use the same bandwidth + for (ChannelStruct channel : bandwidth.second) + { + // add two points for each channel to make a step function + profiles.push_back(QJsonObject({// start of channel + {"hz", QJsonValue(channel.startFreqMHz * 1e6)}, + {"dbm", QJsonValue(channel.eirpLimit_dBm)}})); + profiles.push_back(QJsonObject({// end of channel + {"hz", QJsonValue(channel.stopFreqMHz * 1e6)}, + {"dbm", QJsonValue(channel.eirpLimit_dBm)}})); + } + + parentProfiles.push_back(profiles); + QJsonObject spectrum = QJsonObject{ + { "resolutionBwHz", bandwidth.first * 1e6 }, + { "profiles", parentProfiles } + }; + spectra.push_back(spectrum); + } + + // add properties to return object + // most of it (esspecially for phase 1) is static + QJsonObject result = QJsonObject{ + {"type", "AVAIL_SPECTRUM_RESP"}, + {"version", "1.0"}, + {"timestamp", ISO8601TimeUTC() }, + {"deviceDesc", deviceDesc}, + {"spectrumSpecs", + QJsonArray{ + QJsonObject{ + {"rulesetInfo", + QJsonObject + { + {"authority", "US"}, + {"rulesetId", "AFC-6GHZ-DEMO-1.0"} + } + }, + {"spectrumSchedules", + QJsonArray + { + QJsonObject + { + {"eventTime", + QJsonObject + { + {"startTime", ISO8601TimeUTC() }, + {"stopTime", ISO8601TimeUTC(1) } + } + }, + {"spectra", spectra} // add generated spectra here + } + } + } + } + } + } + }; + + return result; +} +#endif + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::getAngleFromDMS ****/ +/**** Process DMS string and return angle (lat or lon) in deg. ****/ +/******************************************************************************************/ +double AfcManager::getAngleFromDMS(std::string dmsStr) +{ + char *chptr; + double angleDeg; + + bool error = false; + + std::size_t dashPosn1; + std::size_t dashPosn2; + std::size_t letterPosn; + + dashPosn1 = dmsStr.find('-'); + if ((dashPosn1 == std::string::npos) || (dashPosn1 == 0)) { + // Angle is in decimal format, not DMS + angleDeg = strtod(dmsStr.c_str(), &chptr); + } else { + if (!error) { + dashPosn2 = dmsStr.find('-', dashPosn1 + 1); + if (dashPosn2 == std::string::npos) { + error = true; + } + } + + double dVal, mVal, sVal; + if (!error) { + letterPosn = dmsStr.find_first_of("NEWS", dashPosn2 + 1); + + std::string dStr = dmsStr.substr(0, dashPosn1); + std::string mStr = dmsStr.substr(dashPosn1 + 1, dashPosn2 - dashPosn1 - 1); + std::string sStr = ((letterPosn == std::string::npos) ? + dmsStr.substr(dashPosn2 + 1) : + dmsStr.substr(dashPosn2 + 1, + letterPosn - dashPosn2 - 1)); + + dVal = strtod(dStr.c_str(), &chptr); + mVal = strtod(mStr.c_str(), &chptr); + sVal = strtod(sStr.c_str(), &chptr); + } + + if (error) { + throw std::runtime_error(ErrStream() << "ERROR: Unable to convert DMS " + "string to angle, DMS string = \"" + << dmsStr << "\""); + } + + angleDeg = dVal + (mVal + sVal / 60.0) / 60.0; + + if (letterPosn != std::string::npos) { + if ((dmsStr.at(letterPosn) == 'W') || (dmsStr.at(letterPosn) == 'S')) { + angleDeg *= -1; + } + } + } + + return (angleDeg); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::findULSAntenna() ****/ +/******************************************************************************************/ +int AfcManager::findULSAntenna(std::string strval) +{ + int antIdx = -1; + bool found = false; + + for (int aIdx = 0; (aIdx < (int)_antennaList.size()) && (!found); aIdx++) { + if (std::string(_antennaList[aIdx]->get_strid()) == strval) { + found = true; + antIdx = aIdx; + } + } + + return (antIdx); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** AfcManager::findULSID ****/ +/******************************************************************************************/ +ULSClass *AfcManager::findULSID(int ulsID, int dbIdx, int &ulsIdx) +{ + int ulsIdxA, ulsIdxB; + int id, db, idA, idB, dbIdxA, dbIdxB; + ULSClass *uls = (ULSClass *)NULL; + ulsIdx = -1; + + bool found = false; + + ulsIdxA = 0; + idA = (*_ulsList)[ulsIdxA]->getID(); + dbIdxA = (*_ulsList)[ulsIdxA]->getDBIdx(); + if ((idA == ulsID) && (dbIdxA == dbIdx)) { + found = true; + ulsIdx = ulsIdxA; + uls = (*_ulsList)[ulsIdx]; + } else if ((dbIdx < dbIdxA) || ((dbIdx == dbIdxA) && (ulsID < idA))) { + throw std::runtime_error(ErrStream() << "ERROR: Invalid DBIDX = " << dbIdx + << " FSID = " << ulsID); + } + + ulsIdxB = _ulsList->getSize() - 1; + ; + idB = (*_ulsList)[ulsIdxB]->getID(); + dbIdxB = (*_ulsList)[ulsIdxB]->getDBIdx(); + if ((idB == ulsID) && (dbIdxB == dbIdx)) { + found = true; + ulsIdx = ulsIdxB; + uls = (*_ulsList)[ulsIdx]; + } else if ((dbIdx > dbIdxB) || ((dbIdx == dbIdxB) && (ulsID > idB))) { + throw std::runtime_error(ErrStream() << "ERROR: Invalid DBIDX = " << dbIdx + << " FSID = " << ulsID); + } + + while ((ulsIdxA + 1 < ulsIdxB) && (!found)) { + ulsIdx = (ulsIdxA + ulsIdxB) / 2; + id = (*_ulsList)[ulsIdx]->getID(); + db = (*_ulsList)[ulsIdx]->getDBIdx(); + if ((ulsID == id) && (dbIdx == db)) { + found = true; + uls = (*_ulsList)[ulsIdx]; + } else if ((dbIdx > db) || ((dbIdx == db) && (ulsID > id))) { + ulsIdxA = ulsIdx; + idA = id; + } else { + ulsIdxB = ulsIdx; + idB = id; + } + } + + if (!found) { + uls = (ULSClass *)NULL; + ulsIdx = -1; + } + + return (uls); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** AfcManager::computeBeamConeLatLon ****/ +/******************************************************************************************/ +// Calculates and stores the beam cone coordinates +std::tuple AfcManager::computeBeamConeLatLon(ULSClass *uls, + LatLon rxLatLonVal, + LatLon txLatLonVal) +{ + // Store lat/lon from ULS class + LatLon FSLatLonVal = rxLatLonVal; + + std::make_pair(uls->getRxLatitudeDeg(), uls->getRxLongitudeDeg()); + + // Obtain the angle in radians for 3 dB attenuation + double theta_rad = uls->computeBeamWidth(3.0) * M_PI / 180; + double cosTheta = cos(theta_rad); + double sinTheta = sin(theta_rad); + + double cosVal = cos(rxLatLonVal.first * M_PI / 180.0); + double deltaX = (txLatLonVal.second - rxLatLonVal.second) * cosVal; + double deltaY = txLatLonVal.first - rxLatLonVal.first; + + double deltaLon1 = (deltaX * cosTheta + deltaY * sinTheta) / cosVal; + double deltaLat1 = (deltaY * cosTheta - deltaX * sinTheta); + + double deltaLon2 = (deltaX * cosTheta - deltaY * sinTheta) / cosVal; + double deltaLat2 = (deltaY * cosTheta + deltaX * sinTheta); + + // Store in DoublePairs + LatLon posPointLatLon = std::make_pair(rxLatLonVal.first + deltaLat1, + rxLatLonVal.second + deltaLon1); + LatLon negPointLatLon = std::make_pair(rxLatLonVal.first + deltaLat2, + rxLatLonVal.second + deltaLon2); + + // Return as a tuple + return std::make_tuple(FSLatLonVal, posPointLatLon, negPointLatLon); +} + +/******************************************************************************************/ +/**** inline function aciFn() used only in computeSpectralOverlapLoss ****/ +/******************************************************************************************/ +inline double aciFn(double fMHz, double BMHz) +{ + double overlap; + double fabsMHz = fabs(fMHz); + int sign; + + if (fMHz < 0.0) { + sign = -1; + } else if (fMHz > 0.0) { + sign = 1; + } else { + return 0.0; + } + + // LOGGER_INFO(logger) << "ACIFN: =========================="; + // LOGGER_INFO(logger) << "ACIFN: fMHz = " << fMHz; + // LOGGER_INFO(logger) << "ACIFN: BMHz = " << BMHz; + + if (fabsMHz <= BMHz / 2) { + overlap = fabsMHz; + } else { + overlap = BMHz / 2; + } + + // LOGGER_INFO(logger) << "ACIFN: overlap_0 = " << overlap; + + if (fabsMHz > BMHz / 2) { + if (fabsMHz <= BMHz / 2 + 1.0) { + overlap += (1.0 - exp(log(10.0) * (BMHz - 2 * fabsMHz))) / (2 * log(10.0)); + } else { + overlap += 0.99 / (2 * log(10.0)); + } + } + + // LOGGER_INFO(logger) << "ACIFN: overlap_1 = " << overlap; + + if (fabsMHz > BMHz / 2 + 1.0) { + if (fabsMHz <= BMHz) { + overlap += exp(log(10.0) * (-6 * BMHz + 28.0) / (5 * BMHz - 10.0)) * + (exp(log(10.0) * ((-8.0) / (5 * BMHz - 10.0)) * + (BMHz / 2 + 1.0)) - + exp(log(10.0) * ((-8.0 * fabsMHz) / (5 * BMHz - 10.0)))) / + ((8.0 * log(10.0)) / (5 * BMHz - 10.0)); + } else { + overlap += exp(log(10.0) * (-6 * BMHz + 28.0) / (5 * BMHz - 10.0)) * + (exp(log(10.0) * ((-8.0) / (5 * BMHz - 10.0)) * + (BMHz / 2 + 1.0)) - + exp(log(10.0) * ((-8.0 * BMHz) / (5 * BMHz - 10.0)))) / + ((8.0 * log(10.0)) / (5 * BMHz - 10.0)); + } + } + + // LOGGER_INFO(logger) << "ACIFN: overlap_2 = " << overlap; + + if (fabsMHz > BMHz) { + if (fabsMHz <= 3 * BMHz / 2) { + overlap += exp(-log(10.0) * 0.4) * + (exp(log(10.0) * (-2.4)) - + exp(log(10.0) * (-2.4 * fabsMHz / BMHz))) / + ((2.4 * log(10.0) / BMHz)); + } else { + overlap += exp(-log(10.0) * 0.4) * + (exp(log(10.0) * (-2.4)) - exp(log(10.0) * (-3.6))) / + ((2.4 * log(10.0) / BMHz)); + } + } + + // LOGGER_INFO(logger) << "ACIFN: overlap_3 = " << overlap; + + overlap = sign * overlap / BMHz; + + // LOGGER_INFO(logger) << "ACIFN: overlap = " << overlap; + // LOGGER_INFO(logger) << "ACIFN: =========================="; + + // printf("ACIFN: %12.10f %12.10f %12.10f\n", fMHz, BMHz, overlap); + + return overlap; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** AfcManager::computeSpectralOverlapLoss ****/ +/******************************************************************************************/ +bool AfcManager::computeSpectralOverlapLoss(double *spectralOverlapLossDBptr, + double sigStartFreq, + double sigStopFreq, + double rxStartFreq, + double rxStopFreq, + bool aciFlag, + CConst::SpectralAlgorithmEnum spectralAlgorithm) const +{ + bool hasOverlap; + + if (!aciFlag) { + if ((sigStopFreq <= rxStartFreq) || (sigStartFreq >= rxStopFreq)) { + hasOverlap = false; + if (spectralOverlapLossDBptr) { + *spectralOverlapLossDBptr = + -std::numeric_limits::infinity(); + } + } else { + hasOverlap = true; + if (spectralOverlapLossDBptr) { + double f1 = (sigStartFreq < rxStartFreq ? rxStartFreq : + sigStartFreq); + double f2 = (sigStopFreq > rxStopFreq ? rxStopFreq : sigStopFreq); + double overlap; + if (spectralAlgorithm == CConst::pwrSpectralAlgorithm) { + overlap = (f2 - f1) / (sigStopFreq - sigStartFreq); + } else { + overlap = (rxStopFreq - rxStartFreq) / + (sigStopFreq - sigStartFreq); + } + *spectralOverlapLossDBptr = -10.0 * log(overlap) / log(10.0); + } + } + } else { + if ((2 * sigStopFreq - sigStartFreq <= rxStartFreq) || + (2 * sigStartFreq - sigStopFreq >= rxStopFreq)) { + hasOverlap = false; + if (spectralOverlapLossDBptr) { + *spectralOverlapLossDBptr = + -std::numeric_limits::infinity(); + } + } else { + hasOverlap = true; + if (spectralOverlapLossDBptr) { + double BMHz = (sigStopFreq - sigStartFreq) * 1.0e-6; + double fStartMHz = (rxStartFreq - + (sigStartFreq + sigStopFreq) / 2) * + 1.0e-6; + double fStopMHz = (rxStopFreq - (sigStartFreq + sigStopFreq) / 2) * + 1.0e-6; + if (spectralAlgorithm == CConst::pwrSpectralAlgorithm) { + double overlap = aciFn(fStopMHz, BMHz) - + aciFn(fStartMHz, BMHz); + *spectralOverlapLossDBptr = -10.0 * log(overlap) / + log(10.0); + } else { + double fCrit; + if ((fStartMHz <= 0.0) && (fStopMHz >= 0.0)) { + fCrit = 0.0; + } else { + fCrit = std::min(fabs(fStartMHz), fabs(fStopMHz)); + } + double psdDB; + if (fCrit < BMHz / 2) { + psdDB = 0.0; + } else if (fCrit < BMHz / 2 + 1.0) { + psdDB = -20.0 * (fCrit - BMHz / 2); + } else if (fCrit < BMHz) { + psdDB = (-20.0 * (BMHz - fCrit) - + 28.0 * (fCrit - BMHz / 2 - 1.0)) / + (BMHz / 2 - 1.0); + } else { + psdDB = (-28.0 * (3 * BMHz / 2 - fCrit) - + 40.0 * (fCrit - BMHz)) / + (BMHz / 2); + } + double overlap = (rxStopFreq - rxStartFreq) / + (sigStopFreq - sigStartFreq); + *spectralOverlapLossDBptr = -psdDB - + 10.0 * log(overlap) / log(10.0); + } + } + } + } + + return (hasOverlap); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** AfcManager::readULSData() ****/ +/**** linkDirection: 0: RX ****/ +/**** 1: TX ****/ +/**** 2: RX and TX ****/ +/******************************************************************************************/ +void AfcManager::readULSData( + const std::vector> &ulsDatabaseList, + PopGridClass *popGridVal, + int linkDirection, + double minFreq, + double maxFreq, + bool removeMobileFlag, + CConst::SimulationEnum simulationFlag, + const double &minLat, + const double &maxLat, + const double &minLon, + const double &maxLon) +{ + AnomalyGzipCsv anomGc(_fsAnomFile); + + int prIdx; + int dbIdx; + bool prevGdalDirectMode = (_terrainDataModel ? _terrainDataModel->setGdalDirectMode(true) : + false); + for (dbIdx = 0; dbIdx < (int)ulsDatabaseList.size(); ++dbIdx) { + std::string name = std::get<0>(ulsDatabaseList[dbIdx]); + std::string filename = std::get<1>(ulsDatabaseList[dbIdx]); + + LOGGER_INFO(logger) << "Reading " << name << " ULS Database: " << filename; + int linenum; + // char *chptr; + std::string str; + std::string reasonIgnored; + // std::size_t strpos; + + bool fixAnomalousEntries = false; + + int numIgnoreInvalid = 0; + int numIgnoreOutOfBand = 0; + int numIgnoreOutsideSimulationRegion = 0; + int numIgnoreMobile = 0; + int numFixed = 0; + int numValid = 0; + + int fsid = -1; + std::string callsign; + int pathNumber; + std::string radioService; + std::string entityName; + std::string rxCallsign; + int rxAntennaNumber; + int numPR; + std::string frequencyAssigned; + double startFreq, stopFreq; + double rxLatitudeDeg, rxLongitudeDeg; + double rxGroundElevation; + double rxHeightAboveTerrain; + double txLatitudeDeg, txLongitudeDeg; + double txGroundElevation; + std::string txPolarization; + double txHeightAboveTerrain; + double azimuthAngleToTx; + double elevationAngleToTx; + double rxGain; + double rxAntennaDiameter; + double rxNearFieldAntDiameter; + double rxNearFieldDistLimit; + double rxNearFieldAntEfficiency; + CConst::AntennaCategoryEnum rxAntennaCategory; + double txGain; + double txEIRP; + CConst::ULSAntennaTypeEnum rxAntennaType; + CConst::ULSAntennaTypeEnum txAntennaType; + // double operatingRadius; + // double rxSensitivity; + + bool hasDiversity; + double diversityHeightAboveTerrain; + double diversityGain; + double diversityAntennaDiameter; + + AntennaClass *rxAntenna = (AntennaClass *)NULL; + AntennaClass *txAntenna = (AntennaClass *)NULL; + double fadeMarginDB; + std::string status; + + ULSClass *uls; + UlsDatabase *ulsDatabase = new UlsDatabase(); + + if (filename.empty()) { + throw std::runtime_error("ERROR: No ULS data file specified"); + } + + int numUrbanULS = 0; + int numSuburbanULS = 0; + int numRuralULS = 0; + int numBarrenULS = 0; + + LOGGER_INFO(logger) << "Analysis Band: [" << minFreq << ", " << maxFreq << "]"; + + linenum = 0; + + std::vector rows = std::vector(); + if (_analysisType == "ExclusionZoneAnalysis") { + // can also use UlsDatabase::getFSById(QString, int) to get a single record + // only by Id + ulsDatabase->loadFSById(QString::fromStdString(filename), + _deniedRegionList, + _antennaList, + rows, + _exclusionZoneFSID); + } else if ((_analysisType == "HeatmapAnalysis") && (_heatmapFSID != -1)) { + ulsDatabase->loadFSById(QString::fromStdString(filename), + _deniedRegionList, + _antennaList, + rows, + _heatmapFSID); + if (rows.size() == 0) { + LOGGER_WARN(logger) << "GENERAL FAILURE: invalid FSID specified " + "for heatmap analysis: " + << _heatmapFSID; + _responseCode = CConst::generalFailureResponseCode; + return; + } + } else { + ulsDatabase->loadUlsData(QString::fromStdString(filename), + _deniedRegionList, + _antennaList, + rows, + minLat, + maxLat, + minLon, + maxLon); + } + // Distributing FS TX by 1x1 degree squares to minimize GDAL reopening + std::sort(rows.begin(), rows.end(), [](const UlsRecord &rl, const UlsRecord &rr) { + if (std::isnan(rl.txLatitudeDeg) || std::isnan(rl.txLongitudeDeg)) { + return false; + } else if (std::isnan(rr.txLatitudeDeg) || std::isnan(rr.txLongitudeDeg)) { + return true; + } else { + double latDegL = std::floor(rl.txLatitudeDeg); + double latDegR = std::floor(rr.txLatitudeDeg); + if (latDegL != latDegR) { + return latDegL < latDegR; + } + double lonDegL = std::floor(rl.txLongitudeDeg); + double lonDegR = std::floor(rr.txLongitudeDeg); + if (lonDegL != lonDegR) { + return lonDegL < lonDegR; + } + return false; + } + }); + + for (UlsRecord row : rows) { + linenum++; + bool ignoreFlag = false; + bool fixedFlag = false; + std::string fixedStr = ""; + char rxPropEnv, txPropEnv; + + radioService = row.radioService; + entityName = row.entityName; + for (auto &c : entityName) + c = toupper(c); + + /**************************************************************************/ + /* FSID */ + /**************************************************************************/ + fsid = row.fsid; + /**************************************************************************/ + + /**************************************************************************/ + /* callsign, rxCallsign */ + /**************************************************************************/ + callsign = row.callsign; + rxCallsign = row.rxCallsign; + pathNumber = row.pathNumber; + /**************************************************************************/ + + /**************************************************************************/ + /* rxAntennaNumber */ + /**************************************************************************/ + rxAntennaNumber = row.rxAntennaNumber; + /**************************************************************************/ + + /**************************************************************************/ + /* frequencyAssigned => startFreq, stopFreq */ + /**************************************************************************/ + + if (!_filterSimRegionOnly) { + if (!ignoreFlag) { + startFreq = row.startFreq * 1.0e6; + stopFreq = row.stopFreq * 1.0e6; + + if (stopFreq < startFreq) { + throw std::runtime_error( + ErrStream() + << "ERROR reading ULS data: FSID = " << fsid + << ", startFreq = " << startFreq + << ", stopFreq = " << stopFreq + << ", must have startFreq < stopFreq"); + } + } + + if (!ignoreFlag) { + if ((stopFreq <= minFreq) || (startFreq >= maxFreq)) { + ignoreFlag = true; + reasonIgnored = "out of analysis band"; + numIgnoreOutOfBand++; + } + } + } + /**************************************************************************/ + + /**************************************************************************/ + /* Remove mobile ULS entries */ + /**************************************************************************/ + if ((!_filterSimRegionOnly) && (removeMobileFlag) && (!ignoreFlag)) { + if ((row.mobile || (radioService == "TP") || + ((startFreq < 6525.0e6) && (stopFreq > 6425.0e6)))) { + ignoreFlag = true; + reasonIgnored = "Mobile ULS entry"; + numIgnoreMobile++; + } + } + /**************************************************************************/ + + /**************************************************************************/ + /* rxLatCoords => rxLatitudeDeg */ + /**************************************************************************/ + if (!ignoreFlag) { + rxLatitudeDeg = row.rxLatitudeDeg; + + if (rxLatitudeDeg == 0.0) { + if ((linkDirection == 0) || (linkDirection == 2)) { + ignoreFlag = true; + reasonIgnored = "RX Latitude has value 0"; + numIgnoreInvalid++; + } else if (linkDirection == 1) { + reasonIgnored = "Ignored: Rx Latitude has value 0"; + ignoreFlag = true; + numIgnoreInvalid++; + } else { + throw std::runtime_error(ErrStream() + << "ERROR reading ULS " + "data: linkDirection = " + << linkDirection + << " INVALID value"); + } + } + } + /**************************************************************************/ + + /**************************************************************************/ + /* rxLongCoords => rxLongitudeDeg */ + /**************************************************************************/ + if (!ignoreFlag) { + rxLongitudeDeg = row.rxLongitudeDeg; + + if (rxLongitudeDeg == 0.0) { + if ((linkDirection == 0) || (linkDirection == 2)) { + ignoreFlag = true; + reasonIgnored = "RX Longitude has value 0"; + } else if (linkDirection == 1) { + reasonIgnored = "Ignored: Rx Longitude has value 0"; + ignoreFlag = true; + numIgnoreInvalid++; + } + numIgnoreInvalid++; + } + } + /**************************************************************************/ + + /**************************************************************************/ + /* Check rxLatitude and rxLongitude region defined by popGrid (SIMULATION + * REGION) */ + /**************************************************************************/ + if ((!ignoreFlag) && ((linkDirection == 0) || (linkDirection == 2)) && + (popGridVal)) { + int lonIdx; + int latIdx; + int regionIdx; + popGridVal->findDeg(rxLongitudeDeg, + rxLatitudeDeg, + lonIdx, + latIdx, + rxPropEnv, + regionIdx); + if ((rxPropEnv == 0) || (rxPropEnv == 'X')) { + ignoreFlag = true; + reasonIgnored = "RX outside SIMULATION REGION"; + numIgnoreOutsideSimulationRegion++; + } + } + /**************************************************************************/ + + /**************************************************************************/ + /* rxGroundElevation */ + /**************************************************************************/ + rxGroundElevation = row.rxGroundElevation; // could be NaN + /**************************************************************************/ + + /**************************************************************************/ + /* rxHeightAboveTerrain */ + /**************************************************************************/ + if (!_filterSimRegionOnly) { + rxHeightAboveTerrain = row.rxHeightAboveTerrain; + if (!ignoreFlag) { + if (std::isnan(rxHeightAboveTerrain)) { + bool fixedMissingRxHeight = false; + if (fixAnomalousEntries) { + if (!std::isnan(row.txHeightAboveTerrain)) { + txHeightAboveTerrain = + row.txHeightAboveTerrain; + if (txHeightAboveTerrain > 0.0) { + rxHeightAboveTerrain = + row.txHeightAboveTerrain; + fixedStr += "Fixed: " + "missing Rx " + "Height above " + "Terrain set " + "to Tx Height " + "above Terrain"; + fixedMissingRxHeight = true; + fixedFlag = true; + } else if (txHeightAboveTerrain == + 0.0) { + rxHeightAboveTerrain = 0.1; + fixedStr += + "Fixed: missing Rx " + "Height above " + "Terrain set to " + + std::to_string( + rxHeightAboveTerrain); + fixedMissingRxHeight = true; + fixedFlag = true; + } + } else { + if (radioService == "CF") { + rxHeightAboveTerrain = 39.3; + fixedStr += + "Fixed: missing Rx " + "Height above " + "Terrain for " + + radioService + + " set to " + + std::to_string( + rxHeightAboveTerrain); + fixedMissingRxHeight = true; + fixedFlag = true; + } else if (radioService == "MG") { + rxHeightAboveTerrain = 41.0; + fixedStr += + "Fixed: missing Rx " + "Height above " + "Terrain for " + + radioService + + " set to " + + std::to_string( + rxHeightAboveTerrain); + fixedMissingRxHeight = true; + fixedFlag = true; + } else if (radioService == "MW") { + rxHeightAboveTerrain = 39.9; + fixedStr += + "Fixed: missing Rx " + "Height above " + "Terrain for " + + radioService + + " set to " + + std::to_string( + rxHeightAboveTerrain); + fixedMissingRxHeight = true; + fixedFlag = true; + } else if (radioService == "TI") { + rxHeightAboveTerrain = 41.8; + fixedStr += + "Fixed: missing Rx " + "Height above " + "Terrain for " + + radioService + + " set to " + + std::to_string( + rxHeightAboveTerrain); + fixedMissingRxHeight = true; + fixedFlag = true; + } else if (radioService == "TP") { + rxHeightAboveTerrain = 30.0; + fixedStr += + "Fixed: missing Rx " + "Height above " + "Terrain for " + + radioService + + " set to " + + std::to_string( + rxHeightAboveTerrain); + fixedMissingRxHeight = true; + fixedFlag = true; + } else if (radioService == "TS") { + rxHeightAboveTerrain = 41.5; + fixedStr += + "Fixed: missing Rx " + "Height above " + "Terrain for " + + radioService + + " set to " + + std::to_string( + rxHeightAboveTerrain); + fixedMissingRxHeight = true; + fixedFlag = true; + } else if (radioService == "TT") { + rxHeightAboveTerrain = 42.1; + fixedStr += + "Fixed: missing Rx " + "Height above " + "Terrain for " + + radioService + + " set to " + + std::to_string( + rxHeightAboveTerrain); + fixedMissingRxHeight = true; + fixedFlag = true; + } + } + } + + if (!fixedMissingRxHeight) { + ignoreFlag = true; + reasonIgnored = "missing Rx Height above " + "Terrain"; + numIgnoreInvalid++; + } + } + } + + if (!ignoreFlag) { + if (rxHeightAboveTerrain < 3.0) { + if (fixAnomalousEntries) { + rxHeightAboveTerrain = 3.0; + fixedStr += "Fixed: Rx Height above " + "Terrain < 3.0 set to 3.0"; + fixedFlag = true; + } else { + LOGGER_WARN(logger) + << "WARNING: ULS data for FSID = " + << fsid + << ", rxHeightAboveTerrain = " + << rxHeightAboveTerrain + << " is < 3.0"; + } + } + } + } + /**************************************************************************/ + + /**************************************************************************/ + /* txLatCoords => txLatitudeDeg */ + /**************************************************************************/ + txLatitudeDeg = row.txLatitudeDeg; + /**************************************************************************/ + + /**************************************************************************/ + /* txLongCoords => txLongitudeDeg */ + /**************************************************************************/ + txLongitudeDeg = row.txLongitudeDeg; + /**************************************************************************/ + + /**************************************************************************/ + /* txGroundElevation */ + /**************************************************************************/ + txGroundElevation = row.txGroundElevation; // may be NaN + /**************************************************************************/ + + /**************************************************************************/ + /* txPolarization */ + /**************************************************************************/ + txPolarization = row.txPolarization; + /**************************************************************************/ + + /**************************************************************************/ + /* txHeightAboveTerrain */ + /**************************************************************************/ + txHeightAboveTerrain = row.txHeightAboveTerrain; + /**************************************************************************/ + + /**************************************************************************/ + /* txLocFlag: true if the TX location is known, and false otherwise. */ + /* Note that Cadada does not provode TX location. */ + /**************************************************************************/ + bool txLocFlag = !(std::isnan(txLongitudeDeg) || + std::isnan(txLatitudeDeg) || + std::isnan(txHeightAboveTerrain)); + /**************************************************************************/ + + /**************************************************************************/ + /* Check txLatitude and txLongitude region defined by popGrid (SIMULATION + * REGION) */ + /**************************************************************************/ + if (txLocFlag && (!ignoreFlag) && + ((linkDirection == 1) || (linkDirection == 2)) && popGridVal) { + int lonIdx; + int latIdx; + int regionIdx; + popGridVal->findDeg(txLongitudeDeg, + txLatitudeDeg, + lonIdx, + latIdx, + txPropEnv, + regionIdx); + if ((txPropEnv == 0) || (txPropEnv == 'X')) { + ignoreFlag = true; + reasonIgnored = "TX outside SIMULATION REGION"; + numIgnoreOutsideSimulationRegion++; + } + } + /**************************************************************************/ + + /**************************************************************************/ + /* azimuthAngleToTx, elevationAngleToTx */ + /**************************************************************************/ + azimuthAngleToTx = row.azimuthAngleToTx; + elevationAngleToTx = row.elevationAngleToTx; + /**************************************************************************/ + + /**************************************************************************/ + /* txPtgFlag: true if pointing to TX location is known, false otherwise. */ + /* Note that Cadada specifies these parameters, US does not. */ + /**************************************************************************/ + bool txPtgFlag = !(std::isnan(azimuthAngleToTx) || + std::isnan(elevationAngleToTx)); + /**************************************************************************/ + + /**************************************************************************/ + /* If Tx location not provided and pointing to TX not provided, ignore */ + /**************************************************************************/ + if ((!txLocFlag) && (!txPtgFlag)) { + ignoreFlag = true; + reasonIgnored = "TX location and pointing both not specified"; + } + /**************************************************************************/ + + /**************************************************************************/ + /* numPR (Number of Passive Repeaters) */ + /**************************************************************************/ + numPR = row.numPR; + /**************************************************************************/ + + /**************************************************************************/ + /* Passive Repeater Parameters */ + /**************************************************************************/ + if ((!_filterSimRegionOnly) && (!ignoreFlag)) { + for (prIdx = 0; prIdx < numPR; ++prIdx) { + /******************************************************************/ + /* prLongitudeDeg */ + /******************************************************************/ + if (std::isnan(row.prLongitudeDeg[prIdx]) || + (row.prLongitudeDeg[prIdx] == 0.0)) { + reasonIgnored = "Ignored: PR Longitude has value " + "nan or 0"; + ignoreFlag = true; + numIgnoreInvalid++; + } + /******************************************************************/ + + /******************************************************************/ + /* prLatitudeDeg */ + /******************************************************************/ + if (std::isnan(row.prLatitudeDeg[prIdx]) || + (row.prLatitudeDeg[prIdx] == 0.0)) { + reasonIgnored = "Ignored: PR Latitude has value " + "nan or 0"; + ignoreFlag = true; + numIgnoreInvalid++; + } + /******************************************************************/ + + /******************************************************************/ + /* prHeightAboveTerrain */ + /******************************************************************/ + if (std::isnan(row.prHeightAboveTerrainRx[prIdx])) { + ignoreFlag = true; + reasonIgnored = "missing PR Height above Terrain"; + numIgnoreInvalid++; + } + + if (!ignoreFlag) { + if (row.prHeightAboveTerrainRx[prIdx] <= 0.0) { + LOGGER_WARN(logger) + << "WARNING: ULS data for FSID = " + << fsid << ", Passive Repeater " + << (prIdx + 1) + << ", prHeightAboveTerrainRx = " + << row.prHeightAboveTerrainRx[prIdx] + << " is < 0.0"; + } + } + + if (std::isnan(row.prHeightAboveTerrainTx[prIdx])) { + ignoreFlag = true; + reasonIgnored = "missing PR Height above Terrain"; + numIgnoreInvalid++; + } + + if (!ignoreFlag) { + if (row.prHeightAboveTerrainTx[prIdx] <= 0.0) { + LOGGER_WARN(logger) + << "WARNING: ULS data for FSID = " + << fsid << ", Passive Repeater " + << (prIdx + 1) + << ", prHeightAboveTerrainTx = " + << row.prHeightAboveTerrainTx[prIdx] + << " is < 0.0"; + } + } + /******************************************************************/ + } + } + /**************************************************************************/ + + if (!_filterSimRegionOnly) { + if ((linkDirection == 0) || (linkDirection == 2) || + (simulationFlag == CConst::MobileSimulation)) { + /**************************************************************************/ + /* rxGain */ + /**************************************************************************/ + rxGain = row.rxGain; + if (!ignoreFlag) { + if (std::isnan(rxGain)) { + if (fixAnomalousEntries) { + if (radioService == "CF") { + rxGain = 39.3; + fixedStr += + "Fixed: missing Rx " + "Gain for " + + radioService + + " gain set to " + + std::to_string( + rxGain); + fixedFlag = true; + } else if (radioService == "MG") { + rxGain = 41.0; + fixedStr += + "Fixed: missing Rx " + "Gain for " + + radioService + + " gain set to " + + std::to_string( + rxGain); + fixedFlag = true; + } else if (radioService == "MW") { + rxGain = 39.9; + fixedStr += + "Fixed: missing Rx " + "Gain for " + + radioService + + " gain set to " + + std::to_string( + rxGain); + fixedFlag = true; + } else if (radioService == "TI") { + rxGain = 41.8; + fixedStr += + "Fixed: missing Rx " + "Gain for " + + radioService + + " gain set to " + + std::to_string( + rxGain); + fixedFlag = true; + } else if (radioService == "TP") { + rxGain = 30.0; + fixedStr += + "Fixed: missing Rx " + "Gain for " + + radioService + + " gain set to " + + std::to_string( + rxGain); + fixedFlag = true; + } else if (radioService == "TS") { + rxGain = 41.5; + fixedStr += + "Fixed: missing Rx " + "Gain for " + + radioService + + " gain set to " + + std::to_string( + rxGain); + fixedFlag = true; + } else if (radioService == "TT") { + rxGain = 42.1; + fixedStr += + "Fixed: missing Rx " + "Gain for " + + radioService + + " gain set to " + + std::to_string( + rxGain); + fixedFlag = true; + } else if (radioService == "TB") { + rxGain = 40.7; + fixedStr += + "Fixed: missing Rx " + "Gain for " + + radioService + + " gain set to " + + std::to_string( + rxGain); + fixedFlag = true; + } else { + ignoreFlag = true; + reasonIgnored = "missing " + "Rx Gain"; + numIgnoreInvalid++; + } + } else { + ignoreFlag = true; + reasonIgnored = "missing Rx Gain"; + numIgnoreInvalid++; + } + } else if ((callsign == "WQUY451") && + (rxGain == 1.8)) { + if (fixAnomalousEntries) { + rxGain = 39.3; + fixedStr += "Fixed: anomalous Rx " + "Gain for " + + callsign + + " changed from 1.8 " + "to " + + std::to_string(rxGain); + fixedFlag = true; + } + } + } + + if (!ignoreFlag) { + if (rxGain < 10.0) { + if (fixAnomalousEntries) { + if (radioService == "CF") { + rxGain = 39.3; + fixedStr += + "Fixed: invalid Rx " + "Gain " + + std::to_string( + rxGain) + + " for " + + radioService + + " set to 39.3"; + fixedFlag = true; + } else if (radioService == "MG") { + rxGain = 41.0; + fixedStr += + "Fixed: invalid Rx " + "Gain " + + std::to_string( + rxGain) + + " for " + + radioService + + " set to 41.0"; + fixedFlag = true; + } else if (radioService == "MW") { + rxGain = 39.9; + fixedStr += + "Fixed: invalid Rx " + "Gain " + + std::to_string( + rxGain) + + " for " + + radioService + + " set to 39.9"; + fixedFlag = true; + } else if (radioService == "TI") { + rxGain = 41.8; + fixedStr += + "Fixed: invalid Rx " + "Gain " + + std::to_string( + rxGain) + + " for " + + radioService + + " set to 41.8"; + fixedFlag = true; + } else if (radioService == "TS") { + rxGain = 41.5; + fixedStr += + "Fixed: invalid Rx " + "Gain " + + std::to_string( + rxGain) + + " for " + + radioService + + " set to 41.5"; + fixedFlag = true; + } else { + ignoreFlag = true; + reasonIgnored = "invalid " + "Rx Gain"; + numIgnoreInvalid++; + } + } else { + ignoreFlag = true; + reasonIgnored = "invalid Rx Gain"; + numIgnoreInvalid++; + } + } + } + /**************************************************************************/ + + /**************************************************************************/ + /* rxAntennaDiameter */ + /**************************************************************************/ + rxAntennaDiameter = row.rxAntennaDiameter; + /**************************************************************************/ + + /**************************************************************************/ + /* rxNearFieldAntDiameter; */ + /**************************************************************************/ + rxNearFieldAntDiameter = row.rxNearFieldAntDiameter; + /**************************************************************************/ + + /**************************************************************************/ + /* rxNearFieldDistLimit; */ + /**************************************************************************/ + rxNearFieldDistLimit = row.rxNearFieldDistLimit; + /**************************************************************************/ + + /**************************************************************************/ + /* rxNearFieldAntEfficiency; */ + /**************************************************************************/ + rxNearFieldAntEfficiency = row.rxNearFieldAntEfficiency; + /**************************************************************************/ + + /**************************************************************************/ + /* rxAntennaCategory */ + /**************************************************************************/ + rxAntennaCategory = row.rxAntennaCategory; + /**************************************************************************/ + + /**************************************************************************/ + /* rxAntenna */ + /**************************************************************************/ + if (!ignoreFlag) { + rxAntenna = row.rxAntenna; + if (rxAntenna) { + LOGGER_DEBUG(logger) + << "Antenna Found " << fsid << ": " + << rxAntenna->get_strid(); + rxAntennaType = CConst::LUTAntennaType; + } else { + std::string strval = row.rxAntennaModelName; + int validFlag; + rxAntennaType = + (CConst::ULSAntennaTypeEnum)CConst:: + strULSAntennaTypeList + ->str_to_type( + strval, + validFlag, + 0); + if (!validFlag) { + // std::ostringstream errStr; + // errStr << "Invalid ULS data for + // FSID = " << fsid + // << ", Unknown Rx Antenna \"" + // << strval + // << "\" using " << + // CConst::strULSAntennaTypeList->type_to_str(_ulsDefaultAntennaType); + // LOGGER_WARN(logger) << + // errStr.str(); + // statusMessageList.push_back(errStr.str()); + + rxAntennaType = + _ulsDefaultAntennaType; + } + } + } + /**************************************************************************/ + + /**************************************************************************/ + /* hasDiversity */ + /**************************************************************************/ + hasDiversity = row.hasDiversity; + /**************************************************************************/ + + if (hasDiversity) { + /**********************************************************************/ + /* diversityHeightAboveTerrain */ + /**********************************************************************/ + diversityHeightAboveTerrain = + row.diversityHeightAboveTerrain; + if (!ignoreFlag) { + if (std::isnan( + diversityHeightAboveTerrain)) { + ignoreFlag = true; + reasonIgnored = "missing Rx " + "Diversity Height " + "above Terrain"; + numIgnoreInvalid++; + } + } + + if (!ignoreFlag) { + if (diversityHeightAboveTerrain < 3.0) { + LOGGER_WARN(logger) + << "WARNING: ULS data for " + "FSID = " + << fsid + << ", " + "diversityHeightAboveTer" + "rain = " + << diversityHeightAboveTerrain + << " is < 3.0"; + } + } + /**********************************************************************/ + + /**********************************************************************/ + /* diversityGain */ + /**********************************************************************/ + diversityGain = row.diversityGain; + if (!ignoreFlag) { + if (std::isnan(diversityGain)) { + ignoreFlag = true; + reasonIgnored = "missing Rx " + "Diversity Gain"; + numIgnoreInvalid++; + } + } + /**********************************************************************/ + + /**********************************************************************/ + /* rxAntennaDiameter */ + /**********************************************************************/ + diversityAntennaDiameter = + row.diversityAntennaDiameter; + /**********************************************************************/ + } + + /**************************************************************************/ + /* fadeMargin */ + /**************************************************************************/ + + // ULS Db does not have fadeMargin field so set this as + // default + fadeMarginDB = -1.0; + + /**************************************************************************/ + } + } + + if (!_filterSimRegionOnly) { + if ((linkDirection == 1) || (linkDirection == 2) || + (simulationFlag == CConst::MobileSimulation) || + (simulationFlag == CConst::RLANSensingSimulation) || + (simulationFlag == CConst::showFSPwrAtRLANSimulation)) { + /**************************************************************************/ + /* txGain */ + /**************************************************************************/ + txGain = row.txGain; + if (!ignoreFlag) { + if (std::isnan(txGain)) { + if (fixAnomalousEntries) { + if (radioService == "CF") { + txGain = 39.3; + fixedStr += + "Fixed: missing Tx " + "Gain for " + + radioService + + " gain set to " + + std::to_string( + txGain); + fixedFlag = true; + } else { + ignoreFlag = true; + reasonIgnored = "missing " + "Tx Gain"; + numIgnoreInvalid++; + } + } else { + ignoreFlag = true; + reasonIgnored = "missing Tx Gain"; + numIgnoreInvalid++; + } + } + } + + /**************************************************************************/ + + /**************************************************************************/ + /* txEIRP */ + /**************************************************************************/ + txEIRP = row.txEIRP; + if (!ignoreFlag) { + if (std::isnan(txEIRP)) { + if (fixAnomalousEntries && + (radioService == "CF")) { + txEIRP = 66; + fixedStr += "Fixed: missing txEIRP " + "set to " + + std::to_string(txEIRP) + + " dBm"; + fixedFlag = true; + } else { + ignoreFlag = true; + reasonIgnored = "missing Tx EIRP"; + numIgnoreInvalid++; + } + } + } + + if (!ignoreFlag) { + txEIRP = txEIRP - 30; // Convert dBm to dBW + + if (txEIRP >= 80.0) { + if (fixAnomalousEntries) { + txEIRP = 39.3; + fixedStr += "Fixed: Tx EIRP > 80 " + "dBW set to 39.3 dBW"; + fixedFlag = true; + } else { + LOGGER_WARN(logger) + << "WARNING: ULS data for " + "FSID = " + << fsid + << ", txEIRP = " << txEIRP + << " (dBW) is >= 80.0"; + } + } + } + /**************************************************************************/ + + /**************************************************************************/ + /* txAntenna */ + /**************************************************************************/ + // ULS db does not have txAntenna field so use default + txAntennaType = _ulsDefaultAntennaType; + /**************************************************************************/ + } + } + + /**************************************************************************/ + /* status */ + /**************************************************************************/ + status = row.status; + /**************************************************************************/ + + if (!_filterSimRegionOnly) { + if (!ignoreFlag) { + for (int segIdx = 0; segIdx < numPR + 1; ++segIdx) { + Vector3 txLon = + (segIdx == 0 ? + txLongitudeDeg : + row.prLongitudeDeg[segIdx - 1]); + Vector3 txLat = + (segIdx == 0 ? + txLatitudeDeg : + row.prLatitudeDeg[segIdx - 1]); + Vector3 rxLon = + (segIdx == numPR ? + rxLongitudeDeg : + row.prLongitudeDeg[segIdx]); + Vector3 rxLat = (segIdx == numPR ? + rxLatitudeDeg : + row.prLatitudeDeg[segIdx]); + + if ((rxLat == txLat) && (rxLon == txLon)) { + reasonIgnored = "Ignored: RX and TX " + "LON/LAT values are " + "identical for segment " + + std::to_string(segIdx); + ignoreFlag = true; + numIgnoreInvalid++; + } + } + } + + if (!ignoreFlag) { + if (rxGain > 80.0) { + if (fixAnomalousEntries) { + rxGain = 30.0; + fixedStr += "Fixed: RX Gain > 80 dB: set " + "to 30 dB"; + fixedFlag = true; + } else { + LOGGER_WARN(logger) + << "WARNING: ULS data for FSID = " + << fsid << ", rxGain = " << rxGain + << " is > 80.0"; + } + } + } + } + + if ((!ignoreFlag) && (!fixedFlag)) { + numValid++; + } else if ((!ignoreFlag) && (fixedFlag)) { + numFixed++; + } + + if (!ignoreFlag) { + bool unii5Flag = + computeSpectralOverlapLoss((double *)NULL, + startFreq, + stopFreq, + 5925.0e6, + 6425.0e6, + false, + CConst::psdSpectralAlgorithm); + + bool unii7Flag = + computeSpectralOverlapLoss((double *)NULL, + startFreq, + stopFreq, + 6525.0e6, + 6875.0e6, + false, + CConst::psdSpectralAlgorithm); + + double rxAntennaFeederLossDB = row.rxLineLoss; + if (std::isnan(rxAntennaFeederLossDB)) { + // R2-AIP-10: set feeder loss according to txArchitecture + // R2-AIP-10-CAN: for canada sim, set _rxFeederLossDBIDU = + // _rxFeederLossDBODU = _rxFeederLossDBUnknown = 0.0 + if (row.txArchitecture == "IDU") { + rxAntennaFeederLossDB = _rxFeederLossDBIDU; + } else if (row.txArchitecture == "ODU") { + rxAntennaFeederLossDB = _rxFeederLossDBODU; + } else { + rxAntennaFeederLossDB = _rxFeederLossDBUnknown; + } + } + double noisePSD; + double centerFreq = (startFreq + stopFreq) / 2; + bool found = false; + for (int i = 0; (i < (int)_noisePSDFreqList.size()) && (!found); + ++i) { + if (centerFreq <= _noisePSDFreqList[i]) { + noisePSD = _noisePSDList[i]; + found = true; + } + } + if (!found) { + noisePSD = _noisePSDList[_noisePSDFreqList.size()]; + } + double antennaCenterFreq; + if (row.region == "US") { + if (unii5Flag && unii7Flag) { + antennaCenterFreq = centerFreq; + } else if (unii5Flag) { + antennaCenterFreq = (CConst::unii5StartFreqMHz + + CConst::unii5StopFreqMHz) * + 0.5e6; + } else if (unii7Flag) { + antennaCenterFreq = (CConst::unii7StartFreqMHz + + CConst::unii7StopFreqMHz) * + 0.5e6; + } else { + antennaCenterFreq = centerFreq; + } + } else { + antennaCenterFreq = centerFreq; + } + double lambda = CConst::c / antennaCenterFreq; + double rxDlambda = rxAntennaDiameter / lambda; + double noiseBandwidth = stopFreq - startFreq; + + uls = new ULSClass(this, fsid, dbIdx, numPR, row.region); + _ulsList->append(uls); + uls->setCallsign(callsign); + uls->setPathNumber(pathNumber); + uls->setRxCallsign(rxCallsign); + uls->setRxAntennaNumber(rxAntennaNumber); + uls->setRadioService(radioService); + uls->setEntityName(entityName); + uls->setStartFreq(startFreq); + uls->setStopFreq(stopFreq); + uls->setNoiseBandwidth(noiseBandwidth); + uls->setRxGroundElevation(rxGroundElevation); + uls->setRxLatitudeDeg(rxLatitudeDeg); + uls->setRxLongitudeDeg(rxLongitudeDeg); + uls->setTxGroundElevation(txGroundElevation); + uls->setTxPolarization(txPolarization); + uls->setTxLatitudeDeg(txLatitudeDeg); + uls->setTxLongitudeDeg(txLongitudeDeg); + uls->setAzimuthAngleToTx(azimuthAngleToTx); + uls->setElevationAngleToTx(elevationAngleToTx); + uls->setRxGain(rxGain); + uls->setRxDlambda(rxDlambda); + uls->setRxNearFieldAntDiameter(rxNearFieldAntDiameter); + uls->setRxNearFieldDistLimit(rxNearFieldDistLimit); + uls->setRxNearFieldAntEfficiency(rxNearFieldAntEfficiency); + uls->setRxAntennaModel(row.rxAntennaModelName); + uls->setRxAntennaType(rxAntennaType); + uls->setTxAntennaType(txAntennaType); + uls->setRxAntenna(rxAntenna); + uls->setTxAntenna(txAntenna); + uls->setRxAntennaCategory(rxAntennaCategory); + uls->setTxGain(txGain); + uls->setTxEIRP(txEIRP); + uls->setRxAntennaFeederLossDB(rxAntennaFeederLossDB); + uls->setFadeMarginDB(fadeMarginDB); + uls->setStatus(status); + + uls->setHasDiversity(hasDiversity); + if (hasDiversity) { + uls->setDiversityGain(diversityGain); + + double diversityDlambda = diversityAntennaDiameter / lambda; + uls->setDiversityDlambda(diversityDlambda); + } + + if (simulationFlag == CConst::MobileSimulation) { + throw std::invalid_argument("Mobile simulation not " + "supported"); + } + + bool rxTerrainHeightFlag, txTerrainHeightFlag; + double terrainHeight; + double bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum rxHeightSource; + CConst::HeightSourceEnum txHeightSource; + CConst::HeightSourceEnum prHeightSource; + Vector3 rxPosition, txPosition, prPosition, diversityPosition; + + if ((_terrainDataModel)) { + _terrainDataModel->getTerrainHeight(rxLongitudeDeg, + rxLatitudeDeg, + terrainHeight, + bldgHeight, + lidarHeightResult, + rxHeightSource); + rxTerrainHeightFlag = true; + } else { + rxTerrainHeightFlag = false; + terrainHeight = 0.0; + } + double rxHeight = rxHeightAboveTerrain + terrainHeight; + + uls->setRxTerrainHeightFlag(rxTerrainHeightFlag); + uls->setRxTerrainHeight(terrainHeight); + uls->setRxHeightAboveTerrain(rxHeightAboveTerrain); + uls->setRxHeightAMSL(rxHeight); + uls->setRxHeightSource(rxHeightSource); + + rxPosition = EcefModel::geodeticToEcef(rxLatitudeDeg, + rxLongitudeDeg, + rxHeight / 1000.0); + uls->setRxPosition(rxPosition); + + if (hasDiversity) { + double diversityHeight = diversityHeightAboveTerrain + + terrainHeight; + uls->setDiversityHeightAboveTerrain( + diversityHeightAboveTerrain); + uls->setDiversityHeightAMSL(diversityHeight); + + diversityPosition = + EcefModel::geodeticToEcef(rxLatitudeDeg, + rxLongitudeDeg, + diversityHeight / 1000.0); + uls->setDiversityPosition(diversityPosition); + } + + if (txLocFlag) { + if ((_terrainDataModel)) { + _terrainDataModel->getTerrainHeight( + txLongitudeDeg, + txLatitudeDeg, + terrainHeight, + bldgHeight, + lidarHeightResult, + txHeightSource); + txTerrainHeightFlag = true; + } else { + txTerrainHeightFlag = false; + terrainHeight = 0.0; + } + double txHeight = txHeightAboveTerrain + terrainHeight; + + uls->setTxTerrainHeightFlag(txTerrainHeightFlag); + uls->setTxTerrainHeight(terrainHeight); + uls->setTxHeightAboveTerrain(txHeightAboveTerrain); + uls->setTxHeightSource(txHeightSource); + uls->setTxHeightAMSL(txHeight); + + txPosition = EcefModel::geodeticToEcef(txLatitudeDeg, + txLongitudeDeg, + txHeight / 1000.0); + uls->setTxPosition(txPosition); + } else { + uls->setTxTerrainHeightFlag(true); + uls->setTxTerrainHeight(quietNaN); + uls->setTxHeightAboveTerrain(quietNaN); + uls->setTxHeightSource(CConst::unknownHeightSource); + uls->setTxHeightAMSL(quietNaN); + Vector3 nanVector3 = Vector3(quietNaN, quietNaN, quietNaN); + uls->setTxPosition(nanVector3); + } + + for (prIdx = 0; prIdx < numPR; ++prIdx) { + PRClass &pr = uls->getPR(prIdx); + + int validFlag; + pr.type = (CConst::PRTypeEnum)CConst::strPRTypeList + ->str_to_type(row.prType[prIdx], + validFlag, + 1); + pr.longitudeDeg = row.prLongitudeDeg[prIdx]; + pr.latitudeDeg = row.prLatitudeDeg[prIdx]; + pr.heightAboveTerrainRx = row.prHeightAboveTerrainRx[prIdx]; + pr.heightAboveTerrainTx = row.prHeightAboveTerrainTx[prIdx]; + + if ((_terrainDataModel)) { + _terrainDataModel->getTerrainHeight( + pr.longitudeDeg, + pr.latitudeDeg, + terrainHeight, + bldgHeight, + lidarHeightResult, + prHeightSource); + pr.terrainHeightFlag = true; + } else { + pr.terrainHeightFlag = false; + terrainHeight = 0.0; + } + + pr.terrainHeight = terrainHeight; + pr.heightAMSLRx = pr.heightAboveTerrainRx + + pr.terrainHeight; + pr.heightAMSLTx = pr.heightAboveTerrainTx + + pr.terrainHeight; + pr.heightSource = prHeightSource; + + pr.positionRx = EcefModel::geodeticToEcef(pr.latitudeDeg, + pr.longitudeDeg, + pr.heightAMSLRx / + 1000.0); + pr.positionTx = EcefModel::geodeticToEcef(pr.latitudeDeg, + pr.longitudeDeg, + pr.heightAMSLTx / + 1000.0); + + if (row.prType[prIdx] == "Ant") { + pr.type = CConst::backToBackAntennaPRType; + pr.txGain = row.prTxGain[prIdx]; + pr.txDlambda = row.prTxAntennaDiameter[prIdx] / + lambda; + pr.rxGain = row.prRxGain[prIdx]; + pr.rxDlambda = row.prRxAntennaDiameter[prIdx] / + lambda; + pr.antCategory = row.prAntCategory[prIdx]; + pr.antModel = row.prAntModelName[prIdx]; + + /**************************************************************************/ + /* Passive Repeater Antenna Pattern */ + /**************************************************************************/ + if (!ignoreFlag) { + pr.antenna = row.prAntenna[prIdx]; + if (pr.antenna) { + LOGGER_DEBUG(logger) + << "Passive Repeater " + "Antenna Found " + << fsid << ": " + << pr.antenna->get_strid(); + pr.antennaType = + CConst::LUTAntennaType; + } else { + std::string strval = + row.prAntModelName[prIdx]; + pr.antennaType = + (CConst::ULSAntennaTypeEnum) + CConst::strULSAntennaTypeList + ->str_to_type( + strval, + validFlag, + 0); + if (!validFlag) { + // std::ostringstream + // errStr; errStr << + // "Invalid ULS data for + // FSID = " << fsid + // << ", Unknown + // Passive Repeater Antenna + // \"" << strval + // << "\" using " << + // CConst::strULSAntennaTypeList->type_to_str(_ulsDefaultAntennaType); + // LOGGER_WARN(logger) << + // errStr.str(); + // statusMessageList.push_back(errStr.str()); + + pr.antennaType = + _ulsDefaultAntennaType; + } + } + } + /**************************************************************************/ + } else if (row.prType[prIdx] == "Ref") { + pr.type = CConst::billboardReflectorPRType; + pr.reflectorHeightLambda = + row.prReflectorHeight[prIdx] / lambda; + pr.reflectorWidthLambda = + row.prReflectorWidth[prIdx] / lambda; + + } else { + CORE_DUMP; + } + } + + for (int segIdx = 0; segIdx <= numPR; ++segIdx) { + Vector3 segTxPosn = + (segIdx == 0 ? txPosition : + uls->getPR(segIdx - 1).positionTx); + Vector3 segRxPosn = (segIdx == numPR ? + rxPosition : + uls->getPR(segIdx).positionRx); + Vector3 pointing; + double segDist; + + if (txLocFlag || (segIdx > 0)) { + pointing = (segTxPosn - segRxPosn).normalized(); + segDist = (segTxPosn - segRxPosn).len() * 1000.0; + } else { + Vector3 upVec = segRxPosn.normalized(); + Vector3 zVec = Vector3(0.0, 0.0, 1.0); + Vector3 eastVec = zVec.cross(upVec).normalized(); + Vector3 northVec = upVec.cross(eastVec); + + double ca = cos(row.azimuthAngleToTx * M_PI / + 180.0); + double sa = sin(row.azimuthAngleToTx * M_PI / + 180.0); + double ce = cos(row.elevationAngleToTx * M_PI / + 180.0); + double se = sin(row.elevationAngleToTx * M_PI / + 180.0); + + pointing = northVec * ca * ce + eastVec * sa * ce + + upVec * se; + segDist = -1.0; + } + + if (segIdx == numPR) { + uls->setAntennaPointing( + pointing); // Pointing of Rx antenna + uls->setLinkDistance(segDist); + if (hasDiversity) { + pointing = (segTxPosn - diversityPosition) + .normalized(); + uls->setDiversityAntennaPointing( + pointing); // Pointing of Rx + // Diversity antenna + } + } else { + uls->getPR(segIdx).pointing = + pointing; // Pointing of Passive Receiver + uls->getPR(segIdx).segmentDistance = segDist; + } + } + + /******************************************************************/ + /* Calculate PR parameters: */ + /* Orthonormal basis */ + /* thetaIN */ + /* alphaAZ */ + /* alphaEL */ + /******************************************************************/ + for (prIdx = numPR - 1; prIdx >= 0; prIdx--) { + PRClass &pr = uls->getPR(prIdx); + double nextSegDist = + ((prIdx == numPR - 1) ? + uls->getLinkDistance() : + uls->getPR(prIdx + 1).segmentDistance); + double nextSegFSPL = 20.0 * + log((4 * M_PI * centerFreq * + nextSegDist) / + CConst::c) / + log(10.0); + + if (pr.type == CConst::backToBackAntennaPRType) { + pr.pathSegGain = pr.rxGain + pr.txGain - + nextSegFSPL; + } else if (pr.type == CConst::billboardReflectorPRType) { + Vector3 pointingA = pr.pointing; + Vector3 pointingB = -( + prIdx == numPR - 1 ? + uls->getAntennaPointing() : + uls->getPR(prIdx + 1).pointing); + pr.reflectorZ = + (pointingA + pointingB) + .normalized(); // Perpendicular to + // reflector surface + Vector3 upVec = pr.positionRx.normalized(); + pr.reflectorX = (upVec.cross(pr.reflectorZ)) + .normalized(); // Horizontal + pr.reflectorY = (pr.reflectorZ.cross(pr.reflectorX)) + .normalized(); + + double Ax = pointingA.dot(pr.reflectorX); + double Ay = pointingA.dot(pr.reflectorY); + double Az = pointingA.dot(pr.reflectorZ); + + double cosThetaIN = pointingA.dot(pr.reflectorZ); + + // Spec was changed from: + // s = if ((alphaEL <= alphaAZ)) + // reflectorWidthLambda*cosThetaIN else + // pr.reflectorHeightLambda*cosThetaIN to s = + // MAX(reflectorWidthLambda, + // reflectorHeightLambda)*cosThetaIN + + bool conditionW; + if (0) { + // previous spec + double alphaAZ = (180.0 / M_PI) * + fabs(atan(Ax / Az)); + double alphaEL = (180.0 / M_PI) * + fabs(atan(Ay / Az)); + conditionW = (alphaEL <= alphaAZ); + } else { + // current spec + conditionW = (pr.reflectorWidthLambda >= + pr.reflectorHeightLambda); + } + + if (conditionW) { + pr.reflectorSLambda = + pr.reflectorWidthLambda * + cosThetaIN; + } else { + pr.reflectorSLambda = + pr.reflectorHeightLambda * + cosThetaIN; + } + pr.reflectorTheta1 = + (180.0 / M_PI) * + asin(1.0 / (2 * pr.reflectorSLambda)); + double Ks = 4 * pr.reflectorWidthLambda * + pr.reflectorHeightLambda * cosThetaIN * + lambda / (M_PI * nextSegDist); + double alpha_n = 20 * log10(M_PI * Ks / 4); + + double Q = -1.0; + if ((Ks <= 0.4) || + ((prIdx < numPR - 1) && + (uls->getPR(prIdx + 1).type == + CConst::billboardReflectorPRType))) { + pr.pathSegGain = std::min(3.0, alpha_n); + } else { + double nextDlambda = + ((prIdx == numPR - 1) ? + uls->getRxDlambda() : + uls->getPR(prIdx + 1) + .rxDlambda); + Q = nextDlambda * + sqrt(M_PI / + (4 * pr.reflectorWidthLambda * + pr.reflectorHeightLambda * + cosThetaIN)); + pr.pathSegGain = + _prTable->computePRTABLE(Q, + 1.0 / Ks); + } + + pr.reflectorThetaIN = acos(cosThetaIN) * 180.0 / + M_PI; + pr.reflectorKS = Ks; + pr.reflectorQ = Q; + } + pr.effectiveGain = + (prIdx == numPR - 1 ? + uls->getRxGain() : + uls->getPR(prIdx + 1).effectiveGain) + + pr.pathSegGain; + } + /******************************************************************/ + + double noiseLevelDBW = noisePSD + 10.0 * log10(noiseBandwidth); + uls->setNoiseLevelDBW(noiseLevelDBW); + if (fixedFlag && anomGc) { + anomGc.fsid = fsid; + anomGc.dbName = name; + anomGc.callsign = callsign; + anomGc.rxLatitudeDeg = rxLatitudeDeg; + anomGc.rxLongitudeDeg = rxLongitudeDeg; + anomGc.anomaly = fixedStr; + anomGc.completeRow(); + } + } else if (anomGc) { + anomGc.fsid = fsid; + anomGc.dbName = name; + anomGc.callsign = callsign; + anomGc.rxLatitudeDeg = rxLatitudeDeg; + anomGc.rxLongitudeDeg = rxLongitudeDeg; + anomGc.anomaly = reasonIgnored; + anomGc.completeRow(); + } + } + + LOGGER_INFO(logger) << "TOTAL NUM VALID ULS: " << numValid; + LOGGER_INFO(logger) << "TOTAL NUM IGNORE ULS (invalid data):" << numIgnoreInvalid; + LOGGER_INFO(logger) << "TOTAL NUM IGNORE ULS (out of band): " << numIgnoreOutOfBand; + LOGGER_INFO(logger) << "TOTAL NUM IGNORE ULS (out of SIMULATION REGION): " + << numIgnoreOutsideSimulationRegion; + LOGGER_INFO(logger) << "TOTAL NUM IGNORE ULS (Mobile): " << numIgnoreMobile; + LOGGER_INFO(logger) << "TOTAL NUM FIXED ULS: " << numFixed; + LOGGER_INFO(logger) << "TOTAL NUM VALID ULS IN SIMULATION (VALID + FIXED): " + << _ulsList->getSize(); + if (linkDirection == 0) { + LOGGER_INFO(logger) + << "NUM URBAN ULS: " << numUrbanULS << " = " + << ((double)numUrbanULS / _ulsList->getSize()) * 100.0 << " %"; + LOGGER_INFO(logger) + << "NUM SUBURBAN ULS: " << numSuburbanULS << " = " + << ((double)numSuburbanULS / _ulsList->getSize()) * 100.0 << " %"; + LOGGER_INFO(logger) + << "NUM RURAL ULS: " << numRuralULS << " = " + << ((double)numRuralULS / _ulsList->getSize()) * 100.0 << " %"; + LOGGER_INFO(logger) + << "NUM BARREN ULS: " << numBarrenULS << " = " + << ((double)numBarrenULS / _ulsList->getSize()) * 100.0 << " %"; + } + + if (_filterSimRegionOnly) { + exit(1); + } + + delete ulsDatabase; + } + if (_terrainDataModel) { + _terrainDataModel->setGdalDirectMode(prevGdalDirectMode); + } + + // _ulsList is expected to be sorted by ID (used in findULSID() ) + _ulsList->sort([](ULSClass *const &l, ULSClass *const &r) { + return l->getID() < r->getID(); + }); + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** AfcManager::readDeniedRegionData() ****/ +/******************************************************************************************/ +void AfcManager::readDeniedRegionData(std::string filename) +{ + if (filename.empty()) { + LOGGER_INFO(logger) << "No denied region file specified"; + return; + } + + int linenum, fIdx; + std::string line, strval; + char *chptr; + FILE *fp = (FILE *)NULL; + std::string str; + std::string reasonIgnored; + std::ostringstream errStr; + + int startFreqFieldIdx = -1; + int stopFreqFieldIdx = -1; + int exclusionZoneTypeFieldIdx = -1; + + int lat1Rect1FieldIdx; + int lat2Rect1FieldIdx; + int lon1Rect1FieldIdx; + int lon2Rect1FieldIdx; + + int lat1Rect2FieldIdx; + int lat2Rect2FieldIdx; + int lon1Rect2FieldIdx; + int lon2Rect2FieldIdx; + + int radiusFieldIdx; + int latCircleFieldIdx; + int lonCircleFieldIdx; + + int heightAGLFieldIdx; + + std::vector fieldIdxList; + std::vector fieldLabelList; + fieldIdxList.push_back(&startFreqFieldIdx); + fieldLabelList.push_back("Start Freq (MHz)"); + fieldIdxList.push_back(&stopFreqFieldIdx); + fieldLabelList.push_back("Stop Freq (MHz)"); + fieldIdxList.push_back(&stopFreqFieldIdx); + fieldLabelList.push_back("End Freq (MHz)"); + fieldIdxList.push_back(&exclusionZoneTypeFieldIdx); + fieldLabelList.push_back("Exclusion Zone"); + + fieldIdxList.push_back(&lat1Rect1FieldIdx); + fieldLabelList.push_back("Rectangle1 Lat 1"); + fieldIdxList.push_back(&lat2Rect1FieldIdx); + fieldLabelList.push_back("Rectangle1 Lat 2"); + fieldIdxList.push_back(&lon1Rect1FieldIdx); + fieldLabelList.push_back("Rectangle1 Lon 1"); + fieldIdxList.push_back(&lon2Rect1FieldIdx); + fieldLabelList.push_back("Rectangle1 Lon 2"); + + fieldIdxList.push_back(&lat1Rect2FieldIdx); + fieldLabelList.push_back("Rectangle2 Lat 1"); + fieldIdxList.push_back(&lat2Rect2FieldIdx); + fieldLabelList.push_back("Rectangle2 Lat 2"); + fieldIdxList.push_back(&lon1Rect2FieldIdx); + fieldLabelList.push_back("Rectangle2 Lon 1"); + fieldIdxList.push_back(&lon2Rect2FieldIdx); + fieldLabelList.push_back("Rectangle2 Lon 2"); + + fieldIdxList.push_back(&radiusFieldIdx); + fieldLabelList.push_back("Circle Radius (km)"); + fieldIdxList.push_back(&latCircleFieldIdx); + fieldLabelList.push_back("Circle center Lat"); + fieldIdxList.push_back(&lonCircleFieldIdx); + fieldLabelList.push_back("Circle center Lon"); + + fieldIdxList.push_back(&heightAGLFieldIdx); + fieldLabelList.push_back("Antenna AGL height (m)"); + + int drid; + double startFreq, stopFreq; + DeniedRegionClass::GeometryEnum exclusionZoneType; + double lat1Rect1, lat2Rect1, lon1Rect1, lon2Rect1; + double lat1Rect2, lat2Rect2, lon1Rect2, lon2Rect2; + double radius; + double latCircle, lonCircle; + bool horizonDistFlag; + double heightAGL; + + int fieldIdx; + + LOGGER_INFO(logger) << "Reading denied region Datafile: " << filename; + + if (!(fp = fopen(filename.c_str(), "rb"))) { + str = std::string("ERROR: Unable to open Denied Region Data File \"") + filename + + std::string("\"\n"); + throw std::runtime_error(str); + } + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + DeniedRegionClass *dr; + + linenum = 0; + bool foundLabelLine = false; + while (fgetline(fp, line, false)) { + linenum++; + std::vector fieldList = splitCSV(line); + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if ((lineType == unknownLineType) && (!foundLabelLine)) { + lineType = labelLineType; + foundLabelLine = 1; + } + if ((lineType == unknownLineType) && (foundLabelLine)) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + bool found; + std::string field; + switch (lineType) { + case labelLineType: + for (fieldIdx = 0; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + + // std::cout << "FIELD: \"" << field << "\"" << std::endl; + + found = false; + for (fIdx = 0; + (fIdx < (int)fieldLabelList.size()) && (!found); + fIdx++) { + if (field == fieldLabelList.at(fIdx)) { + *fieldIdxList.at(fIdx) = fieldIdx; + found = true; + } + } + } + + for (fIdx = 0; fIdx < (int)fieldIdxList.size(); fIdx++) { + if (*fieldIdxList.at(fIdx) == -1) { + errStr << "ERROR: Invalid Denied Region Data file " + "\"" + << filename << "\" label line missing \"" + << fieldLabelList.at(fIdx) << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + } + + break; + case dataLineType: + /**************************************************************************/ + /* startFreq */ + /**************************************************************************/ + strval = fieldList.at(startFreqFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Invalid Denied Region Data file \"" + << filename << "\" line " << linenum + << " missing Start Freq" << std::endl; + throw std::runtime_error(errStr.str()); + } + startFreq = std::strtod(strval.c_str(), &chptr) * + 1.0e6; // Convert MHz to Hz + /**************************************************************************/ + + /**************************************************************************/ + /* stopFreq */ + /**************************************************************************/ + strval = fieldList.at(stopFreqFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Invalid Denied Region Data file \"" + << filename << "\" line " << linenum + << " missing Stop Freq" << std::endl; + throw std::runtime_error(errStr.str()); + } + stopFreq = std::strtod(strval.c_str(), &chptr) * + 1.0e6; // Convert MHz to Hz + /**************************************************************************/ + + /**************************************************************************/ + /* exclusionZoneType */ + /**************************************************************************/ + strval = fieldList.at(exclusionZoneTypeFieldIdx); + + if (strval == "One Rectangle") { + exclusionZoneType = DeniedRegionClass::rectGeometry; + } else if (strval == "Two Rectangles") { + exclusionZoneType = DeniedRegionClass::rect2Geometry; + } else if (strval == "Circle") { + exclusionZoneType = DeniedRegionClass::circleGeometry; + } else if (strval == "Horizon Distance") { + exclusionZoneType = DeniedRegionClass::horizonDistGeometry; + } else { + errStr << "ERROR: Invalid Denied Region Data file \"" + << filename << "\" line " << linenum + << " exclusion zone set to unrecognized value " + << strval << std::endl; + throw std::runtime_error(errStr.str()); + } + /**************************************************************************/ + + drid = (_deniedRegionList.size() ? + _deniedRegionList[_deniedRegionList.size() - 1] + ->getID() + + 1 : + 0); + + switch (exclusionZoneType) { + case DeniedRegionClass::rectGeometry: + case DeniedRegionClass::rect2Geometry: + dr = (DeniedRegionClass *)new RectDeniedRegionClass( + drid); + + strval = fieldList.at(lat1Rect1FieldIdx); + lat1Rect1 = getAngleFromDMS(strval); + strval = fieldList.at(lat2Rect1FieldIdx); + lat2Rect1 = getAngleFromDMS(strval); + strval = fieldList.at(lon1Rect1FieldIdx); + lon1Rect1 = getAngleFromDMS(strval); + strval = fieldList.at(lon2Rect1FieldIdx); + lon2Rect1 = getAngleFromDMS(strval); + ((RectDeniedRegionClass *)dr) + ->addRect(lon1Rect1, + lon2Rect1, + lat1Rect1, + lat2Rect1); + + if (exclusionZoneType == + DeniedRegionClass::rect2Geometry) { + strval = fieldList.at(lat1Rect2FieldIdx); + lat1Rect2 = getAngleFromDMS(strval); + strval = fieldList.at(lat2Rect2FieldIdx); + lat2Rect2 = getAngleFromDMS(strval); + strval = fieldList.at(lon1Rect2FieldIdx); + lon1Rect2 = getAngleFromDMS(strval); + strval = fieldList.at(lon2Rect2FieldIdx); + lon2Rect2 = getAngleFromDMS(strval); + ((RectDeniedRegionClass *)dr) + ->addRect(lon1Rect2, + lon2Rect2, + lat1Rect2, + lat2Rect2); + } + break; + case DeniedRegionClass::circleGeometry: + case DeniedRegionClass::horizonDistGeometry: + strval = fieldList.at(lonCircleFieldIdx); + lonCircle = getAngleFromDMS(strval); + strval = fieldList.at(latCircleFieldIdx); + latCircle = getAngleFromDMS(strval); + + horizonDistFlag = + (exclusionZoneType == + DeniedRegionClass::horizonDistGeometry); + + dr = (DeniedRegionClass *)new CircleDeniedRegionClass( + drid, + horizonDistFlag); + + ((CircleDeniedRegionClass *)dr) + ->setLongitudeCenter(lonCircle); + ((CircleDeniedRegionClass *)dr) + ->setLatitudeCenter(latCircle); + + if (!horizonDistFlag) { + strval = fieldList.at(radiusFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Invalid Denied " + "Region Data file \"" + << filename << "\" line " + << linenum + << " missing Circle Radius" + << std::endl; + throw std::runtime_error( + errStr.str()); + } + radius = std::strtod(strval.c_str(), + &chptr) * + 1.0e3; // Convert km to m + ((CircleDeniedRegionClass *)dr) + ->setRadius(radius); + } else { + /**************************************************************************/ + /* heightAGL */ + /**************************************************************************/ + strval = fieldList.at(heightAGLFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Invalid Denied " + "Region Data file \"" + << filename << "\" line " + << linenum + << " missing Antenna AGL " + "Height" + << std::endl; + throw std::runtime_error( + errStr.str()); + } + heightAGL = std::strtod(strval.c_str(), + &chptr); + dr->setHeightAGL(heightAGL); + /**************************************************************************/ + } + + break; + default: + CORE_DUMP; + break; + } + + dr->setStartFreq(startFreq); + dr->setStopFreq(stopFreq); + dr->setType(DeniedRegionClass::userSpecifiedType); + _deniedRegionList.push_back(dr); + + break; + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + CORE_DUMP; + break; + } + } + + if (fp) { + fclose(fp); + } + + LOGGER_INFO(logger) << "TOTAL NUM DENIED REGION: " << _deniedRegionList.size(); + + return; +} +/******************************************************************************************/ + +void AfcManager::fixFSTerrain() +{ + int ulsIdx; + for (ulsIdx = 0; ulsIdx <= _ulsList->getSize() - 1; ulsIdx++) { + ULSClass *uls = (*_ulsList)[ulsIdx]; + bool updateFlag = false; + double bldgHeight, terrainHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum heightSource; + if (!uls->getRxTerrainHeightFlag()) { + _terrainDataModel->getTerrainHeight(uls->getRxLongitudeDeg(), + uls->getRxLatitudeDeg(), + terrainHeight, + bldgHeight, + lidarHeightResult, + heightSource); + updateFlag = true; + uls->setRxTerrainHeightFlag(true); + double rxHeight = uls->getRxHeightAboveTerrain() + terrainHeight; + Vector3 rxPosition = EcefModel::geodeticToEcef(uls->getRxLatitudeDeg(), + uls->getRxLongitudeDeg(), + rxHeight / 1000.0); + uls->setRxPosition(rxPosition); + uls->setRxTerrainHeight(terrainHeight); + uls->setRxHeightAMSL(rxHeight); + uls->setRxHeightSource(heightSource); + } + if (!uls->getTxTerrainHeightFlag()) { + _terrainDataModel->getTerrainHeight(uls->getTxLongitudeDeg(), + uls->getTxLatitudeDeg(), + terrainHeight, + bldgHeight, + lidarHeightResult, + heightSource); + updateFlag = true; + uls->setTxTerrainHeightFlag(true); + double txHeight = uls->getTxHeightAboveTerrain() + terrainHeight; + Vector3 txPosition = EcefModel::geodeticToEcef(uls->getTxLatitudeDeg(), + uls->getTxLongitudeDeg(), + txHeight / 1000.0); + uls->setTxPosition(txPosition); + uls->setTxTerrainHeight(terrainHeight); + uls->setTxHeightAMSL(txHeight); + uls->setTxHeightSource(heightSource); + } + for (int prIdx = 0; prIdx < uls->getNumPR(); ++prIdx) { + PRClass &pr = uls->getPR(prIdx); + + if (!pr.terrainHeightFlag) { + _terrainDataModel->getTerrainHeight(pr.longitudeDeg, + pr.latitudeDeg, + terrainHeight, + bldgHeight, + lidarHeightResult, + heightSource); + updateFlag = true; + pr.terrainHeightFlag = true; + + double prHeightRx = pr.heightAboveTerrainRx + terrainHeight; + double prHeightTx = pr.heightAboveTerrainTx + terrainHeight; + pr.positionRx = EcefModel::geodeticToEcef(pr.latitudeDeg, + pr.longitudeDeg, + prHeightRx / 1000.0); + pr.positionTx = EcefModel::geodeticToEcef(pr.latitudeDeg, + pr.longitudeDeg, + prHeightTx / 1000.0); + pr.terrainHeight = terrainHeight; + pr.heightAMSLRx = prHeightRx; + pr.heightAMSLTx = prHeightTx; + pr.heightSource = heightSource; + } + } + + if (updateFlag) { + for (int segIdx = 0; segIdx < uls->getNumPR() + 1; ++segIdx) { + Vector3 segTxPosn = (segIdx == 0 ? + uls->getTxPosition() : + uls->getPR(segIdx - 1).positionTx); + Vector3 segRxPosn = (segIdx == uls->getNumPR() ? + uls->getRxPosition() : + uls->getPR(segIdx).positionRx); + + Vector3 pointing = + (segTxPosn - segRxPosn) + .normalized(); // boresight pointing direction of RX + double segDist = (segTxPosn - segRxPosn).len() * 1000.0; + + bool txLocFlag = (!std::isnan(segTxPosn.x())) && + (!std::isnan(segTxPosn.y())) && + (!std::isnan(segTxPosn.z())); + + if (txLocFlag) { + pointing = (segTxPosn - segRxPosn).normalized(); + segDist = (segTxPosn - segRxPosn).len() * 1000.0; + } else { + double azimuthAngleToTx = uls->getAzimuthAngleToTx(); + double elevationAngleToTx = uls->getElevationAngleToTx(); + + Vector3 upVec = segRxPosn.normalized(); + Vector3 zVec = Vector3(0.0, 0.0, 1.0); + Vector3 eastVec = zVec.cross(upVec).normalized(); + Vector3 northVec = upVec.cross(eastVec); + + double ca = cos(azimuthAngleToTx * M_PI / 180.0); + double sa = sin(azimuthAngleToTx * M_PI / 180.0); + double ce = cos(elevationAngleToTx * M_PI / 180.0); + double se = sin(elevationAngleToTx * M_PI / 180.0); + + pointing = northVec * ca * ce + eastVec * sa * ce + + upVec * se; + segDist = -1.0; + } + + if (segIdx == uls->getNumPR()) { + uls->setAntennaPointing(pointing); // Pointing of Rx antenna + uls->setLinkDistance(segDist); + } else { + uls->getPR(segIdx).pointing = pointing; + uls->getPR(segIdx).segmentDistance = segDist; + } + } + } + } + + return; +} + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::q() ****/ +/**** Gaussian Q() function ****/ +/******************************************************************************************/ +double AfcManager::q(double Z) const +{ + static const double sqrt2 = sqrt(2.0); + + return (0.5 * erfc(Z / sqrt2)); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::computeBuildingPenetration ****/ +/**** Compute Random Building Penetration according to ITU-R P.[BEL] ****/ +/**** Note that a loss value in DB is returned as a negative number. ****/ +/******************************************************************************************/ +double AfcManager::computeBuildingPenetration(CConst::BuildingTypeEnum buildingType, + double elevationAngleDeg, + double frequency, + std::string &buildingPenetrationModelStr, + double &buildingPenetrationCDF) const +{ + double r, s, t, u, v, w, x, y, z; + double A, B, C; + double mA, sA, mB, sB; + + if (_fixedBuildingLossFlag) { + buildingPenetrationModelStr = "FIXED VALUE"; + buildingPenetrationCDF = 0.5; + return (_fixedBuildingLossValue); + } else if (buildingType == CConst::noBuildingType) { + buildingPenetrationModelStr = "NONE"; + buildingPenetrationCDF = 0.5; + return (0.0); + } else if (buildingType == CConst::traditionalBuildingType) { + r = 12.64; + s = 3.72; + t = 0.96; + u = 9.6; + v = 2.0; + w = 9.1; + x = -3.0; + y = 4.5; + z = -2.0; + } else if (buildingType == CConst::thermallyEfficientBuildingType) { + r = 28.19; + s = -3.00; + t = 8.48; + u = 13.5; + v = 3.8; + w = 27.8; + x = -2.9; + y = 9.4; + z = -2.1; + } else { + throw std::runtime_error("ERROR in computeBuildingPenetration(), Invalid building " + "type"); + } + + buildingPenetrationModelStr = "P.2109"; + + double fGHz = frequency * 1.0e-9; + double logf = log(fGHz) / log(10.0); + double Le = 0.212 * fabs(elevationAngleDeg); + double Lh = r + s * logf + t * logf * logf; + + mA = Lh + Le; + mB = w + x * logf; + sA = u + v * logf; + sB = y + z * logf; + + arma::vec gauss(1); + gauss[0] = _zbldg2109; + + A = gauss[0] * sA + mA; + B = gauss[0] * sB + mB; + C = -3.0; + + double lossDB = 10.0 * + log(exp(A * log(10.0) / 10.0) + exp(B * log(10.0) / 10.0) + + exp(C * log(10.0) / 10.0)) / + log(10.0); + buildingPenetrationCDF = q(-gauss[0]); + + return (lossDB); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** AfcManager::computePathLoss ****/ +/******************************************************************************************/ +void AfcManager::computePathLoss(CConst::PathLossModelEnum pathLossModel, + bool itmFSPLFlag, + CConst::PropEnvEnum propEnv, + CConst::PropEnvEnum propEnvRx, + CConst::NLCDLandCatEnum nlcdLandCatTx, + CConst::NLCDLandCatEnum nlcdLandCatRx, + double distKm, + double fsplDistKm, + double win2DistKm, + double frequency, + double txLongitudeDeg, + double txLatitudeDeg, + double txHeightM, + double elevationAngleTxDeg, + double rxLongitudeDeg, + double rxLatitudeDeg, + double rxHeightM, + double elevationAngleRxDeg, + double &pathLoss, + double &pathClutterTxDB, + double &pathClutterRxDB, + std::string &pathLossModelStr, + double &pathLossCDF, + std::string &pathClutterTxModelStr, + double &pathClutterTxCDF, + std::string &pathClutterRxModelStr, + double &pathClutterRxCDF, + std::string *txClutterStrPtr, + std::string *rxClutterStrPtr, + double **ITMProfilePtr, + double **isLOSProfilePtr, + double *isLOSSurfaceFracPtr +#if DEBUG_AFC + , + std::vector &ITMHeightType +#endif +) const +{ + double frequencyGHz = frequency * 1.0e-9; + + if (txClutterStrPtr) { + *txClutterStrPtr = ""; + } + + if (rxClutterStrPtr) { + *rxClutterStrPtr = ""; + } + + pathLossModelStr = ""; + pathClutterTxModelStr = ""; + pathClutterTxCDF = -1.0; + pathClutterRxModelStr = ""; + pathClutterRxCDF = -1.0; + + if (pathLossModel == CConst::ITMBldgPathLossModel) { + if ((propEnv == CConst::urbanPropEnv) || (propEnv == CConst::suburbanPropEnv)) { + if (win2DistKm * 1000 < _closeInDist) { + if (_closeInPathLossModel == "WINNER2") { + int winner2LOSValue = + 0; // 1: Force LOS, 2: Force NLOS, 0: Compute + // probLOS, then select or combine. + if (win2DistKm * 1000 <= 50.0) { + winner2LOSValue = 1; + } else if ((_winner2LOSOption == + CConst::BldgDataLOSOption) || + (_winner2LOSOption == + CConst::BldgDataReqTxLOSOption) || + (_winner2LOSOption == + CConst::BldgDataReqRxLOSOption) || + (_winner2LOSOption == + CConst::BldgDataReqTxRxLOSOption) || + (_winner2LOSOption == CConst::CdsmLOSOption)) { + double terrainHeight; + double bldgHeight; + MultibandRasterClass::HeightResult + lidarHeightResult; + CConst::HeightSourceEnum txHeightSource, + rxHeightSource; + _terrainDataModel->getTerrainHeight( + txLongitudeDeg, + txLatitudeDeg, + terrainHeight, + bldgHeight, + lidarHeightResult, + txHeightSource, + false); + _terrainDataModel->getTerrainHeight( + rxLongitudeDeg, + rxLatitudeDeg, + terrainHeight, + bldgHeight, + lidarHeightResult, + rxHeightSource, + false); + + bool reqTx = (_winner2LOSOption == + CConst::BldgDataReqTxLOSOption) || + (_winner2LOSOption == + CConst::BldgDataReqTxRxLOSOption); + bool reqRx = (_winner2LOSOption == + CConst::BldgDataReqRxLOSOption) || + (_winner2LOSOption == + CConst::BldgDataReqTxRxLOSOption); + + if (((!reqTx) || (txHeightSource == + CConst::lidarHeightSource)) && + ((!reqRx) || (rxHeightSource == + CConst::lidarHeightSource))) { + int numPts = std::min( + ((int)floor(distKm * 1000 / + _itmMinSpacing)) + + 1, + _itmMaxNumPts); + bool losFlag = + UlsMeasurementAnalysis::isLOS( + _terrainDataModel, + QPointF(txLatitudeDeg, + txLongitudeDeg), + txHeightM, + QPointF(rxLatitudeDeg, + rxLongitudeDeg), + rxHeightM, + distKm, + numPts, + isLOSProfilePtr, + isLOSSurfaceFracPtr); + if (losFlag) { + if ((_winner2LOSOption == + CConst::CdsmLOSOption) && + (*isLOSSurfaceFracPtr < + _cdsmLOSThr)) { + winner2LOSValue = 0; + } else { + winner2LOSValue = 1; + } + } else { + winner2LOSValue = 2; + } + } + } else if (_winner2LOSOption == CConst::ForceLOSLOSOption) { + winner2LOSValue = 1; + } else if (_winner2LOSOption == + CConst::ForceNLOSLOSOption) { + winner2LOSValue = 2; + } + + double sigma, probLOS; + if (propEnv == CConst::urbanPropEnv) { + // Winner2 C2: urban + pathLoss = Winner2_C2urban(1000 * win2DistKm, + rxHeightM, + txHeightM, + frequency, + sigma, + pathLossModelStr, + pathLossCDF, + probLOS, + winner2LOSValue); + } else if (propEnv == CConst::suburbanPropEnv) { + // Winner2 C1: suburban + pathLoss = Winner2_C1suburban(1000 * win2DistKm, + rxHeightM, + txHeightM, + frequency, + sigma, + pathLossModelStr, + pathLossCDF, + probLOS, + winner2LOSValue); + } + } else { + throw std::runtime_error(ErrStream() + << "ERROR: Invalid close in path " + "loss model = " + << _closeInPathLossModel); + } + } else if (itmFSPLFlag) { + pathLoss = 20.0 * + log((4 * M_PI * frequency * fsplDistKm * 1000) / + CConst::c) / + log(10.0); + pathLossModelStr = "FSPL"; + pathLossCDF = 0.5; + } else { + // Terrain propagation: Terrain + ITM + double frequencyMHz = 1.0e-6 * frequency; + // std::cerr << "PATHLOSS," << txLatitudeDeg << "," << + // txLongitudeDeg << "," << rxLatitudeDeg << "," << rxLongitudeDeg + // << std::endl; + int numPts = std::min(((int)floor(distKm * 1000 / _itmMinSpacing)) + + 1, + _itmMaxNumPts); + + int radioClimate = _ituData->getRadioClimateValue(txLatitudeDeg, + txLongitudeDeg); + int radioClimateTmp = + _ituData->getRadioClimateValue(rxLatitudeDeg, + rxLongitudeDeg); + if (radioClimateTmp < radioClimate) { + radioClimate = radioClimateTmp; + } + double surfaceRefractivity = _ituData->getSurfaceRefractivityValue( + (txLatitudeDeg + rxLatitudeDeg) / 2, + (txLongitudeDeg + rxLongitudeDeg) / 2); + + /******************************************************************************************/ + /**** NOTE: ITM based on signal loss, so higher confidence + * corresponds to higher loss. ****/ + /******************************************************************************************/ + double u = _confidenceITM; + + pathLoss = UlsMeasurementAnalysis::runPointToPoint( + _terrainDataModel, + true, + QPointF(txLatitudeDeg, txLongitudeDeg), + txHeightM, + QPointF(rxLatitudeDeg, rxLongitudeDeg), + rxHeightM, + distKm, + _itmEpsDielect, + _itmSgmConductivity, + surfaceRefractivity, + frequencyMHz, + radioClimate, + _itmPolarization, + u, + _reliabilityITM, + numPts, + NULL, + ITMProfilePtr); + pathLossModelStr = "ITM_BLDG"; + pathLossCDF = _confidenceITM; + } + } else if ((propEnv == CConst::ruralPropEnv) || + (propEnv == CConst::barrenPropEnv)) { + if (itmFSPLFlag) { + pathLoss = 20.0 * + log((4 * M_PI * frequency * fsplDistKm * 1000) / + CConst::c) / + log(10.0); + pathLossModelStr = "FSPL"; + pathLossCDF = 0.5; + } else { + // Terrain propagation: Terrain + ITM + double frequencyMHz = 1.0e-6 * frequency; + int numPts = std::min(((int)floor(distKm * 1000 / _itmMinSpacing)) + + 1, + _itmMaxNumPts); + int radioClimate = _ituData->getRadioClimateValue(txLatitudeDeg, + txLongitudeDeg); + int radioClimateTmp = + _ituData->getRadioClimateValue(rxLatitudeDeg, + rxLongitudeDeg); + if (radioClimateTmp < radioClimate) { + radioClimate = radioClimateTmp; + } + double surfaceRefractivity = _ituData->getSurfaceRefractivityValue( + (txLatitudeDeg + rxLatitudeDeg) / 2, + (txLongitudeDeg + rxLongitudeDeg) / 2); + double u = _confidenceITM; + pathLoss = UlsMeasurementAnalysis::runPointToPoint( + _terrainDataModel, + true, + QPointF(txLatitudeDeg, txLongitudeDeg), + txHeightM, + QPointF(rxLatitudeDeg, rxLongitudeDeg), + rxHeightM, + distKm, + _itmEpsDielect, + _itmSgmConductivity, + surfaceRefractivity, + frequencyMHz, + radioClimate, + _itmPolarization, + u, + _reliabilityITM, + numPts, + NULL, + ITMProfilePtr); + pathLossModelStr = "ITM_BLDG"; + pathLossCDF = _confidenceITM; + + pathLossModelStr = "ITM_BLDG"; + pathLossCDF = _confidenceITM; + } + } else { + throw std::runtime_error(ErrStream() << "ERROR reading ULS data: propEnv = " + << propEnv << " INVALID value"); + } + pathClutterTxDB = 0.0; + pathClutterTxModelStr = "NONE"; + pathClutterTxCDF = 0.5; + pathClutterRxDB = 0.0; + pathClutterRxModelStr = "NONE"; + pathClutterRxCDF = 0.5; + } else if (pathLossModel == CConst::CoalitionOpt6PathLossModel) { +#if 1 + // As of 2021.12.03 this path loss model is no longer supported for AFC. + throw std::runtime_error(ErrStream() + << "ERROR: unsupported path loss model selected"); +#else + // Path Loss Model selected for 6 GHz Coalition + // Option 6 in PropagationModelOptions Winner-II 20171004.pptx + + if ((propEnv == CConst::urbanPropEnv) || (propEnv == CConst::suburbanPropEnv)) { + if (distKm * 1000 < _closeInDist) { + if (_closeInPathLossModel == "WINNER2") { + int winner2LOSValue = + 0; // 1: Force LOS, 2: Force NLOS, 0: Compute + // probLOS, then select or combine. + if (distKm * 1000 <= 50.0) { + winner2LOSValue = 1; + } else if (_winner2LOSOption == CConst::BldgDataLOSOption) { + double terrainHeight; + double bldgHeight; + MultibandRasterClass::HeightResult + lidarHeightResult; + CConst::HeightSourceEnum txHeightSource, + rxHeightSource; + _terrainDataModel->getTerrainHeight( + txLongitudeDeg, + txLatitudeDeg, + terrainHeight, + bldgHeight, + lidarHeightResult, + txHeightSource); + _terrainDataModel->getTerrainHeight( + rxLongitudeDeg, + rxLatitudeDeg, + terrainHeight, + bldgHeight, + lidarHeightResult, + rxHeightSource); + + if ((txHeightSource == CConst::lidarHeightSource) && + (rxHeightSource == CConst::lidarHeightSource)) { + int numPts = std::min( + ((int)floor(distKm * 1000 / + _itmMinSpacing)) + + 1, + _itmMaxNumPts); + bool losFlag = + UlsMeasurementAnalysis::isLOS( + _terrainDataModel, + QPointF(txLatitudeDeg, + txLongitudeDeg), + txHeightM, + QPointF(rxLatitudeDeg, + rxLongitudeDeg), + rxHeightM, + distKm, + numPts, + isLOSProfilePtr); + winner2LOSValue = (losFlag ? 1 : 2); + } + } else if (_winner2LOSOption == CConst::ForceLOSLOSOption) { + winner2LOSValue = 1; + } else if (_winner2LOSOption == + CConst::ForceNLOSLOSOption) { + winner2LOSValue = 2; + } + + double sigma, probLOS; + if (propEnv == CConst::urbanPropEnv) { + // Winner2 C2: urban + pathLoss = Winner2_C2urban(1000 * distKm, + rxHeightM, + txHeightM, + frequency, + sigma, + pathLossModelStr, + pathLossCDF, + probLOS, + winner2LOSValue); + } else if (propEnv == CConst::suburbanPropEnv) { + // Winner2 C1: suburban + pathLoss = Winner2_C1suburban(1000 * distKm, + rxHeightM, + txHeightM, + frequency, + sigma, + pathLossModelStr, + pathLossCDF, + probLOS, + winner2LOSValue); + } + } else { + throw std::runtime_error(ErrStream() + << "ERROR: Invalid close in path " + "loss model = " + << _closeInPathLossModel); + } + pathClutterTxDB = 0.0; + pathClutterTxModelStr = "NONE"; + pathClutterTxCDF = 0.5; + } else { + if (itmFSPLFlag) { + pathLoss = 20.0 * + log((4 * M_PI * frequency * fsplDistKm * 1000) / + CConst::c) / + log(10.0); + pathLossModelStr = "FSPL"; + pathLossCDF = 0.5; + } else { + // Terrain propagation: Terrain + ITM + double frequencyMHz = 1.0e-6 * frequency; + // std::cerr << "PATHLOSS," << txLatitudeDeg << "," << + // txLongitudeDeg << "," << rxLatitudeDeg << "," << + // rxLongitudeDeg << std::endl; + int numPts = std::min( + ((int)floor(distKm * 1000 / _itmMinSpacing)) + 1, + _itmMaxNumPts); + int radioClimate = + _ituData->getRadioClimateValue(txLatitudeDeg, + txLongitudeDeg); + int radioClimateTmp = + _ituData->getRadioClimateValue(rxLatitudeDeg, + rxLongitudeDeg); + if (radioClimateTmp < radioClimate) { + radioClimate = radioClimateTmp; + } + double surfaceRefractivity = + _ituData->getSurfaceRefractivityValue( + (txLatitudeDeg + rxLatitudeDeg) / 2, + (txLongitudeDeg + rxLongitudeDeg) / 2); + + /******************************************************************************************/ + /**** NOTE: ITM based on signal loss, so higher confidence + * corresponds to higher loss. ****/ + /******************************************************************************************/ + double u = _confidenceITM; + + pathLoss = UlsMeasurementAnalysis::runPointToPoint( + _terrainDataModel, + false, + QPointF(txLatitudeDeg, txLongitudeDeg), + txHeightM, + QPointF(rxLatitudeDeg, rxLongitudeDeg), + rxHeightM, + distKm, + _itmEpsDielect, + _itmSgmConductivity, + surfaceRefractivity, + frequencyMHz, + radioClimate, + _itmPolarization, + u, + _reliabilityITM, + numPts, + NULL, + ITMProfilePtr); + pathLossModelStr = "ITM"; + pathLossCDF = _confidenceITM; + } + + // ITU-R P.[CLUTTER] sec 3.2 + double Ll = 23.5 + 9.6 * log(frequencyGHz) / log(10.0); + double Ls = 32.98 + 23.9 * log(distKm) / log(10.0) + + 3.0 * log(frequencyGHz) / log(10.0); + + arma::vec gauss(1); + gauss[0] = _zclutter2108; + + double Lctt = -5.0 * + log(exp(-0.2 * Ll * log(10.0)) + + exp(-0.2 * Ls * log(10.0))) / + log(10.0) + + 6.0 * gauss[0]; + pathClutterTxDB = Lctt; + + pathClutterTxModelStr = "P.2108"; + pathClutterTxCDF = q(-gauss[0]); + if (_applyClutterFSRxFlag && (rxHeightM <= 10.0) && + (distKm >= 1.0)) { + pathClutterRxDB = pathClutterTxDB; + pathClutterRxModelStr = pathClutterTxModelStr; + pathClutterRxCDF = pathClutterTxCDF; + } else { + pathClutterRxDB = 0.0; + pathClutterRxModelStr = "NONE"; + pathClutterRxCDF = 0.5; + } + } + } else if ((propEnv == CConst::ruralPropEnv) || + (propEnv == CConst::barrenPropEnv)) { + if (itmFSPLFlag) { + pathLoss = 20.0 * + log((4 * M_PI * frequency * fsplDistKm * 1000) / + CConst::c) / + log(10.0); + pathLossModelStr = "FSPL"; + pathLossCDF = 0.5; + } else { + // Terrain propagation: Terrain + ITM + double frequencyMHz = 1.0e-6 * frequency; + int numPts = std::min(((int)floor(distKm * 1000 / _itmMinSpacing)) + + 1, + _itmMaxNumPts); + int radioClimate = _ituData->getRadioClimateValue(txLatitudeDeg, + txLongitudeDeg); + int radioClimateTmp = + _ituData->getRadioClimateValue(rxLatitudeDeg, + rxLongitudeDeg); + if (radioClimateTmp < radioClimate) { + radioClimate = radioClimateTmp; + } + double surfaceRefractivity = _ituData->getSurfaceRefractivityValue( + (txLatitudeDeg + rxLatitudeDeg) / 2, + (txLongitudeDeg + rxLongitudeDeg) / 2); + double u = _confidenceITM; + pathLoss = UlsMeasurementAnalysis::runPointToPoint( + _terrainDataModel, + false, + QPointF(txLatitudeDeg, txLongitudeDeg), + txHeightM, + QPointF(rxLatitudeDeg, rxLongitudeDeg), + rxHeightM, + distKm, + _itmEpsDielect, + _itmSgmConductivity, + surfaceRefractivity, + frequencyMHz, + radioClimate, + _itmPolarization, + u, + _reliabilityITM, + numPts, + NULL, + ITMProfilePtr); + pathLossModelStr = "ITM"; + pathLossCDF = _confidenceITM; + } + + // ITU-R p.452 Clutter loss function + pathClutterTxDB = AfcManager::computeClutter452HtEl(txHeightM, + distKm, + elevationAngleTxDeg); + pathClutterTxModelStr = "452_HT_ELANG"; + pathClutterTxCDF = 0.5; + + if (_applyClutterFSRxFlag && (rxHeightM <= 10.0) && (distKm >= 1.0)) { + pathClutterRxDB = + AfcManager::computeClutter452HtEl(rxHeightM, + distKm, + elevationAngleRxDeg); + pathClutterRxModelStr = "452_HT_ELANG"; + pathClutterRxCDF = 0.5; + } else { + pathClutterRxDB = 0.0; + pathClutterRxModelStr = "NONE"; + pathClutterRxCDF = 0.5; + } + } else { + throw std::runtime_error(ErrStream() << "ERROR: propEnv = " << propEnv + << " INVALID value"); + } +#endif + } else if (pathLossModel == CConst::FCC6GHzReportAndOrderPathLossModel) { + // Path Loss Model used in FCC Report and Order + + if (fsplDistKm * 1000 < 30.0) { + pathLoss = 20.0 * + log((4 * M_PI * frequency * fsplDistKm * 1000) / CConst::c) / + log(10.0); + pathLossModelStr = "FSPL"; + pathLossCDF = 0.5; + + pathClutterTxDB = 0.0; + pathClutterTxModelStr = "NONE"; + pathClutterTxCDF = 0.5; + } else if (win2DistKm * 1000 < _closeInDist) { + int winner2LOSValue = 0; // 1: Force LOS, 2: Force NLOS, 0: Compute probLOS, + // then select or combine. + if (win2DistKm * 1000 <= 50.0) { + winner2LOSValue = 1; + } else if ((_winner2LOSOption == CConst::BldgDataLOSOption) || + (_winner2LOSOption == CConst::BldgDataReqTxLOSOption) || + (_winner2LOSOption == CConst::BldgDataReqRxLOSOption) || + (_winner2LOSOption == CConst::BldgDataReqTxRxLOSOption) || + (_winner2LOSOption == CConst::CdsmLOSOption)) { + double terrainHeight; + double bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum txHeightSource, rxHeightSource; + _terrainDataModel->getTerrainHeight(txLongitudeDeg, + txLatitudeDeg, + terrainHeight, + bldgHeight, + lidarHeightResult, + txHeightSource); + _terrainDataModel->getTerrainHeight(rxLongitudeDeg, + rxLatitudeDeg, + terrainHeight, + bldgHeight, + lidarHeightResult, + rxHeightSource); + + bool reqTx = (_winner2LOSOption == + CConst::BldgDataReqTxLOSOption) || + (_winner2LOSOption == + CConst::BldgDataReqTxRxLOSOption); + bool reqRx = (_winner2LOSOption == + CConst::BldgDataReqRxLOSOption) || + (_winner2LOSOption == + CConst::BldgDataReqTxRxLOSOption); + + if (((!reqTx) || (txHeightSource == CConst::lidarHeightSource)) && + ((!reqRx) || (rxHeightSource == CConst::lidarHeightSource))) { + int numPts = std::min( + ((int)floor(distKm * 1000 / _itmMinSpacing)) + 1, + _itmMaxNumPts); + bool losFlag = UlsMeasurementAnalysis::isLOS( + _terrainDataModel, + QPointF(txLatitudeDeg, txLongitudeDeg), + txHeightM, + QPointF(rxLatitudeDeg, rxLongitudeDeg), + rxHeightM, + distKm, + numPts, + isLOSProfilePtr, + isLOSSurfaceFracPtr); + if (losFlag) { + if ((_winner2LOSOption == CConst::CdsmLOSOption) && + (*isLOSSurfaceFracPtr < _cdsmLOSThr)) { + winner2LOSValue = 0; + } else { + winner2LOSValue = 1; + } + } else { + winner2LOSValue = 2; + } + } + } else if (_winner2LOSOption == CConst::ForceLOSLOSOption) { + winner2LOSValue = 1; + } else if (_winner2LOSOption == CConst::ForceNLOSLOSOption) { + winner2LOSValue = 2; + } + + double sigma, probLOS; + if (propEnv == CConst::urbanPropEnv) { + // Winner2 C2: urban + pathLoss = Winner2_C2urban(1000 * win2DistKm, + rxHeightM, + txHeightM, + frequency, + sigma, + pathLossModelStr, + pathLossCDF, + probLOS, + winner2LOSValue); + } else if (propEnv == CConst::suburbanPropEnv) { + // Winner2 C1: suburban + pathLoss = Winner2_C1suburban(1000 * win2DistKm, + rxHeightM, + txHeightM, + frequency, + sigma, + pathLossModelStr, + pathLossCDF, + probLOS, + winner2LOSValue); + } else if ((propEnv == CConst::ruralPropEnv) || + (propEnv == CConst::barrenPropEnv)) { + // Winner2 D1: rural + pathLoss = Winner2_D1rural(1000 * win2DistKm, + rxHeightM, + txHeightM, + frequency, + sigma, + pathLossModelStr, + pathLossCDF, + probLOS, + winner2LOSValue); + } else { + throw std::runtime_error(ErrStream() + << "ERROR: propEnv = " << propEnv + << " INVALID value"); + } + if (_winner2LOSOption == CConst::CdsmLOSOption) { + pathLossModelStr += " cdsmFrac = " + + std::to_string(*isLOSSurfaceFracPtr); + } + pathClutterTxModelStr = "NONE"; + pathClutterTxDB = 0.0; + pathClutterTxCDF = 0.5; + } else { + bool rlanHasClutter; + switch (_rlanITMTxClutterMethod) { + case CConst::ForceTrueITMClutterMethod: + rlanHasClutter = true; + break; + case CConst::ForceFalseITMClutterMethod: + rlanHasClutter = false; + break; + case CConst::BldgDataITMCLutterMethod: { + int numPts = std::min( + ((int)floor(distKm * 1000 / _itmMinSpacing)) + 1, + _itmMaxNumPts); + bool losFlag = UlsMeasurementAnalysis::isLOS( + _terrainDataModel, + QPointF(txLatitudeDeg, txLongitudeDeg), + txHeightM, + QPointF(rxLatitudeDeg, rxLongitudeDeg), + rxHeightM, + distKm, + numPts, + isLOSProfilePtr, + isLOSSurfaceFracPtr); + rlanHasClutter = !losFlag; + } break; + } + + if ((propEnv == CConst::urbanPropEnv) || + (propEnv == CConst::suburbanPropEnv)) { + if (itmFSPLFlag) { + pathLoss = 20.0 * + log((4 * M_PI * frequency * fsplDistKm * 1000) / + CConst::c) / + log(10.0); + pathLossModelStr = "FSPL"; + pathLossCDF = 0.5; + } else { + // Terrain propagation: SRTM + ITM + double frequencyMHz = 1.0e-6 * frequency; + int numPts = std::min( + ((int)floor(distKm * 1000 / _itmMinSpacing)) + 1, + _itmMaxNumPts); + int radioClimate = + _ituData->getRadioClimateValue(txLatitudeDeg, + txLongitudeDeg); + int radioClimateTmp = + _ituData->getRadioClimateValue(rxLatitudeDeg, + rxLongitudeDeg); + if (radioClimateTmp < radioClimate) { + radioClimate = radioClimateTmp; + } + double surfaceRefractivity = + _ituData->getSurfaceRefractivityValue( + (txLatitudeDeg + rxLatitudeDeg) / 2, + (txLongitudeDeg + rxLongitudeDeg) / 2); + double u = _confidenceITM; + pathLoss = UlsMeasurementAnalysis::runPointToPoint( + _terrainDataModel, + false, + QPointF(txLatitudeDeg, txLongitudeDeg), + txHeightM, + QPointF(rxLatitudeDeg, rxLongitudeDeg), + rxHeightM, + distKm, + _itmEpsDielect, + _itmSgmConductivity, + surfaceRefractivity, + frequencyMHz, + radioClimate, + _itmPolarization, + u, + _reliabilityITM, + numPts, + NULL, + ITMProfilePtr); + pathLossModelStr = "ITM"; + pathLossCDF = _confidenceITM; + } + + if (rlanHasClutter) { + // ITU-R P.[CLUTTER] sec 3.2 + double Ll = 23.5 + 9.6 * log(frequencyGHz) / log(10.0); + double Ls = 32.98 + 23.9 * log(distKm) / log(10.0) + + 3.0 * log(frequencyGHz) / log(10.0); + + arma::vec gauss(1); + gauss[0] = _zclutter2108; + + double Lctt = -5.0 * + log(exp(-0.2 * Ll * log(10.0)) + + exp(-0.2 * Ls * log(10.0))) / + log(10.0) + + 6.0 * gauss[0]; + + pathClutterTxDB = Lctt; + pathClutterTxModelStr = "P.2108"; + pathClutterTxCDF = q(-gauss[0]); + } else { + pathClutterTxModelStr = "NONE"; + pathClutterTxDB = 0.0; + pathClutterTxCDF = 0.5; + } + + } else if ((propEnv == CConst::ruralPropEnv) || + (propEnv == CConst::barrenPropEnv)) { + if (itmFSPLFlag) { + pathLoss = 20.0 * + log((4 * M_PI * frequency * fsplDistKm * 1000) / + CConst::c) / + log(10.0); + pathLossModelStr = "FSPL"; + pathLossCDF = 0.5; + } else { + // Terrain propagation: SRTM + ITM + double frequencyMHz = 1.0e-6 * frequency; + double u = _confidenceITM; + int numPts = std::min( + ((int)floor(distKm * 1000 / _itmMinSpacing)) + 1, + _itmMaxNumPts); + int radioClimate = + _ituData->getRadioClimateValue(txLatitudeDeg, + txLongitudeDeg); + int radioClimateTmp = + _ituData->getRadioClimateValue(rxLatitudeDeg, + rxLongitudeDeg); + if (radioClimateTmp < radioClimate) { + radioClimate = radioClimateTmp; + } + double surfaceRefractivity = + _ituData->getSurfaceRefractivityValue( + (txLatitudeDeg + rxLatitudeDeg) / 2, + (txLongitudeDeg + rxLongitudeDeg) / 2); + pathLoss = UlsMeasurementAnalysis::runPointToPoint( + _terrainDataModel, + false, + QPointF(txLatitudeDeg, txLongitudeDeg), + txHeightM, + QPointF(rxLatitudeDeg, rxLongitudeDeg), + rxHeightM, + distKm, + _itmEpsDielect, + _itmSgmConductivity, + surfaceRefractivity, + frequencyMHz, + radioClimate, + _itmPolarization, + u, + _reliabilityITM, + numPts, + NULL, + ITMProfilePtr); + pathLossModelStr = "ITM"; + pathLossCDF = _confidenceITM; + } + + if ((rlanHasClutter) && + (nlcdLandCatTx == CConst::noClutterNLCDLandCat)) { + rlanHasClutter = false; + } + + if (rlanHasClutter) { + double ha, dk; + switch (nlcdLandCatTx) { + case CConst::deciduousTreesNLCDLandCat: + ha = 15.0; + dk = 0.05; + if (txClutterStrPtr) { + *txClutterStrPtr = "DECIDUOUS_" + "TREES"; + } + break; + case CConst::coniferousTreesNLCDLandCat: + ha = 20.0; + dk = 0.05; + if (txClutterStrPtr) { + *txClutterStrPtr = "CONIFEROUS_" + "TREES"; + } + break; + case CConst::highCropFieldsNLCDLandCat: + ha = 4.0; + dk = 0.1; + if (txClutterStrPtr) { + *txClutterStrPtr = "HIGH_CROP_" + "FIELDS"; + } + break; + case CConst::villageCenterNLCDLandCat: + case CConst::unknownNLCDLandCat: + ha = 5.0; + dk = 0.07; + if (txClutterStrPtr) { + *txClutterStrPtr = "VILLAGE_CENTER"; + } + break; + case CConst::tropicalRainForestNLCDLandCat: + ha = 20.0; + dk = 0.03; + if (txClutterStrPtr) { + *txClutterStrPtr = "TROPICAL_RAIN_" + "FOREST"; + } + break; + default: + ha = quietNaN; + dk = quietNaN; + CORE_DUMP; + break; + } + + if (distKm < 10 * dk) { + pathClutterTxDB = 0.0; + } else { + double elevationAngleThresholdDeg = + std::atan((ha - txHeightM) / + (dk * 1000.0)) * + 180.0 / M_PI; + if (elevationAngleTxDeg > + elevationAngleThresholdDeg) { + pathClutterTxDB = 0.0; + } else { + const double Ffc = + 0.25 + + 0.375 * (1 + + std::tanh(7.5 * + (frequencyGHz - + 0.5))); + double result = 10.25 * Ffc * exp(-1 * dk); + result *= 1 - + std::tanh(6 * (txHeightM / ha - + 0.625)); + result -= 0.33; + pathClutterTxDB = result; + } + } + + pathClutterTxModelStr = "452_NLCD"; + pathClutterTxCDF = 0.5; + } else { + pathClutterTxModelStr = "NONE"; + pathClutterTxDB = 0.0; + pathClutterTxCDF = 0.5; + } + } else { + CORE_DUMP; + } + } + + if (_applyClutterFSRxFlag && (rxHeightM <= _maxFsAglHeight) && (distKm >= 1.0)) { + if (distKm * 1000 < _closeInDist) { + pathClutterRxDB = 0.0; + pathClutterRxModelStr = "NONE"; + pathClutterRxCDF = 0.5; + } else if ((propEnvRx == CConst::urbanPropEnv) || + (propEnvRx == CConst::suburbanPropEnv)) { + // ITU-R P.[CLUTTER] sec 3.2 + double Ll = 23.5 + 9.6 * log(frequencyGHz) / log(10.0); + double Ls = 32.98 + 23.9 * log(distKm) / log(10.0) + + 3.0 * log(frequencyGHz) / log(10.0); + + arma::vec gauss(1); + gauss[0] = _fsZclutter2108; + + double Lctt = -5.0 * + log(exp(-0.2 * Ll * log(10.0)) + + exp(-0.2 * Ls * log(10.0))) / + log(10.0) + + 6.0 * gauss[0]; + + pathClutterRxDB = Lctt; + pathClutterRxModelStr = "P.2108"; + pathClutterRxCDF = q(-gauss[0]); + } else if ((propEnvRx == CConst::ruralPropEnv) || + (propEnvRx == CConst::barrenPropEnv)) { + bool clutterFlag = _allowRuralFSClutterFlag && + (nlcdLandCatRx == CConst::noClutterNLCDLandCat ? + false : + true); + + if (clutterFlag) { + double ha, dk; + switch (nlcdLandCatRx) { + case CConst::deciduousTreesNLCDLandCat: + ha = 15.0; + dk = 0.05; + if (rxClutterStrPtr) { + *rxClutterStrPtr = "DECIDUOUS_" + "TREES"; + } + break; + case CConst::coniferousTreesNLCDLandCat: + ha = 20.0; + dk = 0.05; + if (rxClutterStrPtr) { + *rxClutterStrPtr = "CONIFEROUS_" + "TREES"; + } + break; + case CConst::highCropFieldsNLCDLandCat: + ha = 4.0; + dk = 0.1; + if (rxClutterStrPtr) { + *rxClutterStrPtr = "HIGH_CROP_" + "FIELDS"; + } + break; + case CConst::villageCenterNLCDLandCat: + case CConst::unknownNLCDLandCat: + ha = 5.0; + dk = 0.07; + if (rxClutterStrPtr) { + *rxClutterStrPtr = "VILLAGE_CENTER"; + } + break; + default: + CORE_DUMP; + break; + } + + if (distKm < 10 * dk) { + pathClutterRxDB = 0.0; + } else { + double elevationAngleThresholdDeg = + std::atan((ha - rxHeightM) / + (dk * 1000.0)) * + 180.0 / M_PI; + if (elevationAngleRxDeg > + elevationAngleThresholdDeg) { + pathClutterRxDB = 0.0; + } else { + const double Ffc = + 0.25 + + 0.375 * (1 + + std::tanh(7.5 * + (frequencyGHz - + 0.5))); + double result = 10.25 * Ffc * exp(-1 * dk); + result *= 1 - + std::tanh(6 * (rxHeightM / ha - + 0.625)); + result -= 0.33; + pathClutterRxDB = result; + } + } + + pathClutterRxModelStr = "452_NLCD"; + pathClutterRxCDF = 0.5; + } else { + pathClutterRxDB = 0.0; + pathClutterRxModelStr = "NONE"; + pathClutterRxCDF = 0.5; + } + } else { + throw std::runtime_error( + ErrStream() << "ERROR: Invalid morphology for location " + << rxLongitudeDeg << " " << rxLatitudeDeg); + } + } else { + pathClutterRxDB = 0.0; + pathClutterRxModelStr = "NONE"; + pathClutterRxCDF = 0.5; + } + } else if (pathLossModel == CConst::FSPLPathLossModel) { + pathLoss = 20.0 * log((4 * M_PI * frequency * fsplDistKm * 1000) / CConst::c) / + log(10.0); + pathLossModelStr = "FSPL"; + pathLossCDF = 0.5; + + pathClutterTxDB = 0.0; + pathClutterTxModelStr = "NONE"; + pathClutterTxCDF = 0.5; + + pathClutterRxDB = 0.0; + pathClutterRxModelStr = "NONE"; + pathClutterRxCDF = 0.5; + } else { + throw std::runtime_error(ErrStream() << "ERROR reading ULS data: pathLossModel = " + << pathLossModel << " INVALID value"); + } + + if (_pathLossClampFSPL) { + double fspl = 20.0 * log((4 * M_PI * frequency * fsplDistKm * 1000) / CConst::c) / + log(10.0); + if (pathLoss < fspl) { + pathLossModelStr += "_" + std::to_string(pathLoss) + "_CLAMPFSPL"; + pathLoss = fspl; + } + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::Winner2_C1suburban_LOS ****/ +/**** Winner II: C1, suburban LOS ****/ +/**** distance = link distance (m) ****/ +/**** hBS = BS antenna height (m) ****/ +/**** hMS = MS antenna height (m) ****/ +/**** frequency = Frequency (Hz) ****/ +/******************************************************************************************/ +double AfcManager::Winner2_C1suburban_LOS(double distance, + double hBS, + double hMS, + double frequency, + double zval, + double &sigma, + double &pathLossCDF) const +{ + double retval; + + double dBP = 4 * hBS * hMS * frequency / CConst::c; + + if (distance < 30.0) { + // FSPL + sigma = 0.0; + retval = -(20.0 * log10(CConst::c / (4 * M_PI * frequency * distance))); + } else if (distance < dBP) { + sigma = 4.0; + retval = 23.8 * log10(distance) + 41.2 + 20 * log10(frequency * 1.0e-9 / 5); + } else { + sigma = 6.0; + retval = 40.0 * log10(distance) + 11.65 - 16.2 * log10(hBS) - 16.2 * log10(hMS) + + 3.8 * log10(frequency * 1.0e-9 / 5); + } + + arma::vec gauss(1); + gauss[0] = zval; + + retval += sigma * (gauss[0]); + pathLossCDF = q(-gauss[0]); + + return (retval); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::Winner2_C1suburban_NLOS ****/ +/**** Winner II: C1, suburban NLOS ****/ +/**** distance = link distance (m) ****/ +/**** hBS = BS antenna height (m) ****/ +/**** hMS = MS antenna height (m) ****/ +/**** frequency = Frequency (Hz) ****/ +/******************************************************************************************/ +double AfcManager::Winner2_C1suburban_NLOS(double distance, + double hBS, + double /* hMS */, + double frequency, + double zval, + double &sigma, + double &pathLossCDF) const +{ + double retval; + + sigma = 8.0; + retval = (44.9 - 6.55 * log10(hBS)) * log10(distance) + 31.46 + 5.83 * log10(hBS) + + 23.0 * log10(frequency * 1.0e-9 / 5); + + arma::vec gauss(1); + gauss[0] = zval; + + retval += sigma * (gauss[0]); + pathLossCDF = q(-gauss[0]); + + return (retval); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::Winner2_C1suburban ****/ +/**** Winner II: C1, suburban ****/ +/**** distance = link distance (m) ****/ +/**** hBS = BS antenna height (m) ****/ +/**** hMS = MS antenna height (m) ****/ +/**** frequency = Frequency (Hz) ****/ +/******************************************************************************************/ +double AfcManager::Winner2_C1suburban(double distance, + double hBS, + double hMS, + double frequency, + double &sigma, + std::string &pathLossModelStr, + double &pathLossCDF, + double &probLOS, + int losValue) const +{ + double retval = quietNaN; + + if ((losValue == 0) && (_closeInHgtFlag) && (hMS > _closeInHgtLOS)) { + losValue = 1; + probLOS = 1.0; + } else { + probLOS = exp(-distance / 200); + } + + if (losValue == 0) { + if (_winner2UnknownLOSMethod == CConst::PLOSCombineWinner2UnknownLOSMethod) { + double sigmaLOS, sigmaNLOS; + double plLOS = Winner2_C1suburban_LOS(distance, + hBS, + hMS, + frequency, + 0.0, + sigmaLOS, + pathLossCDF); + double plNLOS = Winner2_C1suburban_NLOS(distance, + hBS, + hMS, + frequency, + 0.0, + sigmaNLOS, + pathLossCDF); + retval = probLOS * plLOS + (1.0 - probLOS) * plNLOS; + sigma = sqrt(probLOS * probLOS * sigmaLOS * sigmaLOS + + (1.0 - probLOS) * (1.0 - probLOS) * sigmaNLOS * sigmaNLOS); + + arma::vec gauss(1); + gauss[0] = _zwinner2Combined; + + retval += sigma * gauss[0]; + pathLossCDF = q(-(gauss[0])); + + pathLossModelStr = "W2C1_SUBURBAN_COMB"; + } else if (_winner2UnknownLOSMethod == + CConst::PLOSThresholdWinner2UnknownLOSMethod) { + if (probLOS > _winner2ProbLOSThr) { + retval = Winner2_C1suburban_LOS(distance, + hBS, + hMS, + frequency, + _zwinner2LOS, + sigma, + pathLossCDF); + pathLossModelStr = "W2C1_SUBURBAN_LOS"; + } else { + retval = Winner2_C1suburban_NLOS(distance, + hBS, + hMS, + frequency, + _zwinner2NLOS, + sigma, + pathLossCDF); + pathLossModelStr = "W2C1_SUBURBAN_NLOS"; + } + } else { + CORE_DUMP; + } + } else if (losValue == 1) { + retval = Winner2_C1suburban_LOS(distance, + hBS, + hMS, + frequency, + _zwinner2LOS, + sigma, + pathLossCDF); + pathLossModelStr = "W2C1_SUBURBAN_LOS"; + } else if (losValue == 2) { + retval = Winner2_C1suburban_NLOS(distance, + hBS, + hMS, + frequency, + _zwinner2NLOS, + sigma, + pathLossCDF); + pathLossModelStr = "W2C1_SUBURBAN_NLOS"; + } else { + CORE_DUMP; + } + + return (retval); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::Winner2_C2urban_LOS ****/ +/**** Winner II: C2, urban LOS ****/ +/**** distance = link distance (m) ****/ +/**** hBS = BS antenna height (m) ****/ +/**** hMS = MS antenna height (m) ****/ +/**** frequency = Frequency (Hz) ****/ +/******************************************************************************************/ +double AfcManager::Winner2_C2urban_LOS(double distance, + double hBS, + double hMS, + double frequency, + double zval, + double &sigma, + double &pathLossCDF) const +{ + double retval; + + double dBP = 4 * (hBS - 1.0) * (hMS - 1.0) * frequency / CConst::c; + + if (distance < 10.0) { + // FSPL + sigma = 0.0; + retval = -(20.0 * log10(CConst::c / (4 * M_PI * frequency * distance))); + } else if (distance < dBP) { + sigma = 4.0; + retval = 26.0 * log10(distance) + 39.0 + 20 * log10(frequency * 1.0e-9 / 5); + } else { + sigma = 6.0; + retval = 40.0 * log10(distance) + 13.47 - 14.0 * log10(hBS - 1) - + 14.0 * log10(hMS - 1) + 6.0 * log10(frequency * 1.0e-9 / 5); + } + + arma::vec gauss(1); + gauss[0] = zval; + + retval += sigma * (gauss[0]); + pathLossCDF = q(-gauss[0]); + + return (retval); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::Winner2_C2urban_NLOS ****/ +/**** Winner II: C2, urban NLOS ****/ +/**** distance = link distance (m) ****/ +/**** hBS = BS antenna height (m) ****/ +/**** hMS = MS antenna height (m) ****/ +/**** frequency = Frequency (Hz) ****/ +/******************************************************************************************/ +double AfcManager::Winner2_C2urban_NLOS(double distance, + double hBS, + double /* hMS */, + double frequency, + double zval, + double &sigma, + double &pathLossCDF) const +{ + double retval; + + sigma = 8.0; + retval = (44.9 - 6.55 * log10(hBS)) * log10(distance) + 34.46 + 5.83 * log10(hBS) + + 23.0 * log10(frequency * 1.0e-9 / 5); + + arma::vec gauss(1); + gauss[0] = zval; + + retval += sigma * (gauss[0]); + pathLossCDF = q(-gauss[0]); + + return (retval); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::Winner2_C2urban ****/ +/**** Winner II: C2, suburban ****/ +/**** distance = link distance (m) ****/ +/**** hBS = BS antenna height (m) ****/ +/**** hMS = MS antenna height (m) ****/ +/**** frequency = Frequency (Hz) ****/ +/******************************************************************************************/ +double AfcManager::Winner2_C2urban(double distance, + double hBS, + double hMS, + double frequency, + double &sigma, + std::string &pathLossModelStr, + double &pathLossCDF, + double &probLOS, + int losValue) const +{ + double retval = quietNaN; + + if ((losValue == 0) && (_closeInHgtFlag) && (hMS > _closeInHgtLOS)) { + losValue = 1; + probLOS = 1.0; + } else { + probLOS = (distance > 18.0 ? 18.0 / distance : 1.0) * + (1.0 - exp(-distance / 63.0)) + + exp(-distance / 63.0); + } + + if (losValue == 0) { + if (_winner2UnknownLOSMethod == CConst::PLOSCombineWinner2UnknownLOSMethod) { + double sigmaLOS, sigmaNLOS; + double plLOS = Winner2_C2urban_LOS(distance, + hBS, + hMS, + frequency, + 0.0, + sigmaLOS, + pathLossCDF); + double plNLOS = Winner2_C2urban_NLOS(distance, + hBS, + hMS, + frequency, + 0.0, + sigmaNLOS, + pathLossCDF); + retval = probLOS * plLOS + (1.0 - probLOS) * plNLOS; + sigma = sqrt(probLOS * probLOS * sigmaLOS * sigmaLOS + + (1.0 - probLOS) * (1.0 - probLOS) * sigmaNLOS * sigmaNLOS); + + arma::vec gauss(1); + gauss[0] = _zwinner2Combined; + + retval += sigma * gauss[0]; + pathLossCDF = q(-(gauss[0])); + + pathLossModelStr = "W2C2_URBAN_COMB"; + } else if (_winner2UnknownLOSMethod == + CConst::PLOSThresholdWinner2UnknownLOSMethod) { + if (probLOS > _winner2ProbLOSThr) { + retval = Winner2_C2urban_LOS(distance, + hBS, + hMS, + frequency, + _zwinner2LOS, + sigma, + pathLossCDF); + pathLossModelStr = "W2C2_URBAN_LOS"; + } else { + retval = Winner2_C2urban_NLOS(distance, + hBS, + hMS, + frequency, + _zwinner2NLOS, + sigma, + pathLossCDF); + pathLossModelStr = "W2C2_URBAN_NLOS"; + } + } else { + CORE_DUMP; + } + } else if (losValue == 1) { + retval = Winner2_C2urban_LOS(distance, + hBS, + hMS, + frequency, + _zwinner2LOS, + sigma, + pathLossCDF); + pathLossModelStr = "W2C2_URBAN_LOS"; + } else if (losValue == 2) { + retval = Winner2_C2urban_NLOS(distance, + hBS, + hMS, + frequency, + _zwinner2NLOS, + sigma, + pathLossCDF); + pathLossModelStr = "W2C2_URBAN_NLOS"; + } else { + CORE_DUMP; + } + + return (retval); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::Winner2_D1rural_LOS ****/ +/**** Winner II: D1, rural LOS ****/ +/**** distance = link distance (m) ****/ +/**** hBS = BS antenna height (m) ****/ +/**** hMS = MS antenna height (m) ****/ +/**** frequency = Frequency (Hz) ****/ +/******************************************************************************************/ +double AfcManager::Winner2_D1rural_LOS(double distance, + double hBS, + double hMS, + double frequency, + double zval, + double &sigma, + double &pathLossCDF) const +{ + double retval; + + double dBP = 4 * (hBS) * (hMS)*frequency / CConst::c; + + if (distance < 10.0) { + // FSPL + sigma = 0.0; + retval = -(20.0 * log10(CConst::c / (4 * M_PI * frequency * distance))); + } else if (distance < dBP) { + sigma = 4.0; + retval = 21.5 * log10(distance) + 44.2 + 20 * log10(frequency * 1.0e-9 / 5); + } else { + sigma = 6.0; + retval = 40.0 * log10(distance) + 10.5 - 18.5 * log10(hBS) - 18.5 * log10(hMS) + + 1.5 * log10(frequency * 1.0e-9 / 5); + } + + arma::vec gauss(1); + gauss[0] = zval; + + retval += sigma * gauss[0]; + pathLossCDF = q(-gauss[0]); + + return (retval); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::Winner2_D1rural_NLOS ****/ +/**** Winner II: D1, rural NLOS ****/ +/**** distance = link distance (m) ****/ +/**** hBS = BS antenna height (m) ****/ +/**** hMS = MS antenna height (m) ****/ +/**** frequency = Frequency (Hz) ****/ +/******************************************************************************************/ +double AfcManager::Winner2_D1rural_NLOS(double distance, + double hBS, + double hMS, + double frequency, + double zval, + double &sigma, + double &pathLossCDF) const +{ + double retval; + + sigma = 8.0; + retval = 25.1 * log10(distance) + 55.4 - 0.13 * (hBS - 25) * log10(distance / 100) - + 0.9 * (hMS - 1.5) + 21.3 * log10(frequency * 1.0e-9 / 5); + + arma::vec gauss(1); + gauss[0] = zval; + + retval += sigma * gauss[0]; + pathLossCDF = q(-gauss[0]); + + return (retval); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AfcManager::Winner2_D1rural ****/ +/**** Winner II: D1, rural ****/ +/**** distance = link distance (m) ****/ +/**** hBS = BS antenna height (m) ****/ +/**** hMS = MS antenna height (m) ****/ +/**** frequency = Frequency (Hz) ****/ +/******************************************************************************************/ +double AfcManager::Winner2_D1rural(double distance, + double hBS, + double hMS, + double frequency, + double &sigma, + std::string &pathLossModelStr, + double &pathLossCDF, + double &probLOS, + int losValue) const +{ + double retval = quietNaN; + + if ((losValue == 0) && (_closeInHgtFlag) && (hMS > _closeInHgtLOS)) { + losValue = 1; + probLOS = 1.0; + } else { + probLOS = exp(-distance / 1000); + } + + if (losValue == 0) { + if (_winner2UnknownLOSMethod == CConst::PLOSCombineWinner2UnknownLOSMethod) { + double sigmaLOS, sigmaNLOS; + double plLOS = Winner2_D1rural_LOS(distance, + hBS, + hMS, + frequency, + 0.0, + sigmaLOS, + pathLossCDF); + double plNLOS = Winner2_D1rural_NLOS(distance, + hBS, + hMS, + frequency, + 0.0, + sigmaNLOS, + pathLossCDF); + retval = probLOS * plLOS + (1.0 - probLOS) * plNLOS; + sigma = sqrt(probLOS * probLOS * sigmaLOS * sigmaLOS + + (1.0 - probLOS) * (1.0 - probLOS) * sigmaNLOS * sigmaNLOS); + + arma::vec gauss(1); + gauss[0] = _zwinner2Combined; + + retval += sigma * gauss[0]; + pathLossCDF = q(-(gauss[0])); + + pathLossModelStr = "W2D1_RURAL_COMB"; + } else if (_winner2UnknownLOSMethod == + CConst::PLOSThresholdWinner2UnknownLOSMethod) { + if (probLOS > _winner2ProbLOSThr) { + retval = Winner2_D1rural_LOS(distance, + hBS, + hMS, + frequency, + _zwinner2LOS, + sigma, + pathLossCDF); + pathLossModelStr = "W2D1_RURAL_LOS"; + } else { + retval = Winner2_D1rural_NLOS(distance, + hBS, + hMS, + frequency, + _zwinner2NLOS, + sigma, + pathLossCDF); + pathLossModelStr = "W2D1_RURAL_NLOS"; + } + } else { + CORE_DUMP; + } + } else if (losValue == 1) { + retval = Winner2_D1rural_LOS(distance, + hBS, + hMS, + frequency, + _zwinner2LOS, + sigma, + pathLossCDF); + pathLossModelStr = "W2D1_RURAL_LOS"; + } else if (losValue == 2) { + retval = Winner2_D1rural_NLOS(distance, + hBS, + hMS, + frequency, + _zwinner2NLOS, + sigma, + pathLossCDF); + pathLossModelStr = "W2D1_RURAL_NLOS"; + } else { + CORE_DUMP; + } + + return (retval); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/* AfcManager::compute() */ +/******************************************************************************************/ +void AfcManager::compute() +{ + if (_responseCode != CConst::successResponseCode) { + return; + } + + // initialize all channels to max EIRP before computing + for (auto &channel : _channelList) { + for (int freqSegIdx = 0; freqSegIdx < channel.segList.size(); ++freqSegIdx) { + double segEIRP; + if (channel.type == INQUIRED_FREQUENCY) { + segEIRP = _inquiredFrequencyMaxPSD_dBmPerMHz + + 10.0 * log10((double)channel.bandwidth(freqSegIdx)); + } else { + segEIRP = _maxEIRP_dBm; + } + std::get<0>(channel.segList[freqSegIdx]) = segEIRP; + std::get<1>(channel.segList[freqSegIdx]) = segEIRP; + } + } + + if (_analysisType == "AP-AFC") { + runPointAnalysis(); + } else if (_analysisType == "ScanAnalysis") { + runScanAnalysis(); + } else if (_analysisType == "ExclusionZoneAnalysis") { + runExclusionZoneAnalysis(); + } else if (_analysisType == "HeatmapAnalysis") { + runHeatmapAnalysis(); +#if DEBUG_AFC + } else if (_analysisType == "test_itm") { + runTestITM("path_trace_afc.csv"); + } else if (_analysisType == "test_winner2") { + runTestWinner2("w2_alignment.csv", "w2_alignment_afc.csv"); + } else if (_analysisType == "test_aciFn") { + double fStartMHz = -5.0; + double fStopMHz = 25.0; + double BMHz = 40.0; + printf("fStartMHz = %.10f\n", fStartMHz); + printf("fStopMHz = %.10f\n", fStopMHz); + printf("BMHz = %.10f\n", BMHz); + double aciFnStart = aciFn(fStartMHz, BMHz); + double aciFnStop = aciFn(fStopMHz, BMHz); + printf("aciFnStart = %.10f\n", aciFnStart); + printf("aciFnStop = %.10f\n", aciFnStop); + } else if (_analysisType == "ANALYZE_NLCD") { + runAnalyzeNLCD(); +#endif + } else { + throw std::runtime_error(ErrStream() << "ERROR: Unrecognized analysis type = \"" + << _analysisType << "\""); + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/* AfcManager::runPointAnalysis() */ +/******************************************************************************************/ +void AfcManager::runPointAnalysis() +{ + std::ostringstream errStr; + +#if DEBUG_AFC + // std::vector fsidTraceList{2128, 3198, 82443}; + // std::vector fsidTraceList{64324}; + std::vector fsidTraceList {24175}; + std::string pathTraceFile = "path_trace.csv.gz"; +#endif + + LOGGER_INFO(logger) << "Executing AfcManager::runPointAnalysis()"; + + _rlanRegion->configure(_rlanHeightType, _terrainDataModel); + + _rlanPointing = _rlanRegion->computePointing(_rlanAzimuthPointing, _rlanElevationPointing); + + /**************************************************************************************/ + /* Get Uncertainty Region Scan Points */ + /* scanPointList: scan points in horizontal plane (lon/lat) */ + /* For scan point scanPtIdx, numRlanHt[scanPtIdx] heights are considered. */ + /**************************************************************************************/ + std::vector scanPointList = _rlanRegion->getScan(_scanRegionMethod, + _scanres_xy, + _scanres_points_per_degree); + + // Sorting ULS most-interferable-first + std::vector sortedUlsList(getSortedUls()); + + double heightUncertainty = _rlanRegion->getHeightUncertainty(); + int NHt = (int)ceil(heightUncertainty / _scanres_ht); + Vector3 rlanPosnList[scanPointList.size()][2 * NHt + 1]; + GeodeticCoord rlanCoordList[scanPointList.size()][2 * NHt + 1]; + int numRlanHt[scanPointList.size()]; + double rlanTerrainHeight[scanPointList.size()]; + CConst::HeightSourceEnum rlanHeightSource[scanPointList.size()]; + CConst::PropEnvEnum rlanPropEnv[scanPointList.size()]; + CConst::NLCDLandCatEnum rlanNlcdLandCat[scanPointList.size()]; + + int scanPtIdx, rlanHtIdx; + for (scanPtIdx = 0; scanPtIdx < (int)scanPointList.size(); scanPtIdx++) { + LatLon scanPt = scanPointList[scanPtIdx]; + + double bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + _terrainDataModel->getTerrainHeight(scanPt.second, + scanPt.first, + rlanTerrainHeight[scanPtIdx], + bldgHeight, + lidarHeightResult, + rlanHeightSource[scanPtIdx]); + + double height0; + if (_rlanRegion->getFixedHeightAMSL()) { + height0 = _rlanRegion->getCenterHeightAMSL(); + } else { + height0 = _rlanRegion->getCenterHeightAMSL() - + _rlanRegion->getCenterTerrainHeight() + + rlanTerrainHeight[scanPtIdx]; + } + + int htIdx; + numRlanHt[scanPtIdx] = 0; + bool lowHeightFlag = false; + for (htIdx = 0; (htIdx <= 2 * NHt) && (!lowHeightFlag); ++htIdx) { + double heightAMSL = height0 + + (NHt ? (NHt - htIdx) * heightUncertainty / NHt : + 0.0); // scan from top down + double heightAGL = heightAMSL - rlanTerrainHeight[scanPtIdx]; + bool useFlag; + if (heightAGL < _minRlanHeightAboveTerrain) { + switch (_scanPointBelowGroundMethod) { + case CConst::DiscardScanPointBelowGroundMethod: + useFlag = false; + break; + case CConst::TruncateScanPointBelowGroundMethod: + heightAMSL = rlanTerrainHeight[scanPtIdx] + + _minRlanHeightAboveTerrain; + useFlag = true; + break; + default: + CORE_DUMP; + break; + } + lowHeightFlag = true; + } else { + useFlag = true; + } + if (useFlag) { + rlanCoordList[scanPtIdx][htIdx] = + GeodeticCoord::fromLatLon(scanPt.first, + scanPt.second, + heightAMSL / 1000.0); + rlanPosnList[scanPtIdx][htIdx] = EcefModel::fromGeodetic( + rlanCoordList[scanPtIdx][htIdx]); + numRlanHt[scanPtIdx]++; + } + } + + rlanPropEnv[scanPtIdx] = computePropEnv(scanPt.second, + scanPt.first, + rlanNlcdLandCat[scanPtIdx]); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Create excThrFile, useful for debugging */ + /**************************************************************************************/ + ExThrGzipCsv *excthrGc = (ExThrGzipCsv *)NULL; + if (!_excThrFile.empty()) { + excthrGc = new ExThrGzipCsv(_excThrFile); + } + + EirpGzipCsv eirpGc(_eirpGcFile); + /**************************************************************************************/ + + /**************************************************************************************/ + /* Create KML file */ + /**************************************************************************************/ + ZXmlWriter kml_writer(_kmlFile); + auto &fkml = kml_writer.xml_writer; + + if (fkml) { + fkml->setAutoFormatting(true); + fkml->writeStartDocument(); + fkml->writeStartElement("kml"); + fkml->writeAttribute("xmlns", "http://www.opengis.net/kml/2.2"); + fkml->writeStartElement("Document"); + fkml->writeTextElement("name", "AFC"); + fkml->writeTextElement("open", "1"); + fkml->writeTextElement("description", "Display Point Analysis Results"); + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "transGrayPoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "7d7f7f7f"); + fkml->writeEndElement(); // Polystyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "transBluePoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "7dff0000"); + fkml->writeEndElement(); // PolyStyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "redPoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("color", "ff0000ff"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "7d0000ff"); + fkml->writeEndElement(); // Polystyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "yellowPoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("color", "ff00ffff"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "7d00ffff"); + fkml->writeEndElement(); // Polystyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "greenPoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("color", "ff00ff00"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "7d00ff00"); + fkml->writeEndElement(); // Polystyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "bluePoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "ffff0000"); + fkml->writeEndElement(); // PolyStyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "blackPoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("color", "ff000000"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "7d000000"); + fkml->writeEndElement(); // Polystyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "dotStyle"); + fkml->writeStartElement("IconStyle"); + fkml->writeStartElement("Icon"); + fkml->writeTextElement("href", + "http://maps.google.com/mapfiles/kml/shapes/" + "placemark_circle.png"); + fkml->writeEndElement(); // Icon + fkml->writeEndElement(); // IconStyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "redPlacemark"); + fkml->writeStartElement("IconStyle"); + fkml->writeTextElement("color", "ff0000ff"); + fkml->writeStartElement("Icon"); + fkml->writeTextElement("href", + "http://maps.google.com/mapfiles/kml/pushpin/" + "ylw-pushpin.png"); + fkml->writeEndElement(); // Icon + fkml->writeEndElement(); // IconStyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "yellowPlacemark"); + fkml->writeStartElement("IconStyle"); + fkml->writeTextElement("color", "ff00ffff"); + fkml->writeStartElement("Icon"); + fkml->writeTextElement("href", + "http://maps.google.com/mapfiles/kml/pushpin/" + "ylw-pushpin.png"); + fkml->writeEndElement(); // Icon + fkml->writeEndElement(); // IconStyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "greenPlacemark"); + fkml->writeStartElement("IconStyle"); + fkml->writeTextElement("color", "ff00ff00"); + fkml->writeStartElement("Icon"); + fkml->writeTextElement("href", + "http://maps.google.com/mapfiles/kml/pushpin/" + "ylw-pushpin.png"); + fkml->writeEndElement(); // Icon + fkml->writeEndElement(); // IconStyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "blackPlacemark"); + fkml->writeStartElement("IconStyle"); + fkml->writeTextElement("color", "ff000000"); + fkml->writeStartElement("Icon"); + fkml->writeTextElement("href", + "http://maps.google.com/mapfiles/kml/pushpin/" + "ylw-pushpin.png"); + fkml->writeEndElement(); // Icon + fkml->writeEndElement(); // IconStyle + fkml->writeEndElement(); // Style + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Draw uncertainty cylinder in KML */ + /**************************************************************************************/ + if (fkml) { + int ptIdx; + fkml->writeStartElement("Folder"); + fkml->writeTextElement("name", "RLAN"); + + std::vector ptList = _rlanRegion->getBoundary(_terrainDataModel); + + /**********************************************************************************/ + /* CENTER */ + /**********************************************************************************/ + GeodeticCoord rlanCenterPtGeo = EcefModel::toGeodetic(_rlanRegion->getCenterPosn()); + + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", "CENTER"); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#dotStyle"); + fkml->writeStartElement("Point"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeTextElement("coordinates", + QString::asprintf("%.10f,%.10f,%.2f", + rlanCenterPtGeo.longitudeDeg, + rlanCenterPtGeo.latitudeDeg, + rlanCenterPtGeo.heightKm * 1000.0)); + fkml->writeEndElement(); // Point + fkml->writeEndElement(); // Placemark + /**********************************************************************************/ + + /**********************************************************************************/ + /* TOP */ + /**********************************************************************************/ + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", "TOP"); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#transBluePoly"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("tessellate", "0"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + QString top_coords = QString(); + for (ptIdx = 0; ptIdx <= (int)ptList.size(); ptIdx++) { + GeodeticCoord pt = ptList[ptIdx % ptList.size()]; + top_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt.longitudeDeg, + pt.latitudeDeg, + pt.heightKm * 1000.0 + + _rlanRegion->getHeightUncertainty())); + } + + fkml->writeTextElement("coordinates", top_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + /**********************************************************************************/ + + /**********************************************************************************/ + /* BOTTOM */ + /**********************************************************************************/ + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", "BOTTOM"); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#transBluePoly"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("tessellate", "0"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + QString bottom_coords = QString(); + for (ptIdx = 0; ptIdx <= (int)ptList.size(); ptIdx++) { + GeodeticCoord pt = ptList[ptIdx % ptList.size()]; + bottom_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt.longitudeDeg, + pt.latitudeDeg, + pt.heightKm * 1000.0 - + _rlanRegion->getHeightUncertainty())); + } + fkml->writeTextElement("coordinates", bottom_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + + /**********************************************************************************/ + + /**********************************************************************************/ + /* SIDES */ + /**********************************************************************************/ + for (ptIdx = 0; ptIdx < (int)ptList.size(); ptIdx++) { + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", QString::asprintf("S_%d", ptIdx)); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#transBluePoly"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("tessellate", "0"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + GeodeticCoord pt1 = ptList[ptIdx]; + GeodeticCoord pt2 = ptList[(ptIdx + 1) % ptList.size()]; + QString side_coords; + side_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt1.longitudeDeg, + pt1.latitudeDeg, + pt1.heightKm * 1000.0 - + _rlanRegion->getHeightUncertainty())); + side_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt1.longitudeDeg, + pt1.latitudeDeg, + pt1.heightKm * 1000.0 + + _rlanRegion->getHeightUncertainty())); + side_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt2.longitudeDeg, + pt2.latitudeDeg, + pt2.heightKm * 1000.0 + + _rlanRegion->getHeightUncertainty())); + side_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt2.longitudeDeg, + pt2.latitudeDeg, + pt2.heightKm * 1000.0 - + _rlanRegion->getHeightUncertainty())); + side_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt1.longitudeDeg, + pt1.latitudeDeg, + pt1.heightKm * 1000.0 - + _rlanRegion->getHeightUncertainty())); + + fkml->writeTextElement("coordinates", side_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + } + /**********************************************************************************/ + + /**********************************************************************************/ + /* Scan Points */ + /**********************************************************************************/ + fkml->writeStartElement("Folder"); + fkml->writeTextElement("name", "SCAN POINTS"); + + for (scanPtIdx = 0; scanPtIdx < (int)scanPointList.size(); scanPtIdx++) { + LatLon scanPt = scanPointList[scanPtIdx]; + + for (rlanHtIdx = 0; rlanHtIdx < numRlanHt[scanPtIdx]; ++rlanHtIdx) { + double heightAMSL = rlanCoordList[scanPtIdx][rlanHtIdx].heightKm * + 1000; + + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", + QString::asprintf("SCAN_POINT_%d_%d", + scanPtIdx, + rlanHtIdx)); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#dotStyle"); + fkml->writeStartElement("Point"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeTextElement("coordinates", + QString::asprintf("%.10f,%.10f,%.2f", + scanPt.second, + scanPt.first, + heightAMSL)); + fkml->writeEndElement(); // Point + fkml->writeEndElement(); // Placemark + } + } + + fkml->writeEndElement(); // Scan Points + /**********************************************************************************/ + + if (_rlanAntenna) { + double rlanAntennaArrowLength = 1000.0; + Vector3 centerPosn = _rlanRegion->getCenterPosn(); + + Vector3 zvec = _rlanPointing; + Vector3 xvec = (Vector3(zvec.y(), -zvec.x(), 0.0)).normalized(); + Vector3 yvec = zvec.cross(xvec); + + int numCvgPoints = 32; + + std::vector ptgPtList; + double cvgTheta = 2.0 * M_PI / 180.0; + int cvgPhiIdx; + for (cvgPhiIdx = 0; cvgPhiIdx < numCvgPoints; ++cvgPhiIdx) { + double cvgPhi = 2 * M_PI * cvgPhiIdx / numCvgPoints; + Vector3 cvgIntPosn = centerPosn + + (rlanAntennaArrowLength / 1000.0) * + (zvec * cos(cvgTheta) + + (xvec * cos(cvgPhi) + + yvec * sin(cvgPhi)) * + sin(cvgTheta)); + + GeodeticCoord cvgIntPosnGeodetic = EcefModel::ecefToGeodetic( + cvgIntPosn); + ptgPtList.push_back(cvgIntPosnGeodetic); + } + + if (true) { + fkml->writeStartElement("Folder"); + fkml->writeTextElement("name", "DIR_ANTENNA"); + + for (cvgPhiIdx = 0; cvgPhiIdx < numCvgPoints; ++cvgPhiIdx) { + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", + QString::asprintf("p%d", cvgPhiIdx)); + fkml->writeTextElement("styleUrl", "#bluePoly"); + fkml->writeTextElement("visibility", "1"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + QString more_coords = QString::asprintf( + "%.10f,%.10f,%.2f\n", + _rlanRegion->getCenterLongitude(), + _rlanRegion->getCenterLatitude(), + _rlanRegion->getCenterHeightAMSL()); + + GeodeticCoord pt = ptgPtList[cvgPhiIdx]; + more_coords.append(QString::asprintf("%.10f,%.10f,%.2f\n", + pt.longitudeDeg, + pt.latitudeDeg, + pt.heightKm * 1000.0)); + + pt = ptgPtList[(cvgPhiIdx + 1) % numCvgPoints]; + more_coords.append(QString::asprintf("%.10f,%.10f,%.2f\n", + pt.longitudeDeg, + pt.latitudeDeg, + pt.heightKm * 1000.0)); + + more_coords.append(QString::asprintf( + "%.10f,%.10f,%.2f\n", + _rlanRegion->getCenterLongitude(), + _rlanRegion->getCenterLatitude(), + _rlanRegion->getCenterHeightAMSL())); + + fkml->writeTextElement("coordinates", more_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + } + fkml->writeEndElement(); // Beamcone + } + } + + std::vector bdyPtList = _rlanRegion->getBoundaryPolygon( + _terrainDataModel); + + if (bdyPtList.size()) { + /**********************************************************************************/ + /* TOP BOUNDARY */ + /**********************************************************************************/ + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", "TOP BOUNDARY"); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#transGrayPoly"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("tessellate", "0"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + QString top_bdy_coords = QString(); + for (ptIdx = 0; ptIdx <= (int)bdyPtList.size(); ptIdx++) { + GeodeticCoord pt = bdyPtList[ptIdx % bdyPtList.size()]; + top_bdy_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt.longitudeDeg, + pt.latitudeDeg, + _rlanRegion->getMaxHeightAMSL())); + } + + fkml->writeTextElement("coordinates", top_bdy_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + /**********************************************************************************/ + + /**********************************************************************************/ + /* BOTTOM BOUNDARY */ + /**********************************************************************************/ + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", "BOTTOM BOUNDARY"); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#transGrayPoly"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("tessellate", "0"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + QString bottom_bdy_coords = QString(); + for (ptIdx = 0; ptIdx <= (int)bdyPtList.size(); ptIdx++) { + GeodeticCoord pt = bdyPtList[ptIdx % bdyPtList.size()]; + bottom_bdy_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt.longitudeDeg, + pt.latitudeDeg, + _rlanRegion->getMinHeightAMSL())); + } + + fkml->writeTextElement("coordinates", bottom_bdy_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + /**********************************************************************************/ + } + + fkml->writeEndElement(); // Folder + + fkml->writeStartElement("Folder"); + fkml->writeTextElement("name", "Denied Region"); + int drIdx; + for (drIdx = 0; drIdx < (int)_deniedRegionList.size(); ++drIdx) { + DeniedRegionClass *dr = _deniedRegionList[drIdx]; + DeniedRegionClass::TypeEnum drType = dr->getType(); + std::string pfx; + switch (drType) { + case DeniedRegionClass::RASType: + pfx = "RAS_"; + break; + case DeniedRegionClass::userSpecifiedType: + pfx = "USER_SPEC_"; + break; + default: + CORE_DUMP; + break; + } + + fkml->writeStartElement("Folder"); + fkml->writeTextElement("name", + QString::fromStdString(pfx) + + QString::number(dr->getID())); + + int numPtsCircle = 32; + int rectIdx, numRect; + double rectLonStart, rectLonStop, rectLatStart, rectLatStop; + double circleRadius, longitudeCenter, latitudeCenter; + double drTerrainHeight, drBldgHeight, drHeightAGL; + Vector3 drCenterPosn; + Vector3 drUpVec; + Vector3 drEastVec; + Vector3 drNorthVec; + QString dr_coords; + MultibandRasterClass::HeightResult drLidarHeightResult; + CConst::HeightSourceEnum drHeightSource; + DeniedRegionClass::GeometryEnum drGeometry = dr->getGeometry(); + switch (drGeometry) { + case DeniedRegionClass::rectGeometry: + case DeniedRegionClass::rect2Geometry: + numRect = ((RectDeniedRegionClass *)dr)->getNumRect(); + for (rectIdx = 0; rectIdx < numRect; rectIdx++) { + std::tie(rectLonStart, + rectLonStop, + rectLatStart, + rectLatStop) = + ((RectDeniedRegionClass *)dr) + ->getRect(rectIdx); + + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", + QString("RECT_") + + QString::number( + rectIdx)); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", + "#transBluePoly"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("tessellate", "0"); + fkml->writeTextElement("altitudeMode", + "clampToGround"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + dr_coords = QString(); + dr_coords.append(QString::asprintf("%.10f,%.10f,%." + "2f\n", + rectLonStart, + rectLatStart, + 0.0)); + dr_coords.append(QString::asprintf("%.10f,%.10f,%." + "2f\n", + rectLonStop, + rectLatStart, + 0.0)); + dr_coords.append(QString::asprintf("%.10f,%.10f,%." + "2f\n", + rectLonStop, + rectLatStop, + 0.0)); + dr_coords.append(QString::asprintf("%.10f,%.10f,%." + "2f\n", + rectLonStart, + rectLatStop, + 0.0)); + dr_coords.append(QString::asprintf("%.10f,%.10f,%." + "2f\n", + rectLonStart, + rectLatStart, + 0.0)); + + fkml->writeTextElement("coordinates", dr_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + } + break; + case DeniedRegionClass::circleGeometry: + case DeniedRegionClass::horizonDistGeometry: + circleRadius = + ((CircleDeniedRegionClass *)dr) + ->computeRadius( + _rlanRegion->getMaxHeightAGL()); + longitudeCenter = ((CircleDeniedRegionClass *)dr) + ->getLongitudeCenter(); + latitudeCenter = ((CircleDeniedRegionClass *)dr) + ->getLatitudeCenter(); + drHeightAGL = dr->getHeightAGL(); + _terrainDataModel->getTerrainHeight(longitudeCenter, + latitudeCenter, + drTerrainHeight, + drBldgHeight, + drLidarHeightResult, + drHeightSource); + + drCenterPosn = EcefModel::geodeticToEcef( + latitudeCenter, + longitudeCenter, + (drTerrainHeight + drHeightAGL) / 1000.0); + drUpVec = drCenterPosn.normalized(); + drEastVec = (Vector3(-drUpVec.y(), drUpVec.x(), 0.0)) + .normalized(); + drNorthVec = drUpVec.cross(drEastVec); + + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", QString("CIRCLE")); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#transBluePoly"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("tessellate", "0"); + fkml->writeTextElement("altitudeMode", "clampToGround"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + dr_coords = QString(); + for (ptIdx = 0; ptIdx <= numPtsCircle; ++ptIdx) { + double phi = 2 * M_PI * ptIdx / numPtsCircle; + Vector3 circlePtPosn = drCenterPosn + + (circleRadius / 1000) * + (drEastVec * + cos(phi) + + drNorthVec * + sin(phi)); + + GeodeticCoord circlePtPosnGeodetic = + EcefModel::ecefToGeodetic(circlePtPosn); + + dr_coords.append(QString::asprintf( + "%.10f,%.10f,%.2f\n", + circlePtPosnGeodetic.longitudeDeg, + circlePtPosnGeodetic.latitudeDeg, + 0.0)); + } + + fkml->writeTextElement("coordinates", dr_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + + break; + default: + CORE_DUMP; + break; + } + fkml->writeEndElement(); // Folder + } + fkml->writeEndElement(); // Folder + } + /**************************************************************************************/ + +#if DEBUG_AFC + /**************************************************************************************/ + /* Create pathTraceFile, for debugging */ + /**************************************************************************************/ + TraceGzipCsv pathTraceGc(pathTraceFile); + /**************************************************************************************/ +#endif + + /**************************************************************************************/ + /* Compute Channel Availability */ + /**************************************************************************************/ + const double exclusionDistKmSquared = (_exclusionDist / 1000.0) * (_exclusionDist / 1000.0); + const double maxRadiusKmSquared = (_maxRadius / 1000.0) * (_maxRadius / 1000.0); + + if ((_rlanRegion->getMaxHeightAGL() < _minRlanHeightAboveTerrain) && + (_reportErrorRlanHeightLowFlag)) { + LOGGER_WARN(logger) + << std::string("ERROR: Point Analysis: Invalid RLAN parameter settings.") + << std::endl + << std::string("RLAN uncertainty region has a max AGL height of ") + << _rlanRegion->getMaxHeightAGL() << std::string(", which is < ") + << _minRlanHeightAboveTerrain << std::endl; + _responseCode = CConst::invalidValueResponseCode; + _invalidParams << "heightType" + << "height" + << "verticalUncertainty"; + return; + } + +#if DEBUG_AFC + char *tstr; + + time_t tStartDR = time(NULL); + tstr = strdup(ctime(&tStartDR)); + strtok(tstr, "\n"); + + std::cout << "Begin Processing Denied Regions " << tstr << std::endl << std::flush; + + free(tstr); +#endif + + int drIdx; + for (drIdx = 0; drIdx < (int)_deniedRegionList.size(); ++drIdx) { + DeniedRegionClass *dr = _deniedRegionList[drIdx]; + bool foundScanPointInDR = false; + for (scanPtIdx = 0; + (scanPtIdx < (int)scanPointList.size()) && (!foundScanPointInDR); + scanPtIdx++) { + if (numRlanHt[scanPtIdx]) { + rlanHtIdx = 0; + + GeodeticCoord rlanCoord = rlanCoordList[scanPtIdx][rlanHtIdx]; + double rlanHeightAGL = (rlanCoord.heightKm * 1000) - + rlanTerrainHeight[scanPtIdx]; + + if (dr->intersect(rlanCoord.longitudeDeg, + rlanCoord.latitudeDeg, + 0.0, + rlanHeightAGL)) { + int chanIdx; + for (chanIdx = 0; chanIdx < (int)_channelList.size(); + ++chanIdx) { + ChannelStruct *channel = &(_channelList[chanIdx]); + for (int freqSegIdx = 0; + freqSegIdx < channel->segList.size(); + ++freqSegIdx) { + if (std::get<2>( + channel->segList[freqSegIdx]) != + BLACK) { + double chanStartFreq = + channel->freqMHzList + [freqSegIdx] * + 1.0e6; + double chanStopFreq = + channel->freqMHzList + [freqSegIdx + 1] * + 1.0e6; + bool hasOverlap = computeSpectralOverlapLoss( + (double *)NULL, + chanStartFreq, + chanStopFreq, + dr->getStartFreq(), + dr->getStopFreq(), + false, + CConst::psdSpectralAlgorithm); + if (hasOverlap) { + channel->segList + [freqSegIdx] = std::make_tuple( + -std::numeric_limits< + double>:: + infinity(), + -std::numeric_limits< + double>:: + infinity(), + BLACK); + } + } + } + } + foundScanPointInDR = true; + } + } + } + } + +#if DEBUG_AFC + time_t tEndDR = time(NULL); + tstr = strdup(ctime(&tEndDR)); + strtok(tstr, "\n"); + + int elapsedTimeDR = (int)(tEndDR - tStartDR); + + int etDR = elapsedTimeDR; + int elapsedTimeDRSec = etDR % 60; + etDR = etDR / 60; + int elapsedTimeDRMin = etDR % 60; + etDR = etDR / 60; + int elapsedTimeDRHour = etDR % 24; + etDR = etDR / 24; + int elapsedTimeDRDay = etDR; + + std::cout << "End Processing Denied Regions " << tstr + << " Elapsed time = " << (tEndDR - tStartDR) << " sec = " << elapsedTimeDRDay + << " days " << elapsedTimeDRHour << " hours " << elapsedTimeDRMin << " min " + << elapsedTimeDRSec << " sec." << std::endl + << std::flush; + + free(tstr); + +#endif + +#if DEBUG_AFC + time_t tStartULS = time(NULL); + tstr = strdup(ctime(&tStartULS)); + strtok(tstr, "\n"); + + std::cout << "Begin Processing ULS RX's " << tstr << std::endl << std::flush; + + free(tstr); + + int traceIdx = 0; +#endif + + /**************************************************************************************/ + /* Create _fsAnalysisListFile */ + /**************************************************************************************/ + FILE *fFSList; + if (_fsAnalysisListFile.empty()) { + fFSList = (FILE *)NULL; + } else { + if (!(fFSList = fopen(_fsAnalysisListFile.c_str(), "wb"))) { + errStr << std::string("ERROR: Unable to open fsAnalysisListFile \"") + + _fsAnalysisListFile + std::string("\"\n"); + throw std::runtime_error(errStr.str()); + } + } + if (fFSList) { + fprintf(fFSList, + "FSID" + ",RX_CALLSIGN" + ",TX_CALLSIGN" + ",NUM_PASSIVE_REPEATER" + ",IS_DIVERSITY_LINK" + ",SEGMENT_IDX" + ",START_FREQ (MHz)" + ",STOP_FREQ (MHz)" + ",SEGMENT_RX_LONGITUDE (deg)" + ",SEGMENT_RX_LATITUDE (deg)" + ",SEGMENT_RX_HEIGHT_AGL (m)" + ",SEGMENT_DISTANCE (m)" + ",PR_REF_THETA_IN (deg)" + ",PR_REF_KS" + ",PR_REF_Q" + ",PR_PATH_SEGMENT_GAIN (dB)" + ",PR_EFFECTIVE_GAIN (dB)" + "\n"); + } + /**************************************************************************************/ + + int ulsIdx; + double *eirpLimitList = (double *)malloc(sortedUlsList.size() * sizeof(double)); + bool *ulsFlagList = (bool *)malloc(sortedUlsList.size() * sizeof(bool)); + for (ulsIdx = 0; ulsIdx < (int)sortedUlsList.size(); ++ulsIdx) { + eirpLimitList[ulsIdx] = _maxEIRP_dBm; + ulsFlagList[ulsIdx] = false; + } + + int totNumProc = (int)sortedUlsList.size(); + + int numPct = 100; + + if (numPct > totNumProc) { + numPct = totNumProc; + } + + bool cont = true; + int numProc = 0; + for (ulsIdx = 0; (ulsIdx < (int)sortedUlsList.size()) && (cont); ++ulsIdx) { + LOGGER_DEBUG(logger) + << "considering ULSIdx: " << ulsIdx << '/' << sortedUlsList.size(); + ULSClass *uls = sortedUlsList[ulsIdx]; + +#if 0 + // For debugging, identifies anomalous ULS entries + if (uls->getLinkDistance() == -1) { + std::cout << uls->getID() << std::endl; + } +#endif + if (true) { +#if DEBUG_AFC + bool traceFlag = false; + if (traceIdx < (int)fsidTraceList.size()) { + if (uls->getID() == fsidTraceList[traceIdx]) { + traceFlag = true; + } + } +#endif + + int numPR = uls->getNumPR(); + int numDiversity = (uls->getHasDiversity() ? 2 : 1); + + int segStart = (_passiveRepeaterFlag ? 0 : numPR); + + for (int segIdx = segStart; segIdx < numPR + 1; ++segIdx) { + for (int divIdx = 0; divIdx < numDiversity; ++divIdx) { + Vector3 ulsRxPos = + (segIdx == numPR ? + (divIdx == 0 ? + uls->getRxPosition() : + uls->getDiversityPosition()) : + uls->getPR(segIdx).positionRx); + double ulsRxLongitude = + (segIdx == numPR ? uls->getRxLongitudeDeg() : + uls->getPR(segIdx).longitudeDeg); + double ulsRxLatitude = + (segIdx == numPR ? uls->getRxLatitudeDeg() : + uls->getPR(segIdx).latitudeDeg); + + Vector3 lineOfSightVectorKm = ulsRxPos - + _rlanRegion->getCenterPosn(); + double distKmSquared = + (lineOfSightVectorKm).dot(lineOfSightVectorKm); + + if (distKmSquared < maxRadiusKmSquared) { +#if DEBUG_AFC + time_t t1 = time(NULL); +#endif + + ulsFlagList[ulsIdx] = true; + + double ulsRxHeightAGL = + (segIdx == numPR ? + (divIdx == 0 ? + uls->getRxHeightAboveTerrain() : + uls->getDiversityHeightAboveTerrain()) : + uls->getPR(segIdx) + .heightAboveTerrainRx); + double ulsRxHeightAMSL = + (segIdx == numPR ? + (divIdx == 0 ? + uls->getRxHeightAMSL() : + uls->getDiversityHeightAMSL()) : + uls->getPR(segIdx).heightAMSLRx); + double ulsSegmentDistance = + (segIdx == numPR ? + uls->getLinkDistance() : + uls->getPR(segIdx) + .segmentDistance); + + /**************************************************************************************/ + /* Determine propagation environment of FS segment + * RX, if needed. */ + /**************************************************************************************/ + char ulsRxPropEnv = ' '; + CConst::NLCDLandCatEnum nlcdLandCatRx; + CConst::PropEnvEnum fsPropEnv; + if ((_applyClutterFSRxFlag) && + (ulsRxHeightAGL <= _maxFsAglHeight)) { + fsPropEnv = computePropEnv(ulsRxLongitude, + ulsRxLatitude, + nlcdLandCatRx); + switch (fsPropEnv) { + case CConst::urbanPropEnv: + ulsRxPropEnv = 'U'; + break; + case CConst::suburbanPropEnv: + ulsRxPropEnv = 'S'; + break; + case CConst::ruralPropEnv: + ulsRxPropEnv = 'R'; + break; + case CConst::barrenPropEnv: + ulsRxPropEnv = 'B'; + break; + case CConst::unknownPropEnv: + ulsRxPropEnv = 'X'; + break; + default: + CORE_DUMP; + } + } else { + fsPropEnv = CConst::unknownPropEnv; + ulsRxPropEnv = ' '; + } + /**************************************************************************************/ + + LatLon ulsRxLatLon = + std::pair(ulsRxLatitude, + ulsRxLongitude); + bool contains2D, contains3D; + + // If contains2D is set, FS lon/lat is inside + // 2D-uncertainty region, depending on height may be + // above, below, or actually inside uncertainty + // region. If contains3D is set, FS is inside + // 3D-uncertainty + LatLon closestLatLon = + _rlanRegion->closestPoint(ulsRxLatLon, + contains2D); + if ((!contains2D) && + (fabs(closestLatLon.first - ulsRxLatLon.first) < + 2.0 / _scanres_points_per_degree) && + (fabs(closestLatLon.second - + ulsRxLatLon.second) < + 2.0 / _scanres_points_per_degree)) { + double adjFSRxLongitude = + (std::floor( + ulsRxLongitude * + _scanres_points_per_degree) + + 0.5) / + _scanres_points_per_degree; + double adjFSRxLatitude = + (std::floor( + ulsRxLatitude * + _scanres_points_per_degree) + + 0.5) / + _scanres_points_per_degree; + + for (scanPtIdx = 0; + (scanPtIdx < + (int)scanPointList.size()) && + (!contains2D); + scanPtIdx++) { + LatLon scanPt = + scanPointList[scanPtIdx]; + if ((fabs(scanPt.second - + adjFSRxLongitude) < + 1.0e-10) && + (fabs(scanPt.first - + adjFSRxLatitude) < + 1.0e-10)) { + contains2D = true; + } + } + } + + contains3D = false; + if (contains2D) { + if ((ulsRxHeightAMSL >= + _rlanRegion->getMinHeightAMSL()) && + (ulsRxHeightAMSL <= + _rlanRegion->getMaxHeightAMSL())) { + contains3D = true; + LOGGER_INFO(logger) + << "FSID = " << uls->getID() + << (divIdx ? " DIVERSITY " + "LINK" : + "") + << (segIdx == numPR ? + " RX" : + " PR " + + std::to_string( + segIdx + + 1)) + << " inside uncertainty " + "volume"; + } + if (!contains3D) { + LOGGER_INFO(logger) + << "FSID = " << uls->getID() + << (divIdx ? " DIVERSITY " + "LINK" : + "") + << (segIdx == numPR ? + " RX" : + " PR " + + std::to_string( + segIdx + + 1)) + << " inside uncertainty " + "footprint, not in " + "volume"; + } + } + + double minRLANDist = -1.0; + if (distKmSquared <= exclusionDistKmSquared) { + LOGGER_INFO(logger) + << "FSID = " << uls->getID() + << (segIdx == numPR ? + " RX" : + " PR " + + std::to_string( + segIdx)) + << " is inside exclusion distance."; + minRLANDist = sqrt(distKmSquared) * 1000.0; + } else if ((contains3D) && + (!_allowScanPtsInUncRegFlag)) { + int chanIdx; + for (chanIdx = 0; + chanIdx < (int)_channelList.size(); + ++chanIdx) { + ChannelStruct *channel = &( + _channelList[chanIdx]); + ChannelType channelType = + channel->type; + bool useACI = + (channelType == INQUIRED_FREQUENCY ? + false : + _aciFlag); + for (int freqSegIdx = 0; + freqSegIdx < + channel->segList.size(); + ++freqSegIdx) { + ChannelColor chanColor = std::get< + 2>( + channel->segList + [freqSegIdx]); + if ((chanColor != BLACK)) { + double chanStartFreq = + channel->freqMHzList + [freqSegIdx] * + 1.0e6; + double chanStopFreq = + channel->freqMHzList + [freqSegIdx + + 1] * + 1.0e6; + bool hasOverlap = computeSpectralOverlapLoss( + (double *) + NULL, + chanStartFreq, + chanStopFreq, + uls->getStartFreq(), + uls->getStopFreq(), + useACI, + CConst::psdSpectralAlgorithm); + + if (hasOverlap) { + double eirpLimit_dBm; + if (std::isnan( + _reportUnavailPSDdBmPerMHz)) { + eirpLimit_dBm = + -std::numeric_limits< + double>:: + infinity(); + std::get< + 2>( + channel->segList + [freqSegIdx]) = + BLACK; + } else { + double bwMHz = + (double)channel + ->bandwidth( + freqSegIdx); + eirpLimit_dBm = + _reportUnavailPSDdBmPerMHz + + 10 * log10(bwMHz); + } + std::get<0>( + channel->segList + [freqSegIdx]) = + eirpLimit_dBm; + std::get<1>( + channel->segList + [freqSegIdx]) = + eirpLimit_dBm; + + if ((eirpLimit_dBm < + eirpLimitList + [ulsIdx])) { + eirpLimitList + [ulsIdx] = + eirpLimit_dBm; + } + } + } + } + } + minRLANDist = 0.0; + } else { + Vector3 ulsAntennaPointing = + (segIdx == numPR ? + (divIdx == 0 ? + uls->getAntennaPointing() : + uls->getDiversityAntennaPointing()) : + uls->getPR(segIdx) + .pointing); + + double minAngleOffBoresightDeg = 0.0; + if (contains2D) { + double minAOBLon, minAOBLat, + minAOBHeghtAMSL; + minAngleOffBoresightDeg = + _rlanRegion->calcMinAOB( + ulsRxLatLon, + ulsAntennaPointing, + ulsRxHeightAMSL, + minAOBLon, + minAOBLat, + minAOBHeghtAMSL); + LOGGER_INFO(logger) + << std::setprecision(15) + << "FSID = " << uls->getID() + << " MIN_AOB = " + << minAngleOffBoresightDeg + << " at LON = " << minAOBLon + << " LAT = " << minAOBLat + << " HEIGHT_AMSL = " + << minAOBHeghtAMSL; + } + + for (scanPtIdx = 0; + scanPtIdx < (int)scanPointList.size(); + scanPtIdx++) { + LatLon scanPt = + scanPointList[scanPtIdx]; + + // Use Haversine formula with + // average earth radius of 6371 km + double groundDistanceKm; + { + double lon1Rad = + scanPt.second * + M_PI / 180.0; + double lat1Rad = + scanPt.first * + M_PI / 180.0; + double lon2Rad = + ulsRxLongitude * + M_PI / 180.0; + double lat2Rad = + ulsRxLatitude * + M_PI / 180.0; + double slat = sin( + (lat2Rad - + lat1Rad) / + 2); + double slon = sin( + (lon2Rad - + lon1Rad) / + 2); + groundDistanceKm = + 2 * + CConst::averageEarthRadius * + asin(sqrt( + slat * slat + + cos(lat1Rad) * + cos(lat2Rad) * + slon * + slon)) * + 1.0e-3; + } + + for (rlanHtIdx = 0; + rlanHtIdx < + numRlanHt[scanPtIdx]; + ++rlanHtIdx) { + Vector3 rlanPosn = + rlanPosnList + [scanPtIdx] + [rlanHtIdx]; + GeodeticCoord rlanCoord = + rlanCoordList + [scanPtIdx] + [rlanHtIdx]; + lineOfSightVectorKm = + ulsRxPos - rlanPosn; + double distKm = + lineOfSightVectorKm + .len(); + double win2DistKm; + if (_winner2UseGroundDistanceFlag) { + win2DistKm = + groundDistanceKm; + } else { + win2DistKm = distKm; + } + double fsplDistKm; + if (_fsplUseGroundDistanceFlag) { + fsplDistKm = + groundDistanceKm; + } else { + fsplDistKm = distKm; + } + double dAP = rlanPosn.len(); + double duls = + ulsRxPos.len(); + double elevationAngleTxDeg = + 90.0 - + acos(rlanPosn.dot( + lineOfSightVectorKm) / + (dAP * + distKm)) * + 180.0 / + M_PI; + double elevationAngleRxDeg = + 90.0 - + acos(ulsRxPos.dot( + -lineOfSightVectorKm) / + (duls * + distKm)) * + 180.0 / + M_PI; + + double rlanAngleOffBoresightRad; + double rlanDiscriminationGainDB; + if (_rlanAntenna) { + double cosAOB = + _rlanPointing + .dot(lineOfSightVectorKm) / + distKm; + if (cosAOB > 1.0) { + cosAOB = + 1.0; + } else if (cosAOB < + -1.0) { + cosAOB = + -1.0; + } + rlanAngleOffBoresightRad = + acos(cosAOB); + rlanDiscriminationGainDB = + _rlanAntenna->gainDB( + rlanAngleOffBoresightRad); + } else { + rlanAngleOffBoresightRad = + 0.0; + rlanDiscriminationGainDB = + 0.0; + } + double rlanHtAboveTerrain = + rlanCoord.heightKm * + 1000.0 - + rlanTerrainHeight + [scanPtIdx]; + + if ((minRLANDist == -1.0) || + (distKm * 1000.0 < + minRLANDist)) { + minRLANDist = + distKm * + 1000.0; + } + + int chanIdx; + std::vector itmSegList; + std::vector + RxPowerDBW_0PLList + [2]; + std::vector< + ExcThrParamClass> + excThrParamList[2]; + for (chanIdx = 0; + chanIdx < + (int)_channelList + .size(); + ++chanIdx) { + ChannelStruct *channel = + &(_channelList + [chanIdx]); + ChannelType channelType = + channel->type; + itmSegList.clear(); + RxPowerDBW_0PLList[0] + .clear(); + RxPowerDBW_0PLList[1] + .clear(); + excThrParamList[0] + .clear(); + excThrParamList[1] + .clear(); + // First set state = + // 0. When state = + // 0, FSPL is used + // in place of ITM. + // In state = 0, go + // through all freq + // segments and if + // state=0 does not + // limit EIRP those + // segments are + // done. Identity + // list of segments + // for which state = + // 0 (FSPL in place + // of ITM) does + // limit EIRP and + // full Path loss + // calculation is + // required. Next + // set state = 1. + // When state = 1, + // the full path + // loss model is + // used. For the + // range of + // frequencies where + // Path loss + // calculation is + // required, + // calculate path + // loss at min and + // max frequencies, + // then use linear + // interpoplation + // for intermediate + // frequencies. + bool contFlag = + true; + int state = 0; + int freqSegIdx = 0; + int itmSegIdx = -1; + double itmStartPathLoss = + quietNaN; + double itmStopPathLoss = + quietNaN; + double itmStartFreqMHz = + quietNaN; + double itmStopFreqMHz = + quietNaN; + + while (contFlag) { + if (state == + 1) { + freqSegIdx = itmSegList + [itmSegIdx]; + } + + ChannelColor chanColor = std::get< + 2>( + channel->segList + [freqSegIdx]); + if ((chanColor != + BLACK) && + (chanColor != + RED)) { + int chanStartFreqMHz = + channel->freqMHzList + [freqSegIdx]; + int chanStopFreqMHz = + channel->freqMHzList + [freqSegIdx + + 1]; + + bool useACI = + (channelType == INQUIRED_FREQUENCY ? + false : + _aciFlag); + CConst::SpectralAlgorithmEnum spectralAlgorithm = + (channelType == INQUIRED_FREQUENCY ? + CConst::psdSpectralAlgorithm : + _channelResponseAlgorithm); + // LOGGER_INFO(logger) << "COMPUTING SPECTRAL OVERLAP FOR FSID = " << uls->getID(); + double spectralOverlapLossDB; + bool hasOverlap = computeSpectralOverlapLoss( + &spectralOverlapLossDB, + chanStartFreqMHz * + 1.0e6, + chanStopFreqMHz * + 1.0e6, + uls->getStartFreq(), + uls->getStopFreq(), + useACI, + spectralAlgorithm); + if (hasOverlap) { + double rxPowerDBW_0PL + [2]; + ExcThrParamClass excThrParam + [2]; + double eirpLimit_dBm + [2]; + double bandwidthMHz = + (double)channel + ->bandwidth( + freqSegIdx); + int numBandEdge = + (channelType == INQUIRED_FREQUENCY ? + 2 : + 1); + + double maxEIRPdBm; + if (channelType == + INQUIRED_FREQUENCY) { + maxEIRPdBm = + _inquiredFrequencyMaxPSD_dBmPerMHz + + 10.0 * log10(bandwidthMHz); + } else { + maxEIRPdBm = + _maxEIRP_dBm; + } + + for (int bandEdgeIdx = + 0; + bandEdgeIdx < + numBandEdge; + ++bandEdgeIdx) { + double evalFreqMHz; + if (channelType == + INQUIRED_FREQUENCY) { + evalFreqMHz = + (bandEdgeIdx == 0 ? + chanStartFreqMHz : + chanStopFreqMHz); + } else { + evalFreqMHz = + (chanStartFreqMHz + + chanStopFreqMHz) / + 2.0; + } + double evalFreqHz = + evalFreqMHz * + 1.0e6; + + if ((state == + 0) && + (fsplDistKm == + 0)) { + // Possible if FSPL distance is horizontal. This should be extremely rare + continue; + } + + double pathLoss; + double rxPowerDBW; + double I2NDB; + double marginDB; + + if (state == + 0) { + std::string + buildingPenetrationModelStr; + double buildingPenetrationCDF; + double buildingPenetrationDB; + + std::string + txClutterStr; + std::string + rxClutterStr; + std::string + pathLossModelStr; + double pathLossCDF; + std::string + pathClutterTxModelStr; + double pathClutterTxCDF; + double pathClutterTxDB; + std::string + pathClutterRxModelStr; + double pathClutterRxCDF; + double pathClutterRxDB; + double rxGainDB; + double discriminationGain; + std::string + rxAntennaSubModelStr; + double angleOffBoresightDeg; + double nearFieldOffsetDB; + double nearField_xdb; + double nearField_u; + double nearField_eff; + double reflectorD0; + double reflectorD1; + + computePathLoss( + contains2D ? + CConst::FSPLPathLossModel : + _pathLossModel, + true, + rlanPropEnv + [scanPtIdx], + fsPropEnv, + rlanNlcdLandCat + [scanPtIdx], + nlcdLandCatRx, + distKm, + fsplDistKm, + win2DistKm, + evalFreqHz, + rlanCoord + .longitudeDeg, + rlanCoord + .latitudeDeg, + rlanHtAboveTerrain, + elevationAngleTxDeg, + ulsRxLongitude, + ulsRxLatitude, + ulsRxHeightAGL, + elevationAngleRxDeg, + pathLoss, + pathClutterTxDB, + pathClutterRxDB, + pathLossModelStr, + pathLossCDF, + pathClutterTxModelStr, + pathClutterTxCDF, + pathClutterRxModelStr, + pathClutterRxCDF, + &txClutterStr, + &rxClutterStr, + &(uls->ITMHeightProfile), + &(uls->isLOSHeightProfile), + &(uls->isLOSSurfaceFrac) +#if DEBUG_AFC + , + uls->ITMHeightType +#endif + ); + buildingPenetrationDB = computeBuildingPenetration( + _buildingType, + elevationAngleTxDeg, + evalFreqHz, + buildingPenetrationModelStr, + buildingPenetrationCDF); + + if (contains2D) { + angleOffBoresightDeg = + minAngleOffBoresightDeg; + } else { + angleOffBoresightDeg = + acos(ulsAntennaPointing + .dot(-(lineOfSightVectorKm + .normalized()))) * + 180.0 / + M_PI; + } + if (segIdx == + numPR) { + rxGainDB = uls->computeRxGain( + angleOffBoresightDeg, + elevationAngleRxDeg, + evalFreqHz, + rxAntennaSubModelStr, + divIdx); + } else { + discriminationGain = + uls->getPR(segIdx) + .computeDiscriminationGain( + angleOffBoresightDeg, + elevationAngleRxDeg, + evalFreqHz, + reflectorD0, + reflectorD1); + rxGainDB = + uls->getPR(segIdx) + .effectiveGain + + discriminationGain; + } + + nearFieldOffsetDB = + 0.0; + nearField_xdb = + quietNaN; + nearField_u = + quietNaN; + nearField_eff = + quietNaN; + if (segIdx == + numPR) { + if (_nearFieldAdjFlag && + (distKm * + 1000.0 < + uls->getRxNearFieldDistLimit()) && + (angleOffBoresightDeg < + 90.0)) { + bool unii5Flag = computeSpectralOverlapLoss( + (double *) + NULL, + uls->getStartFreq(), + uls->getStopFreq(), + 5925.0e6, + 6425.0e6, + false, + CConst::psdSpectralAlgorithm); + double Fc; + if (unii5Flag) { + Fc = 6175.0e6; + } else { + Fc = 6700.0e6; + } + nearField_eff = + uls->getRxNearFieldAntEfficiency(); + double D = + uls->getRxNearFieldAntDiameter(); + + nearField_xdb = + 10.0 * + log10(CConst::c * + distKm * + 1000.0 / + (2 * + Fc * + D * + D)); + nearField_u = + (Fc * + D * + sin(angleOffBoresightDeg * + M_PI / + 180.0) / + CConst::c); + + nearFieldOffsetDB = _nfa->computeNFA( + nearField_xdb, + nearField_u, + nearField_eff); + } + } + + rxPowerDBW_0PL[bandEdgeIdx] = + (maxEIRPdBm - + 30.0) + + rlanDiscriminationGainDB - + _bodyLossDB - + buildingPenetrationDB - + pathClutterTxDB - + pathClutterRxDB + + rxGainDB + + nearFieldOffsetDB - + spectralOverlapLossDB - + _polarizationLossDB - + uls->getRxAntennaFeederLossDB(); + if (excthrGc) { + excThrParam[bandEdgeIdx] + .rlanDiscriminationGainDB = + rlanDiscriminationGainDB; + excThrParam[bandEdgeIdx] + .bodyLossDB = + _bodyLossDB; + excThrParam[bandEdgeIdx] + .buildingPenetrationModelStr = + buildingPenetrationModelStr; + excThrParam[bandEdgeIdx] + .buildingPenetrationCDF = + buildingPenetrationCDF; + excThrParam[bandEdgeIdx] + .buildingPenetrationDB = + buildingPenetrationDB; + excThrParam[bandEdgeIdx] + .angleOffBoresightDeg = + angleOffBoresightDeg; + excThrParam[bandEdgeIdx] + .pathLossModelStr = + pathLossModelStr; + excThrParam[bandEdgeIdx] + .pathLossCDF = + pathLossCDF; + excThrParam[bandEdgeIdx] + .pathClutterTxModelStr = + pathClutterTxModelStr; + excThrParam[bandEdgeIdx] + .pathClutterTxCDF = + pathClutterTxCDF; + excThrParam[bandEdgeIdx] + .pathClutterTxDB = + pathClutterTxDB; + excThrParam[bandEdgeIdx] + .txClutterStr = + txClutterStr; + excThrParam[bandEdgeIdx] + .pathClutterRxModelStr = + pathClutterRxModelStr; + excThrParam[bandEdgeIdx] + .pathClutterRxCDF = + pathClutterRxCDF; + excThrParam[bandEdgeIdx] + .pathClutterRxDB = + pathClutterRxDB; + excThrParam[bandEdgeIdx] + .rxClutterStr = + rxClutterStr; + excThrParam[bandEdgeIdx] + .rxGainDB = + rxGainDB; + excThrParam[bandEdgeIdx] + .discriminationGain = + discriminationGain; + excThrParam[bandEdgeIdx] + .rxAntennaSubModelStr = + rxAntennaSubModelStr; + excThrParam[bandEdgeIdx] + .nearFieldOffsetDB = + nearFieldOffsetDB; + excThrParam[bandEdgeIdx] + .spectralOverlapLossDB = + spectralOverlapLossDB; + excThrParam[bandEdgeIdx] + .polarizationLossDB = + _polarizationLossDB; + excThrParam[bandEdgeIdx] + .rxAntennaFeederLossDB = + uls->getRxAntennaFeederLossDB(); + excThrParam[bandEdgeIdx] + .nearField_xdb = + nearField_xdb; + excThrParam[bandEdgeIdx] + .nearField_u = + nearField_u; + excThrParam[bandEdgeIdx] + .nearField_eff = + nearField_eff; + excThrParam[bandEdgeIdx] + .reflectorD0 = + reflectorD0; + excThrParam[bandEdgeIdx] + .reflectorD1 = + reflectorD1; + } + } else { + pathLoss = + (itmStartPathLoss * + (itmStopFreqMHz - + evalFreqMHz) + + itmStopPathLoss * + (evalFreqMHz - + itmStartFreqMHz)) / + (itmStopFreqMHz - + itmStartFreqMHz); + rxPowerDBW_0PL[bandEdgeIdx] = RxPowerDBW_0PLList + [bandEdgeIdx] + [itmSegIdx]; + if (excthrGc) { + excThrParam[bandEdgeIdx] = excThrParamList + [bandEdgeIdx] + [itmSegIdx]; + } + } + + rxPowerDBW = + rxPowerDBW_0PL + [bandEdgeIdx] - + pathLoss; + + I2NDB = rxPowerDBW - + uls->getNoiseLevelDBW(); + + marginDB = + _IoverN_threshold_dB - + I2NDB; + + eirpLimit_dBm[bandEdgeIdx] = + maxEIRPdBm + + marginDB; + + if (eirpGc) { + eirpGc.callsign = + uls->getCallsign(); + eirpGc.pathNum = + uls->getPathNumber(); + eirpGc.ulsId = + uls->getID(); + eirpGc.segIdx = + segIdx; + eirpGc.divIdx = + divIdx; + eirpGc.scanLat = + rlanCoord + .latitudeDeg; + eirpGc.scanLon = + rlanCoord + .longitudeDeg; + eirpGc.scanAgl = + rlanHtAboveTerrain; + eirpGc.scanAmsl = + rlanCoord + .heightKm * + 1000.0; + eirpGc.scanPtIdx = + scanPtIdx; + eirpGc.distKm = + distKm; + eirpGc.elevationAngleTx = + elevationAngleTxDeg; + eirpGc.channel = + channel->index; + eirpGc.chanStartMhz = + channel->freqMHzList + [freqSegIdx]; + eirpGc.chanEndMhz = + channel->freqMHzList + [freqSegIdx + + 1]; + eirpGc.chanBwMhz = + bandwidthMHz; + eirpGc.chanType = + channelType; + eirpGc.eirpLimit = eirpLimit_dBm + [bandEdgeIdx]; + eirpGc.fspl = + (state == + 0); + eirpGc.pathLossDb = + pathLoss; + eirpGc.configPathLossModel = + _pathLossModel; + eirpGc.resultedPathLossModel = + excThrParam[bandEdgeIdx] + .pathLossModelStr; + eirpGc.buildingPenetrationDb = + excThrParam[bandEdgeIdx] + .buildingPenetrationDB; + eirpGc.offBoresight = + excThrParam[bandEdgeIdx] + .angleOffBoresightDeg; + eirpGc.rxGainDb = + excThrParam[bandEdgeIdx] + .rxGainDB; + eirpGc.discriminationGainDb = + excThrParam[bandEdgeIdx] + .rlanDiscriminationGainDB; + eirpGc.txPropEnv = rlanPropEnv + [scanPtIdx]; + eirpGc.nlcdTx = rlanNlcdLandCat + [scanPtIdx]; + eirpGc.pathClutterTxModel = + excThrParam[bandEdgeIdx] + .pathClutterTxModelStr; + eirpGc.pathClutterTxDb = + excThrParam[bandEdgeIdx] + .pathClutterTxDB; + eirpGc.txClutter = + excThrParam[bandEdgeIdx] + .txClutterStr; + eirpGc.rxPropEnv = + fsPropEnv; + eirpGc.nlcdRx = + nlcdLandCatRx; + eirpGc.pathClutterRxModel = + excThrParam[bandEdgeIdx] + .pathClutterRxModelStr; + eirpGc.pathClutterRxDb = + excThrParam[bandEdgeIdx] + .pathClutterRxDB; + eirpGc.rxClutter = + excThrParam[bandEdgeIdx] + .rxClutterStr; + eirpGc.nearFieldOffsetDb = + excThrParam[bandEdgeIdx] + .nearFieldOffsetDB; + eirpGc.spectralOverlapLossDb = + spectralOverlapLossDB; + eirpGc.ulsAntennaFeederLossDb = + uls->getRxAntennaFeederLossDB(); + eirpGc.rxPowerDbW = + rxPowerDBW; + eirpGc.ulsNoiseLevelDbW = + uls->getNoiseLevelDBW(); + eirpGc.completeRow(); + } + + // Link budget calculations are written to the exceed threshold file if I2NDB exceeds _visibilityThreshold or + // when link distance is less than _closeInDist. If state==1, these links are always written. If state==0. + // these links are written only if _printSkippedLinksFlag is set. + if (excthrGc && + (((state == + 0) && + (_printSkippedLinksFlag)) || + (state == + 1)) && + (std::isnan( + rxPowerDBW) || + (I2NDB > + _visibilityThreshold) || + (distKm * + 1000 < + _closeInDist))) { + double d1; + double d2; + double pathDifference; + double fresnelIndex = + -1.0; + double ulsWavelength = + CConst::c / + ((uls->getStartFreq() + + uls->getStopFreq()) / + 2); + if (ulsSegmentDistance != + -1.0) { + const Vector3 ulsTxPos = + (segIdx ? + uls->getPR(segIdx - + 1) + .positionTx : + uls->getTxPosition()); + d1 = (ulsRxPos - + rlanPosn) + .len() * + 1000; + d2 = (ulsTxPos - + rlanPosn) + .len() * + 1000; + pathDifference = + d1 + + d2 - + ulsSegmentDistance; + fresnelIndex = + pathDifference / + (ulsWavelength / + 2); + } else { + d1 = (ulsRxPos - + rlanPosn) + .len() * + 1000; + d2 = -1.0; + pathDifference = + -1.0; + } + + std::string + rxAntennaTypeStr; + if (segIdx == + numPR) { + CConst::ULSAntennaTypeEnum ulsRxAntennaType = + uls->getRxAntennaType(); + if (ulsRxAntennaType == + CConst::LUTAntennaType) { + rxAntennaTypeStr = std::string( + uls->getRxAntenna() + ->get_strid()); + } else { + rxAntennaTypeStr = + std::string( + CConst::strULSAntennaTypeList + ->type_to_str( + ulsRxAntennaType)) + + excThrParam[bandEdgeIdx] + .rxAntennaSubModelStr; + } + } else { + if (uls->getPR(segIdx) + .type == + CConst::backToBackAntennaPRType) { + CConst::ULSAntennaTypeEnum ulsRxAntennaType = + uls->getPR(segIdx) + .antennaType; + if (ulsRxAntennaType == + CConst::LUTAntennaType) { + rxAntennaTypeStr = std::string( + uls->getPR(segIdx) + .antenna + ->get_strid()); + } else { + rxAntennaTypeStr = + std::string( + CConst::strULSAntennaTypeList + ->type_to_str( + ulsRxAntennaType)) + + excThrParam[bandEdgeIdx] + .rxAntennaSubModelStr; + } + } else { + rxAntennaTypeStr = + ""; + } + } + + std::string bldgTypeStr = + (_fixedBuildingLossFlag ? + "INDOOR_FIXED" : + _buildingType == + CConst::noBuildingType ? + "OUTDOOR" : + _buildingType == + CConst::traditionalBuildingType ? + "TRADITIONAL" : + "THERMALLY_EFFICIENT"); + + excthrGc->fsid = + uls->getID(); + excthrGc->region = + uls->getRegion(); + excthrGc->dbName = std::get< + 0>( + _ulsDatabaseList + [uls->getDBIdx()]); + excthrGc->rlanPosnIdx = + rlanHtIdx; + excthrGc->callsign = + uls->getCallsign(); + excthrGc->fsLon = + uls->getRxLongitudeDeg(); + excthrGc->fsLat = + uls->getRxLatitudeDeg(); + excthrGc->fsAgl = + divIdx == 0 ? + uls->getRxHeightAboveTerrain() : + uls->getDiversityHeightAboveTerrain(); + excthrGc->fsTerrainHeight = + uls->getRxTerrainHeight(); + excthrGc->fsTerrainSource = + _terrainDataModel + ->getSourceName( + uls->getRxHeightSource()); + excthrGc->fsPropEnv = + ulsRxPropEnv; + excthrGc->numPr = + uls->getNumPR(); + excthrGc->divIdx = + divIdx; + excthrGc->segIdx = + segIdx; + excthrGc->segRxLon = + ulsRxLongitude; + excthrGc->segRxLat = + ulsRxLatitude; + + if ((segIdx < + numPR) && + (uls->getPR(segIdx) + .type == + CConst::billboardReflectorPRType)) { + PRClass &pr = uls->getPR( + segIdx); + excthrGc->refThetaIn = + pr.reflectorThetaIN; + excthrGc->refKs = + pr.reflectorKS; + excthrGc->refQ = + pr.reflectorQ; + excthrGc->refD0 = + excThrParam[bandEdgeIdx] + .reflectorD0; + excthrGc->refD1 = + excThrParam[bandEdgeIdx] + .reflectorD1; + } + + excthrGc->rlanLon = + rlanCoord + .longitudeDeg; + excthrGc->rlanLat = + rlanCoord + .latitudeDeg; + excthrGc->rlanAgl = + rlanCoord.heightKm * + 1000.0 - + rlanTerrainHeight + [scanPtIdx]; + excthrGc->rlanTerrainHeight = rlanTerrainHeight + [scanPtIdx]; + excthrGc->rlanTerrainSource = + _terrainDataModel + ->getSourceName( + rlanHeightSource + [scanPtIdx]); + excthrGc->rlanPropEnv = + CConst::strPropEnvList + ->type_to_str( + rlanPropEnv + [scanPtIdx]); + excthrGc->rlanFsDist = + distKm; + excthrGc->rlanFsGroundDist = + groundDistanceKm; + excthrGc->rlanElevAngle = + elevationAngleTxDeg; + excthrGc->boresightAngle = + excThrParam[bandEdgeIdx] + .angleOffBoresightDeg; + excthrGc->rlanTxEirp = + maxEIRPdBm; + if (_rlanAntenna) { + excthrGc->rlanAntennaModel = + _rlanAntenna + ->get_strid(); + excthrGc->rlanAOB = + rlanAngleOffBoresightRad * + 180.0 / + M_PI; + } else { + excthrGc->rlanAntennaModel = + ""; + excthrGc->rlanAOB = + -1.0; + } + excthrGc->rlanDiscriminationGainDB = + rlanDiscriminationGainDB; + excthrGc->bodyLoss = + _bodyLossDB; + excthrGc->rlanClutterCategory = + excThrParam[bandEdgeIdx] + .txClutterStr; + excthrGc->fsClutterCategory = + excThrParam[bandEdgeIdx] + .rxClutterStr; + excthrGc->buildingType = + bldgTypeStr; + excthrGc->buildingPenetration = + excThrParam[bandEdgeIdx] + .buildingPenetrationDB; + excthrGc->buildingPenetrationModel = + excThrParam[bandEdgeIdx] + .buildingPenetrationModelStr; + excthrGc->buildingPenetrationCdf = + excThrParam[bandEdgeIdx] + .buildingPenetrationCDF; + excthrGc->pathLoss = + pathLoss; + excthrGc->pathLossModel = + excThrParam[bandEdgeIdx] + .pathLossModelStr; + excthrGc->pathLossCdf = + excThrParam[bandEdgeIdx] + .pathLossCDF; + excthrGc->pathClutterTx = + excThrParam[bandEdgeIdx] + .pathClutterTxDB; + excthrGc->pathClutterTxMode = + excThrParam[bandEdgeIdx] + .pathClutterTxModelStr; + excthrGc->pathClutterTxCdf = + excThrParam[bandEdgeIdx] + .pathClutterTxCDF; + excthrGc->pathClutterRx = + excThrParam[bandEdgeIdx] + .pathClutterRxDB; + excthrGc->pathClutterRxMode = + excThrParam[bandEdgeIdx] + .pathClutterRxModelStr; + excthrGc->pathClutterRxCdf = + excThrParam[bandEdgeIdx] + .pathClutterRxCDF; + excthrGc->rlanBandwidth = + bandwidthMHz; + excthrGc->rlanStartFreq = + chanStartFreqMHz; + excthrGc->rlanStopFreq = + chanStopFreqMHz; + excthrGc->ulsStartFreq = + uls->getStartFreq() * + 1.0e-6; + excthrGc->ulsStopFreq = + uls->getStopFreq() * + 1.0e-6; + excthrGc->antType = + rxAntennaTypeStr; + excthrGc->antCategory = + CConst::strAntennaCategoryList + ->type_to_str( + segIdx == numPR ? + uls->getRxAntennaCategory() : + uls->getPR(segIdx) + .antCategory); + excthrGc->antGainPeak = + divIdx == 0 ? + uls->getRxGain() : + uls->getDiversityGain(); + + if (segIdx != + numPR) { + excthrGc->prType = + CConst::strPRTypeList + ->type_to_str( + uls->getPR(segIdx) + .type); + excthrGc->prEffectiveGain = + uls->getPR(segIdx) + .effectiveGain; + excthrGc->prDiscrinminationGain = + excThrParam[bandEdgeIdx] + .discriminationGain; + } + + excthrGc->fsGainToRlan = + excThrParam[bandEdgeIdx] + .rxGainDB; + if (!std::isnan( + excThrParam[bandEdgeIdx] + .nearField_xdb)) { + excthrGc->fsNearFieldXdb = + excThrParam[bandEdgeIdx] + .nearField_xdb; + } + if (!std::isnan( + excThrParam[bandEdgeIdx] + .nearField_u)) { + excthrGc->fsNearFieldU = + excThrParam[bandEdgeIdx] + .nearField_u; + } + if (!std::isnan( + excThrParam[bandEdgeIdx] + .nearField_eff)) { + excthrGc->fsNearFieldEff = + excThrParam[bandEdgeIdx] + .nearField_eff; + } + excthrGc->fsNearFieldOffset = + excThrParam[bandEdgeIdx] + .nearFieldOffsetDB; + excthrGc->spectralOverlapLoss = + spectralOverlapLossDB; + excthrGc->polarizationLoss = + _polarizationLossDB; + excthrGc->fsRxFeederLoss = + uls->getRxAntennaFeederLossDB(); + excthrGc->fsRxPwr = + rxPowerDBW; + excthrGc->fsIN = + I2NDB; + excthrGc->eirpLimit = eirpLimit_dBm + [bandEdgeIdx]; + excthrGc->fsSegDist = + ulsSegmentDistance; + excthrGc->rlanCenterFreq = + evalFreqMHz; + excthrGc->fsTxToRlanDist = + d2; + excthrGc->pathDifference = + pathDifference; + excthrGc->ulsWavelength = + ulsWavelength * + 1000; + excthrGc->fresnelIndex = + fresnelIndex; + + excthrGc->completeRow(); + } + } + + // Trying Free Space Path Loss then (if not skipped) - configured Path Loss. + bool skip; + + if (state == + 0) { + // Skipping further computation if Free Space path loss + // EIRP is larger than already established (hence + // configured path loss will be even larger), + // otherwise proceeding with (potentially slow) + // configured path loss computation + + // 1dB allowance to accommodate for amplifying clutters and other artifacts + if (contains2D) { + skip = false; + } else if ( + ((eirpLimit_dBm + [0] - + 1) < + std::get< + 0>( + channel->segList + [freqSegIdx])) || + ((numBandEdge == + 2) && + ((eirpLimit_dBm + [1] - + 1) < + std::get< + 1>( + channel->segList + [freqSegIdx])))) { + itmSegList + .push_back( + freqSegIdx); + for (int bandEdgeIdx = + 0; + bandEdgeIdx < + numBandEdge; + ++bandEdgeIdx) { + RxPowerDBW_0PLList[bandEdgeIdx] + .push_back( + rxPowerDBW_0PL + [bandEdgeIdx]); + if (excthrGc) { + excThrParamList[bandEdgeIdx] + .push_back( + excThrParam + [bandEdgeIdx]); + } + } + skip = true; + } else { + skip = true; + } + } else { + skip = false; + } + + // When _printSkippedLinksFlag set, links analyzed with FSPL that are skipped are still inserted into the exc_thr file. + // This is useful for testing and debugging. Note that the extra printing impacts execution speed. When _printSkippedLinksFlag is + // not set, skipped links are no inserted in the exc_thr file, so execution speed is not impacted. + // if ((!_printSkippedLinksFlag) && (skip)) { + // continue; + // } + + if (!skip) { + if ((contains2D) && + (!std::isnan( + _reportUnavailPSDdBmPerMHz))) { + double clipeirpLimit_dBm = + _reportUnavailPSDdBmPerMHz + + 10 * log10(bandwidthMHz); + for (int bandEdgeIdx = + 0; + bandEdgeIdx < + numBandEdge; + ++bandEdgeIdx) { + if (eirpLimit_dBm + [bandEdgeIdx] < + clipeirpLimit_dBm) { + eirpLimit_dBm + [bandEdgeIdx] = + clipeirpLimit_dBm; + } + } + } + if (channelType == + ChannelType:: + INQUIRED_CHANNEL) { + if ((std::get< + 2>( + channel->segList + [freqSegIdx]) != + RED) && + (eirpLimit_dBm + [0] < + std::get< + 0>( + channel->segList + [freqSegIdx]))) { + if (eirpLimit_dBm + [0] < + _minEIRP_dBm) { + channel->segList + [freqSegIdx] = + std::make_tuple( + _minEIRP_dBm, + _minEIRP_dBm, + RED); + } else { + std::get< + 0>( + channel->segList + [freqSegIdx]) = eirpLimit_dBm + [0]; + std::get< + 1>( + channel->segList + [freqSegIdx]) = eirpLimit_dBm + [0]; + } + } + if ((eirpLimit_dBm + [0] < + eirpLimitList + [ulsIdx])) { + eirpLimitList[ulsIdx] = eirpLimit_dBm + [0]; + } + } else { + // INQUIRED_FREQUENCY + if (std::get< + 2>( + channel->segList + [freqSegIdx]) != + RED) { + bool redFlag = + true; + for (int bandEdgeIdx = + 0; + bandEdgeIdx < + numBandEdge; + ++bandEdgeIdx) { + double bandEdgeVal = + ((bandEdgeIdx == + 0) ? + std::get< + 0>( + channel->segList + [freqSegIdx]) : + std::get< + 1>( + channel->segList + [freqSegIdx])); + if (eirpLimit_dBm + [bandEdgeIdx] < + bandEdgeVal) { + if (bandEdgeIdx == + 0) { + std::get< + 0>( + channel->segList + [freqSegIdx]) = eirpLimit_dBm + [bandEdgeIdx]; + } else { + std::get< + 1>( + channel->segList + [freqSegIdx]) = eirpLimit_dBm + [bandEdgeIdx]; + } + bandEdgeVal = eirpLimit_dBm + [bandEdgeIdx]; + } + double psd = + bandEdgeVal - + 10.0 * log(bandwidthMHz) / + log(10.0); + if (psd > + _minPSD_dBmPerMHz) { + redFlag = + false; + } + } + if (redFlag) { + double eirp_dBm = + _minPSD_dBmPerMHz + + 10.0 * log10(bandwidthMHz); + channel->segList + [freqSegIdx] = + std::make_tuple( + eirp_dBm, + eirp_dBm, + RED); + } + } + } + } + } + } + + if (state == + 0) { + freqSegIdx++; + if (freqSegIdx == + channel->segList + .size()) { + std::string + txClutterStr; + std::string + rxClutterStr; + std::string + pathLossModelStr; + double pathLossCDF; + std::string + pathClutterTxModelStr; + double pathClutterTxCDF; + double pathClutterTxDB; + std::string + pathClutterRxModelStr; + double pathClutterRxCDF; + double pathClutterRxDB; + + int numItmSeg = + itmSegList + .size(); + if ((numItmSeg > + 1) || + ((numItmSeg > + 0) && + (channelType == + ChannelType:: + INQUIRED_FREQUENCY))) { + if (channelType == + ChannelType:: + INQUIRED_FREQUENCY) { + itmStartFreqMHz = + channel->freqMHzList + [itmSegList + [0]]; + itmStopFreqMHz = + channel->freqMHzList + [itmSegList + [numItmSeg - + 1] + + 1]; + } else { + itmStartFreqMHz = + (channel->freqMHzList + [itmSegList + [0]] + + channel->freqMHzList + [itmSegList + [0] + + 1]) / + 2.0; + itmStopFreqMHz = + (channel->freqMHzList + [itmSegList + [numItmSeg]] + + channel->freqMHzList + [itmSegList + [numItmSeg] + + 1]) / + 2.0; + } + + computePathLoss( + contains2D ? + CConst::FSPLPathLossModel : + _pathLossModel, + false, + rlanPropEnv + [scanPtIdx], + fsPropEnv, + rlanNlcdLandCat + [scanPtIdx], + nlcdLandCatRx, + distKm, + fsplDistKm, + win2DistKm, + itmStartFreqMHz * + 1.0e6, + rlanCoord + .longitudeDeg, + rlanCoord + .latitudeDeg, + rlanHtAboveTerrain, + elevationAngleTxDeg, + ulsRxLongitude, + ulsRxLatitude, + ulsRxHeightAGL, + elevationAngleRxDeg, + itmStartPathLoss, + pathClutterTxDB, + pathClutterRxDB, + pathLossModelStr, + pathLossCDF, + pathClutterTxModelStr, + pathClutterTxCDF, + pathClutterRxModelStr, + pathClutterRxCDF, + &txClutterStr, + &rxClutterStr, + &(uls->ITMHeightProfile), + &(uls->isLOSHeightProfile), + &(uls->isLOSSurfaceFrac) +#if DEBUG_AFC + , + uls->ITMHeightType +#endif + ); + + computePathLoss( + contains2D ? + CConst::FSPLPathLossModel : + _pathLossModel, + false, + rlanPropEnv + [scanPtIdx], + fsPropEnv, + rlanNlcdLandCat + [scanPtIdx], + nlcdLandCatRx, + distKm, + fsplDistKm, + win2DistKm, + itmStopFreqMHz * + 1.0e6, + rlanCoord + .longitudeDeg, + rlanCoord + .latitudeDeg, + rlanHtAboveTerrain, + elevationAngleTxDeg, + ulsRxLongitude, + ulsRxLatitude, + ulsRxHeightAGL, + elevationAngleRxDeg, + itmStopPathLoss, + pathClutterTxDB, + pathClutterRxDB, + pathLossModelStr, + pathLossCDF, + pathClutterTxModelStr, + pathClutterTxCDF, + pathClutterRxModelStr, + pathClutterRxCDF, + &txClutterStr, + &rxClutterStr, + &(uls->ITMHeightProfile), + &(uls->isLOSHeightProfile), + &(uls->isLOSSurfaceFrac) +#if DEBUG_AFC + , + uls->ITMHeightType +#endif + ); + state = 1; + itmSegIdx = + 0; + } else if ( + numItmSeg == + 1) { + itmStartFreqMHz = + channel->freqMHzList + [itmSegList + [0]]; + itmStopFreqMHz = + channel->freqMHzList + [itmSegList + [0] + + 1]; + + computePathLoss( + contains2D ? + CConst::FSPLPathLossModel : + _pathLossModel, + false, + rlanPropEnv + [scanPtIdx], + fsPropEnv, + rlanNlcdLandCat + [scanPtIdx], + nlcdLandCatRx, + distKm, + fsplDistKm, + win2DistKm, + (itmStartFreqMHz + + itmStopFreqMHz) * + 0.5e6, + rlanCoord + .longitudeDeg, + rlanCoord + .latitudeDeg, + rlanHtAboveTerrain, + elevationAngleTxDeg, + ulsRxLongitude, + ulsRxLatitude, + ulsRxHeightAGL, + elevationAngleRxDeg, + itmStartPathLoss, + pathClutterTxDB, + pathClutterRxDB, + pathLossModelStr, + pathLossCDF, + pathClutterTxModelStr, + pathClutterTxCDF, + pathClutterRxModelStr, + pathClutterRxCDF, + &txClutterStr, + &rxClutterStr, + &(uls->ITMHeightProfile), + &(uls->isLOSHeightProfile), + &(uls->isLOSSurfaceFrac) +#if DEBUG_AFC + , + uls->ITMHeightType +#endif + ); + + itmStopPathLoss = + itmStartPathLoss; + + state = 1; + itmSegIdx = + 0; + } else { + contFlag = + false; + } + } + } else { + itmSegIdx++; + if (itmSegIdx == + itmSegList + .size()) { + contFlag = + false; + } + } + } + } + +#if DEBUG_AFC + if (traceFlag && + (!contains2D)) { + if (uls->ITMHeightProfile) { + double lon1Rad = + rlanCoord + .longitudeDeg * + M_PI / + 180.0; + double lat1Rad = + rlanCoord + .latitudeDeg * + M_PI / + 180.0; + int N = ((int)uls->ITMHeightProfile + [0]) + + 1; + for (int ptIdx = + 0; + ptIdx < + N; + ptIdx++) { + Vector3 losPathPosn = + (((double)(N - + 1 - + ptIdx)) / + (N - + 1)) * rlanPosn + + (((double)ptIdx) / + (N - + 1)) * ulsRxPos; + GeodeticCoord losPathPosnGeodetic = + EcefModel::ecefToGeodetic( + losPathPosn); + + double ptLon = + losPathPosnGeodetic + .longitudeDeg; + double ptLat = + losPathPosnGeodetic + .latitudeDeg; + double losPathHeight = + losPathPosnGeodetic + .heightKm * + 1000.0; + + double lon2Rad = + ptLon * + M_PI / + 180.0; + double lat2Rad = + ptLat * + M_PI / + 180.0; + double slat = sin( + (lat2Rad - + lat1Rad) / + 2); + double slon = sin( + (lon2Rad - + lon1Rad) / + 2); + double ptDistKm = + 2 * + CConst::averageEarthRadius * + asin(sqrt( + slat * slat + + cos(lat1Rad) * + cos(lat2Rad) * + slon * + slon)) * + 1.0e-3; + + pathTraceGc + .ptId = + (boost::format( + "PT_%d") % + ptIdx) + .str(); + pathTraceGc + .lon = + ptLon; + pathTraceGc + .lat = + ptLat; + pathTraceGc + .dist = + ptDistKm; + pathTraceGc + .amsl = + uls->ITMHeightProfile + [2 + + ptIdx]; + pathTraceGc + .losAmsl = + losPathHeight; + pathTraceGc + .fsid = + uls->getID(); + pathTraceGc + .divIdx = + divIdx; + pathTraceGc + .segIdx = + segIdx; + pathTraceGc + .scanPtIdx = + scanPtIdx; + pathTraceGc + .rlanHtIdx = + rlanHtIdx; + pathTraceGc + .completeRow(); + } + } + } +#endif + } + + if (uls->ITMHeightProfile) { + free(uls->ITMHeightProfile); + uls->ITMHeightProfile = + (double *)NULL; + } + if (uls->isLOSHeightProfile) { + free(uls->isLOSHeightProfile); + uls->isLOSHeightProfile = + (double *)NULL; + } + } + } + + if (fFSList) { + fprintf(fFSList, "%d", uls->getID()); + fprintf(fFSList, + ",%s,%s", + uls->getRxCallsign().c_str(), + uls->getCallsign().c_str()); + fprintf(fFSList, + ",%d,%d,%d", + numPR, + divIdx, + segIdx); + fprintf(fFSList, + ",%.1f,%.1f", + uls->getStartFreq() * 1.0e-6, + uls->getStopFreq() * 1.0e-6); + fprintf(fFSList, + ",%.6f,%.6f", + ulsRxLongitude, + ulsRxLatitude); + fprintf(fFSList, ",%.1f", ulsRxHeightAGL); + fprintf(fFSList, + ",%.1f", + ulsSegmentDistance); + + if (segIdx < numPR) { + PRClass &pr = uls->getPR(segIdx); + if (pr.type == + CConst::billboardReflectorPRType) { + fprintf(fFSList, + ",%.5f", + pr.reflectorThetaIN); + fprintf(fFSList, + ",%.5f", + pr.reflectorKS); + fprintf(fFSList, + ",%.5f", + pr.reflectorQ); + } else { + fprintf(fFSList, ",,,"); + } + fprintf(fFSList, + ",%.3f", + pr.pathSegGain); + fprintf(fFSList, + ",%.3f", + pr.effectiveGain); + } else { + fprintf(fFSList, ",,,,,"); + } + + fprintf(fFSList, "\n"); + } + +#if DEBUG_AFC + time_t t2 = time(NULL); + tstr = strdup(ctime(&t2)); + strtok(tstr, "\n"); + + LOGGER_DEBUG(logger) + << numProc << " [" << ulsIdx + 1 << " / " + << sortedUlsList.size() + << "] FSID = " << uls->getID() + << " DIV_IDX = " << divIdx + << " SEG_IDX = " << segIdx << " " << tstr + << " Elapsed Time = " << (t2 - t1); + + free(tstr); + + if (false && (numProc == 10)) { + cont = false; + } + +#endif + + } else { +#if DEBUG_AFC + // uls is not included in calculations + LOGGER_DEBUG(logger) + << "FSID: " << uls->getID() + << " DIV_IDX = " << divIdx + << " SEG_IDX = " << segIdx + << ", distKm: " << sqrt(distKmSquared) + << ", Outside MAX_RADIUS"; +#endif + } + } + } + +#if DEBUG_AFC + if (traceFlag) { + traceIdx++; + } +#endif + } + + numProc++; + } + + for (int colorIdx = 0; (colorIdx < 3) && (fkml); ++colorIdx) { + fkml->writeStartElement("Folder"); + std::string visibilityStr; + int addPlacemarks; + std::string placemarkStyleStr; + std::string polyStyleStr; + if (colorIdx == 0) { + fkml->writeTextElement("name", "RED"); + visibilityStr = "1"; + addPlacemarks = 1; + placemarkStyleStr = "#redPlacemark"; + polyStyleStr = "#redPoly"; + } else if (colorIdx == 1) { + fkml->writeTextElement("name", "YELLOW"); + visibilityStr = "1"; + addPlacemarks = 1; + placemarkStyleStr = "#yellowPlacemark"; + polyStyleStr = "#yellowPoly"; + } else { + fkml->writeTextElement("name", "GREEN"); + visibilityStr = "0"; + addPlacemarks = 0; + placemarkStyleStr = "#greenPlacemark"; + polyStyleStr = "#greenPoly"; + } + fkml->writeTextElement("visibility", visibilityStr.c_str()); + + for (ulsIdx = 0; ulsIdx < (int)sortedUlsList.size(); ulsIdx++) { + bool useFlag = ulsFlagList[ulsIdx]; + + if (useFlag) { + if (colorIdx == 0) { + useFlag = ((eirpLimitList[ulsIdx] < _minEIRP_dBm) ? true : + false); + } else if (colorIdx == 1) { + useFlag = (((eirpLimitList[ulsIdx] < _maxEIRP_dBm) && + (eirpLimitList[ulsIdx] >= _minEIRP_dBm)) ? + true : + false); + } else if (colorIdx == 2) { + useFlag = ((eirpLimitList[ulsIdx] >= _maxEIRP_dBm) ? true : + false); + } + } + if ((useFlag) && (fkml)) { + ULSClass *uls = sortedUlsList[ulsIdx]; + std::string dbName = std::get<0>(_ulsDatabaseList[uls->getDBIdx()]); + + fkml->writeStartElement("Folder"); + fkml->writeTextElement("name", + QString::fromStdString( + dbName + "_" + + std::to_string(uls->getID()))); + // fkml->writeTextElement("name", + // QString::fromStdString(uls->getCallsign())); + + int numPR = uls->getNumPR(); + for (int segIdx = 0; segIdx < numPR + 1; ++segIdx) { + Vector3 ulsTxPosn = + (segIdx == 0 ? uls->getTxPosition() : + uls->getPR(segIdx - 1).positionTx); + double ulsTxLongitude = + (segIdx == 0 ? uls->getTxLongitudeDeg() : + uls->getPR(segIdx - 1).longitudeDeg); + double ulsTxLatitude = + (segIdx == 0 ? uls->getTxLatitudeDeg() : + uls->getPR(segIdx - 1).latitudeDeg); + double ulsTxHeight = + (segIdx == 0 ? uls->getTxHeightAMSL() : + uls->getPR(segIdx - 1).heightAMSLTx); + + Vector3 ulsRxPosn = (segIdx == numPR ? + uls->getRxPosition() : + uls->getPR(segIdx).positionRx); + double ulsRxLongitude = + (segIdx == numPR ? uls->getRxLongitudeDeg() : + uls->getPR(segIdx).longitudeDeg); + double ulsRxLatitude = + (segIdx == numPR ? uls->getRxLatitudeDeg() : + uls->getPR(segIdx).latitudeDeg); + double ulsRxHeight = + (segIdx == numPR ? uls->getRxHeightAMSL() : + uls->getPR(segIdx).heightAMSLRx); + + bool txLocFlag = (!std::isnan(ulsTxPosn.x())) && + (!std::isnan(ulsTxPosn.y())) && + (!std::isnan(ulsTxPosn.z())); + + double linkDistKm; + if (!txLocFlag) { + linkDistKm = 1.0; + Vector3 segPointing = + (segIdx == numPR ? + uls->getAntennaPointing() : + uls->getPR(segIdx).pointing); + ulsTxPosn = ulsRxPosn + linkDistKm * segPointing; + } else { + linkDistKm = (ulsTxPosn - ulsRxPosn).len(); + } + + if ((segIdx == 0) && (addPlacemarks) && (txLocFlag)) { + fkml->writeStartElement("Placemark"); + fkml->writeTextElement( + "name", + QString::asprintf("%s %s_%d", + "TX", + dbName.c_str(), + uls->getID())); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", + placemarkStyleStr.c_str()); + fkml->writeStartElement("Point"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeTextElement( + "coordinates", + QString::asprintf("%.10f,%.10f,%.2f", + ulsTxLongitude, + ulsTxLatitude, + ulsTxHeight)); + fkml->writeEndElement(); // Point + fkml->writeEndElement(); // Placemark + } + + double beamWidthDeg = uls->computeBeamWidth(3.0); + double beamWidthRad = beamWidthDeg * (M_PI / 180.0); + + Vector3 zvec = (ulsTxPosn - ulsRxPosn).normalized(); + Vector3 xvec = + (Vector3(zvec.y(), -zvec.x(), 0.0)).normalized(); + Vector3 yvec = zvec.cross(xvec); + + int numCvgPoints = 32; + + std::vector ptList; + double cvgTheta = beamWidthRad; + int cvgPhiIdx; + for (cvgPhiIdx = 0; cvgPhiIdx < numCvgPoints; ++cvgPhiIdx) { + double cvgPhi = 2 * M_PI * cvgPhiIdx / numCvgPoints; + Vector3 cvgIntPosn = + ulsRxPosn + + linkDistKm * (zvec * cos(cvgTheta) + + (xvec * cos(cvgPhi) + + yvec * sin(cvgPhi)) * + sin(cvgTheta)); + + GeodeticCoord cvgIntPosnGeodetic = + EcefModel::ecefToGeodetic(cvgIntPosn); + ptList.push_back(cvgIntPosnGeodetic); + } + + if (addPlacemarks) { + std::string nameStr; + if (segIdx == numPR) { + nameStr = "RX"; + } else { + nameStr = "PR " + + std::to_string(segIdx + 1); + ; + } + fkml->writeStartElement("Placemark"); + fkml->writeTextElement( + "name", + QString::asprintf("%s %s_%d", + nameStr.c_str(), + dbName.c_str(), + uls->getID())); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", + placemarkStyleStr.c_str()); + fkml->writeStartElement("Point"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeTextElement( + "coordinates", + QString::asprintf("%.10f,%.10f,%.2f", + ulsRxLongitude, + ulsRxLatitude, + ulsRxHeight)); + fkml->writeEndElement(); // Point + fkml->writeEndElement(); // Placemark + } + + if (true) { + fkml->writeStartElement("Folder"); + fkml->writeTextElement( + "name", + QString::asprintf("Beamcone_%d", + segIdx + 1)); + + for (cvgPhiIdx = 0; cvgPhiIdx < numCvgPoints; + ++cvgPhiIdx) { + fkml->writeStartElement("Placemark"); + fkml->writeTextElement( + "name", + QString::asprintf("p%d", + cvgPhiIdx)); + fkml->writeTextElement( + "styleUrl", + polyStyleStr.c_str()); + fkml->writeTextElement( + "visibility", + visibilityStr.c_str()); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("altitudeMode", + "absolute"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + QString more_coords = QString::asprintf( + "%.10f,%.10f,%.2f\n", + ulsRxLongitude, + ulsRxLatitude, + ulsRxHeight); + + GeodeticCoord pt = ptList[cvgPhiIdx]; + more_coords.append(QString::asprintf( + "%.10f,%.10f,%.2f\n", + pt.longitudeDeg, + pt.latitudeDeg, + pt.heightKm * 1000.0)); + + pt = ptList[(cvgPhiIdx + 1) % numCvgPoints]; + more_coords.append(QString::asprintf( + "%.10f,%.10f,%.2f\n", + pt.longitudeDeg, + pt.latitudeDeg, + pt.heightKm * 1000.0)); + + more_coords.append( + QString::asprintf("%.10f,%.10f,%." + "2f\n", + ulsRxLongitude, + ulsRxLatitude, + ulsRxHeight)); + + fkml->writeTextElement("coordinates", + more_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + } + fkml->writeEndElement(); // Beamcone + } + } + fkml->writeEndElement(); // Folder + } + } + fkml->writeEndElement(); // Folder + } + + for (ulsIdx = 0; ulsIdx < (int)sortedUlsList.size(); ulsIdx++) { + if (ulsFlagList[ulsIdx]) { + _ulsIdxList.push_back( + ulsIdx); // Store the ULS indices that are used in analysis + } + } + + if (fkml) { + fkml->writeEndElement(); // Document + fkml->writeEndElement(); // kml + fkml->writeEndDocument(); + } + + if (fFSList) { + fclose(fFSList); + } + + if (numProc == 0) { + errStr << "Analysis region contains no FS receivers"; + LOGGER_WARN(logger) << errStr.str(); + statusMessageList.push_back(errStr.str()); + } + +#if DEBUG_AFC + time_t tEndULS = time(NULL); + tstr = strdup(ctime(&tEndULS)); + strtok(tstr, "\n"); + + int elapsedTime = (int)(tEndULS - tStartULS); + + int et = elapsedTime; + int elapsedTimeSec = et % 60; + et = et / 60; + int elapsedTimeMin = et % 60; + et = et / 60; + int elapsedTimeHour = et % 24; + et = et / 24; + int elapsedTimeDay = et; + + std::cout << "End Processing ULS RX's " << tstr + << " Elapsed time = " << (tEndULS - tStartULS) << " sec = " << elapsedTimeDay + << " days " << elapsedTimeHour << " hours " << elapsedTimeMin << " min " + << elapsedTimeSec << " sec." << std::endl + << std::flush; + + free(tstr); + +#endif + /**************************************************************************************/ + + _terrainDataModel->printStats(); + + int chanIdx; + for (chanIdx = 0; chanIdx < (int)_channelList.size(); ++chanIdx) { + ChannelStruct *channel = &(_channelList[chanIdx]); + for (int freqSegIdx = 0; freqSegIdx < channel->segList.size(); ++freqSegIdx) { + ChannelColor chanColor = std::get<2>(channel->segList[freqSegIdx]); + if ((channel->type == ChannelType::INQUIRED_CHANNEL) && + (chanColor != BLACK) && (chanColor != RED)) { + double chanEirpLimit = + std::min(std::get<0>(channel->segList[freqSegIdx]), + std::get<1>(channel->segList[freqSegIdx])); + if (chanEirpLimit == _maxEIRP_dBm) { + std::get<2>(channel->segList[freqSegIdx]) = GREEN; + } else if (chanEirpLimit >= _minEIRP_dBm) { + std::get<2>(channel->segList[freqSegIdx]) = YELLOW; + } else { + std::get<2>(channel->segList[freqSegIdx]) = RED; + } + } + } + } + + free(eirpLimitList); + free(ulsFlagList); + + if (excthrGc) { + delete excthrGc; + } +} + +// Returns _ulsList content, sorted in by decreasing of crude interference +// (computed from free-space path loss and off-bearing gain only) +std::vector AfcManager::getSortedUls() +{ + std::vector ret; + for (auto ulsIdx = 0; ulsIdx < _ulsList->getSize(); ++ulsIdx) { + ret.push_back((*_ulsList)[ulsIdx]); + } + + // AP ECEF coordinates + Vector3 apEcef = EcefModel::fromGeodetic( + GeodeticCoord::fromLatLon(_rlanRegion->getCenterLatitude(), + _rlanRegion->getCenterLongitude(), + _rlanRegion->getCenterHeightAMSL() / 1000.)); + // Maps ULS IDs to sort keys + std::map sortKeys; + for (auto &uls : ret) { + auto ulsRxEcef = EcefModel::fromGeodetic( + GeodeticCoord::fromLatLon(uls->getRxLatitudeDeg(), + uls->getRxLongitudeDeg(), + 0)); + auto ulsCenterFreqHz = (uls->getStartFreq() + uls->getStopFreq()) / 2; + auto ulsAntennaPointing = uls->getAntennaPointing(); + auto lineOfSightVectorKm = ulsRxEcef - apEcef; + double distKm = lineOfSightVectorKm.len(); + + // The more pathloss the less the interference + double pathLoss = 20.0 * + log((4 * M_PI * ulsCenterFreqHz * distKm * 1000) / CConst::c) / + log(10.0); + auto interferenceScore = pathLoss; + + // The more discrimination gain the more the interference + double angleOffBoresightDeg = acos(ulsAntennaPointing.dot( + -(lineOfSightVectorKm.normalized()))) * + 180.0 / M_PI; + std::string rxAntennaSubModelStrDummy; + double discriminationGainDb = uls->computeRxGain(angleOffBoresightDeg, + 0, + ulsCenterFreqHz, + rxAntennaSubModelStrDummy, + 0); + interferenceScore -= discriminationGainDb; + + sortKeys[uls->getID()] = interferenceScore; + } + std::sort(ret.begin(), ret.end(), [sortKeys](ULSClass *const &l, ULSClass *const &r) { + return sortKeys.at(l->getID()) < sortKeys.at(r->getID()); + }); + return ret; +} + +/******************************************************************************************/ + +/******************************************************************************************/ +/* AfcManager::runScanAnalysis() */ +/******************************************************************************************/ +void AfcManager::runScanAnalysis() +{ + std::ostringstream errStr; + + LOGGER_INFO(logger) << "Executing AfcManager::runScanAnalysis()"; + + std::map bw_index_map; + + FILE *fscan; + std::string scanFileName = _serialNumber.toStdString() + "_scan.csv"; + if (!(fscan = fopen(scanFileName.c_str(), "wb"))) { + errStr << std::string("ERROR: Unable to open scanFile \"") + "/tmp/scan.csv" + + std::string("\"\n"); + throw std::runtime_error(errStr.str()); + } + fprintf(fscan, + "SCAN_PT_IDX,HEIGHT_IDX,RLAN_LONGITUDE (deg)" + ",RLAN_LATITUDE (deg)" + ",RLAN_HEIGHT_ABOVE_TERRAIN (m)" + ",RLAN_TERRAIN_HEIGHT (m)" + ",RLAN_TERRAIN_SOURCE" + ",RLAN_PROP_ENV"); + + // List all bandwidths covered by op-classes, merge duplicates + for (auto &opClass : _opClass) { + int bw = opClass.bandWidth; + bw_index_map[bw] = 0; + } + + int numBW = 0; + for (auto &map : bw_index_map) { + int bw = map.first; + fprintf(fscan, + ",NUM_CHAN_BLACK_%d_MHZ,NUM_CHAN_RED_%d_MHZ,NUM_CHAN_YELLOW_%d_MHZ,NUM_" + "CHAN_GREEN_%d_MHZ", + bw, + bw, + bw, + bw); + // Note down the index of bandwidth in the map + map.second = numBW++; + } + fprintf(fscan, "\n"); + fflush(fscan); + + _rlanRegion->configure(_rlanHeightType, _terrainDataModel); + + double heightUncertainty = _rlanRegion->getHeightUncertainty(); + int NHt = (int)ceil(heightUncertainty / _scanres_ht); + Vector3 rlanPosnList[2 * NHt + 1]; + GeodeticCoord rlanCoordList[2 * NHt + 1]; + + int bwIdx; + int numBlack[numBW]; + int numGreen[numBW]; + int numYellow[numBW]; + int numRed[numBW]; + + /**************************************************************************************/ + /* Get Uncertainty Region Scan Points */ + /**************************************************************************************/ + std::vector scanPointList = _rlanRegion->getScan(_scanRegionMethod, + _scanres_xy, + _scanres_points_per_degree); + /**************************************************************************************/ + + /**************************************************************************************/ + /* Create KML file */ + /**************************************************************************************/ + std::string kmzFileName = _serialNumber.toStdString() + "_scan.kmz"; + ZXmlWriter *kml_writer = new ZXmlWriter(kmzFileName); + auto &fkml = kml_writer->xml_writer; + + if (fkml) { + fkml->setAutoFormatting(true); + fkml->writeStartDocument(); + fkml->writeStartElement("kml"); + fkml->writeAttribute("xmlns", "http://www.opengis.net/kml/2.2"); + fkml->writeStartElement("Document"); + fkml->writeTextElement("name", "AFC"); + fkml->writeTextElement("open", "1"); + fkml->writeTextElement("description", "Display Point Analysis Results"); + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "transGrayPoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "7d7f7f7f"); + fkml->writeEndElement(); // Polystyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "transBluePoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "7dff0000"); + fkml->writeEndElement(); // PolyStyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "redPoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("color", "ff0000ff"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "7d0000ff"); + fkml->writeEndElement(); // Polystyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "yellowPoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("color", "ff00ffff"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "7d00ffff"); + fkml->writeEndElement(); // Polystyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "greenPoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("color", "ff00ff00"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "7d00ff00"); + fkml->writeEndElement(); // Polystyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "blackPoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("color", "ff000000"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "7d000000"); + fkml->writeEndElement(); // Polystyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "dotStyle"); + fkml->writeStartElement("IconStyle"); + fkml->writeStartElement("Icon"); + fkml->writeTextElement("href", + "http://maps.google.com/mapfiles/kml/shapes/" + "placemark_circle.png"); + fkml->writeEndElement(); // Icon + fkml->writeEndElement(); // IconStyle + fkml->writeStartElement("LabelStyle"); + fkml->writeTextElement("scale", "0"); + fkml->writeEndElement(); // LabelStyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "redPlacemark"); + fkml->writeStartElement("IconStyle"); + fkml->writeTextElement("color", "ff0000ff"); + fkml->writeStartElement("Icon"); + fkml->writeTextElement("href", + "http://maps.google.com/mapfiles/kml/pushpin/" + "ylw-pushpin.png"); + fkml->writeEndElement(); // Icon + fkml->writeEndElement(); // IconStyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "yellowPlacemark"); + fkml->writeStartElement("IconStyle"); + fkml->writeTextElement("color", "ff00ffff"); + fkml->writeStartElement("Icon"); + fkml->writeTextElement("href", + "http://maps.google.com/mapfiles/kml/pushpin/" + "ylw-pushpin.png"); + fkml->writeEndElement(); // Icon + fkml->writeEndElement(); // IconStyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "greenPlacemark"); + fkml->writeStartElement("IconStyle"); + fkml->writeTextElement("color", "ff00ff00"); + fkml->writeStartElement("Icon"); + fkml->writeTextElement("href", + "http://maps.google.com/mapfiles/kml/pushpin/" + "ylw-pushpin.png"); + fkml->writeEndElement(); // Icon + fkml->writeEndElement(); // IconStyle + fkml->writeEndElement(); // Style + + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "blackPlacemark"); + fkml->writeStartElement("IconStyle"); + fkml->writeTextElement("color", "ff000000"); + fkml->writeStartElement("Icon"); + fkml->writeTextElement("href", + "http://maps.google.com/mapfiles/kml/pushpin/" + "ylw-pushpin.png"); + fkml->writeEndElement(); // Icon + fkml->writeEndElement(); // IconStyle + fkml->writeEndElement(); // Style + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Draw uncertainty cylinder in KML */ + /**************************************************************************************/ + if (fkml) { + int ptIdx; + fkml->writeStartElement("Folder"); + fkml->writeTextElement("name", "RLAN"); + + std::vector ptList = _rlanRegion->getBoundary(_terrainDataModel); + + /**********************************************************************************/ + /* CENTER */ + /**********************************************************************************/ + GeodeticCoord rlanCenterPtGeo = EcefModel::toGeodetic(_rlanRegion->getCenterPosn()); + + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", "CENTER"); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#dotStyle"); + fkml->writeStartElement("Point"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeTextElement("coordinates", + QString::asprintf("%.10f,%.10f,%.2f", + rlanCenterPtGeo.longitudeDeg, + rlanCenterPtGeo.latitudeDeg, + rlanCenterPtGeo.heightKm * 1000.0)); + fkml->writeEndElement(); // Point + fkml->writeEndElement(); // Placemark + /**********************************************************************************/ + + /**********************************************************************************/ + /* TOP */ + /**********************************************************************************/ + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", "TOP"); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#transGrayPoly"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("tessellate", "0"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + QString top_coords = QString(); + for (ptIdx = 0; ptIdx <= (int)ptList.size(); ptIdx++) { + GeodeticCoord pt = ptList[ptIdx % ptList.size()]; + top_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt.longitudeDeg, + pt.latitudeDeg, + pt.heightKm * 1000.0 + + _rlanRegion->getHeightUncertainty())); + } + + fkml->writeTextElement("coordinates", top_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + /**********************************************************************************/ + + /**********************************************************************************/ + /* BOTTOM */ + /**********************************************************************************/ + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", "BOTTOM"); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#transGrayPoly"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("tessellate", "0"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + QString bottom_coords = QString(); + for (ptIdx = 0; ptIdx <= (int)ptList.size(); ptIdx++) { + GeodeticCoord pt = ptList[ptIdx % ptList.size()]; + bottom_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt.longitudeDeg, + pt.latitudeDeg, + pt.heightKm * 1000.0 - + _rlanRegion->getHeightUncertainty())); + } + fkml->writeTextElement("coordinates", bottom_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + + /**********************************************************************************/ + + /**********************************************************************************/ + /* SIDES */ + /**********************************************************************************/ + for (ptIdx = 0; ptIdx < (int)ptList.size(); ptIdx++) { + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", QString::asprintf("S_%d", ptIdx)); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#transGrayPoly"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("tessellate", "0"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + GeodeticCoord pt1 = ptList[ptIdx]; + GeodeticCoord pt2 = ptList[(ptIdx + 1) % ptList.size()]; + QString side_coords; + side_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt1.longitudeDeg, + pt1.latitudeDeg, + pt1.heightKm * 1000.0 - + _rlanRegion->getHeightUncertainty())); + side_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt1.longitudeDeg, + pt1.latitudeDeg, + pt1.heightKm * 1000.0 + + _rlanRegion->getHeightUncertainty())); + side_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt2.longitudeDeg, + pt2.latitudeDeg, + pt2.heightKm * 1000.0 + + _rlanRegion->getHeightUncertainty())); + side_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt2.longitudeDeg, + pt2.latitudeDeg, + pt2.heightKm * 1000.0 - + _rlanRegion->getHeightUncertainty())); + side_coords.append( + QString::asprintf("%.10f,%.10f,%.2f\n", + pt1.longitudeDeg, + pt1.latitudeDeg, + pt1.heightKm * 1000.0 - + _rlanRegion->getHeightUncertainty())); + + fkml->writeTextElement("coordinates", side_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + } + /**********************************************************************************/ + + /**********************************************************************************/ + /* Scan Points */ + /**********************************************************************************/ + fkml->writeStartElement("Folder"); + fkml->writeTextElement("name", "SCAN POINTS"); + + for (ptIdx = 0; ptIdx < (int)scanPointList.size(); ptIdx++) { + LatLon scanPt = scanPointList[ptIdx]; + + double rlanTerrainHeight, bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum rlanHeightSource; + _terrainDataModel->getTerrainHeight(scanPt.second, + scanPt.first, + rlanTerrainHeight, + bldgHeight, + lidarHeightResult, + rlanHeightSource); + + double height0; + if (_rlanRegion->getFixedHeightAMSL()) { + height0 = _rlanRegion->getCenterHeightAMSL(); + } else { + height0 = _rlanRegion->getCenterHeightAMSL() - + _rlanRegion->getCenterTerrainHeight() + rlanTerrainHeight; + } + + int htIdx; + for (htIdx = 0; htIdx <= 2 * NHt; ++htIdx) { + double heightAMSL = height0 + + (NHt ? (htIdx - NHt) * heightUncertainty / NHt : + 0.0); + + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", + QString::asprintf("SCAN_POINT_%d_%d", + ptIdx, + htIdx)); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#dotStyle"); + fkml->writeStartElement("Point"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeTextElement("coordinates", + QString::asprintf("%.10f,%.10f,%.2f", + scanPt.second, + scanPt.first, + heightAMSL)); + fkml->writeEndElement(); // Point + fkml->writeEndElement(); // Placemark + } + } + + fkml->writeEndElement(); // Scan Points + /**********************************************************************************/ + + fkml->writeEndElement(); // Folder + + fkml->writeStartElement("Folder"); + fkml->writeTextElement("name", "Denied Region"); + int drIdx; + for (drIdx = 0; drIdx < (int)_deniedRegionList.size(); ++drIdx) { + DeniedRegionClass *dr = _deniedRegionList[drIdx]; + + fkml->writeStartElement("Folder"); + fkml->writeTextElement("name", + QString("DR_") + QString::number(dr->getID())); + + int numPtsCircle = 32; + int rectIdx, numRect; + double rectLonStart, rectLonStop, rectLatStart, rectLatStop; + double circleRadius, longitudeCenter, latitudeCenter; + double drTerrainHeight, drBldgHeight, drHeightAGL; + Vector3 drCenterPosn; + Vector3 drUpVec; + Vector3 drEastVec; + Vector3 drNorthVec; + QString dr_coords; + MultibandRasterClass::HeightResult drLidarHeightResult; + CConst::HeightSourceEnum drHeightSource; + DeniedRegionClass::GeometryEnum drGeometry = dr->getGeometry(); + switch (drGeometry) { + case DeniedRegionClass::rectGeometry: + case DeniedRegionClass::rect2Geometry: + numRect = ((RectDeniedRegionClass *)dr)->getNumRect(); + for (rectIdx = 0; rectIdx < numRect; rectIdx++) { + std::tie(rectLonStart, + rectLonStop, + rectLatStart, + rectLatStop) = + ((RectDeniedRegionClass *)dr) + ->getRect(rectIdx); + + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", + QString("RECT_") + + QString::number( + rectIdx)); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", + "#transBluePoly"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("tessellate", "0"); + fkml->writeTextElement("altitudeMode", + "clampToGround"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + dr_coords = QString(); + dr_coords.append(QString::asprintf("%.10f,%.10f,%." + "2f\n", + rectLonStart, + rectLatStart, + 0.0)); + dr_coords.append(QString::asprintf("%.10f,%.10f,%." + "2f\n", + rectLonStop, + rectLatStart, + 0.0)); + dr_coords.append(QString::asprintf("%.10f,%.10f,%." + "2f\n", + rectLonStop, + rectLatStop, + 0.0)); + dr_coords.append(QString::asprintf("%.10f,%.10f,%." + "2f\n", + rectLonStart, + rectLatStop, + 0.0)); + dr_coords.append(QString::asprintf("%.10f,%.10f,%." + "2f\n", + rectLonStart, + rectLatStart, + 0.0)); + + fkml->writeTextElement("coordinates", dr_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + } + break; + case DeniedRegionClass::circleGeometry: + case DeniedRegionClass::horizonDistGeometry: + circleRadius = + ((CircleDeniedRegionClass *)dr) + ->computeRadius( + _rlanRegion->getMaxHeightAGL()); + longitudeCenter = ((CircleDeniedRegionClass *)dr) + ->getLongitudeCenter(); + latitudeCenter = ((CircleDeniedRegionClass *)dr) + ->getLatitudeCenter(); + drHeightAGL = dr->getHeightAGL(); + _terrainDataModel->getTerrainHeight(longitudeCenter, + latitudeCenter, + drTerrainHeight, + drBldgHeight, + drLidarHeightResult, + drHeightSource); + + drCenterPosn = EcefModel::geodeticToEcef( + latitudeCenter, + longitudeCenter, + (drTerrainHeight + drHeightAGL) / 1000.0); + drUpVec = drCenterPosn.normalized(); + drEastVec = (Vector3(-drUpVec.y(), drUpVec.x(), 0.0)) + .normalized(); + drNorthVec = drUpVec.cross(drEastVec); + + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", + QString("RECT_") + + QString::number(rectIdx)); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#transBluePoly"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("extrude", "0"); + fkml->writeTextElement("tessellate", "0"); + fkml->writeTextElement("altitudeMode", "clampToGround"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + dr_coords = QString(); + for (ptIdx = 0; ptIdx <= numPtsCircle; ++ptIdx) { + double phi = 2 * M_PI * ptIdx / numPtsCircle; + Vector3 circlePtPosn = drCenterPosn + + (circleRadius / 1000) * + (drEastVec * + cos(phi) + + drNorthVec * + sin(phi)); + + GeodeticCoord circlePtPosnGeodetic = + EcefModel::ecefToGeodetic(circlePtPosn); + + dr_coords.append(QString::asprintf( + "%.10f,%.10f,%.2f\n", + circlePtPosnGeodetic.longitudeDeg, + circlePtPosnGeodetic.latitudeDeg, + 0.0)); + } + + fkml->writeTextElement("coordinates", dr_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + + break; + default: + CORE_DUMP; + break; + } + fkml->writeEndElement(); // Folder + } + fkml->writeEndElement(); // Folder + } + /**************************************************************************************/ + + if (fkml) { + fkml->writeEndElement(); // Document + fkml->writeEndElement(); // kml + fkml->writeEndDocument(); + } + delete kml_writer; + + /**************************************************************************************/ + /* Compute Channel Availability */ + /**************************************************************************************/ + const double exclusionDistKmSquared = (_exclusionDist / 1000.0) * (_exclusionDist / 1000.0); + const double maxRadiusKmSquared = (_maxRadius / 1000.0) * (_maxRadius / 1000.0); + + if (_rlanRegion->getMinHeightAGL() < _minRlanHeightAboveTerrain) { + throw std::runtime_error( + ErrStream() + << std::string("ERROR: Point Analysis: Invalid RLAN parameter settings.") + << std::endl + << std::string("RLAN Min Height above terrain = ") + << _rlanRegion->getMinHeightAGL() << std::endl + << std::string("RLAN must be more than ") << _minRlanHeightAboveTerrain + << " meters above terrain" << std::endl); + } + + int scanPtIdx; + for (scanPtIdx = 0; scanPtIdx < (int)scanPointList.size(); scanPtIdx++) { + LatLon scanPt = scanPointList[scanPtIdx]; + + /**************************************************************************************/ + /* Determine propagation environment of RLAN using scan point */ + /**************************************************************************************/ + CConst::NLCDLandCatEnum nlcdLandCatTx; + CConst::PropEnvEnum rlanPropEnv = computePropEnv(scanPt.second, + scanPt.first, + nlcdLandCatTx); + /**************************************************************************************/ + + double rlanTerrainHeight, bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum rlanHeightSource; + _terrainDataModel->getTerrainHeight(scanPt.second, + scanPt.first, + rlanTerrainHeight, + bldgHeight, + lidarHeightResult, + rlanHeightSource); + + double height0; + if (_rlanRegion->getFixedHeightAMSL()) { + height0 = _rlanRegion->getCenterHeightAMSL(); + } else { + height0 = _rlanRegion->getCenterHeightAMSL() - + _rlanRegion->getCenterTerrainHeight() + rlanTerrainHeight; + } + + int rlanPosnIdx; + int htIdx; + for (htIdx = 0; htIdx <= 2 * NHt; ++htIdx) { + rlanCoordList[htIdx] = GeodeticCoord::fromLatLon( + scanPt.first, + scanPt.second, + (height0 + (NHt ? (htIdx - NHt) * heightUncertainty / NHt : 0.0)) / + 1000.0); + rlanPosnList[htIdx] = EcefModel::fromGeodetic(rlanCoordList[htIdx]); + } + + int numRlanPosn = 2 * NHt + 1; + + for (rlanPosnIdx = 0; rlanPosnIdx < numRlanPosn; ++rlanPosnIdx) { + Vector3 rlanPosn = rlanPosnList[rlanPosnIdx]; + GeodeticCoord rlanCoord = rlanCoordList[rlanPosnIdx]; + + /**************************************************************************************/ + /* Initialize eirpLimit_dBm to _maxEIRP_dBm for all channels */ + /**************************************************************************************/ + for (auto &channel : _channelList) { + for (int freqSegIdx = 0; freqSegIdx < channel.segList.size(); + ++freqSegIdx) { + channel.segList[freqSegIdx] = std::make_tuple(_maxEIRP_dBm, + _maxEIRP_dBm, + GREEN); + } + } + /**************************************************************************************/ + + double rlanHtAboveTerrain = rlanCoord.heightKm * 1000.0 - rlanTerrainHeight; + + int drIdx; + for (drIdx = 0; drIdx < (int)_deniedRegionList.size(); ++drIdx) { + DeniedRegionClass *dr = _deniedRegionList[drIdx]; + if (dr->intersect(rlanCoord.longitudeDeg, + rlanCoord.latitudeDeg, + 0.0, + rlanHtAboveTerrain)) { + for (auto &channel : _channelList) { + for (int freqSegIdx = 0; + freqSegIdx < channel.segList.size(); + ++freqSegIdx) { + if (std::get<2>( + channel.segList[freqSegIdx]) != + BLACK) { + double chanStartFreq = + channel.freqMHzList + [freqSegIdx] * + 1.0e6; + double chanStopFreq = + channel.freqMHzList + [freqSegIdx + 1] * + 1.0e6; + bool hasOverlap = computeSpectralOverlapLoss( + (double *)NULL, + chanStartFreq, + chanStopFreq, + dr->getStartFreq(), + dr->getStopFreq(), + false, + CConst::psdSpectralAlgorithm); + if (hasOverlap) { + channel.segList + [freqSegIdx] = std::make_tuple( + -std::numeric_limits< + double>:: + infinity(), + -std::numeric_limits< + double>:: + infinity(), + BLACK); + } + } + } + } + } + } + + int ulsIdx; + for (ulsIdx = 0; ulsIdx < _ulsList->getSize(); ++ulsIdx) { + ULSClass *uls = (*_ulsList)[ulsIdx]; + const Vector3 ulsRxPos = uls->getRxPosition(); + Vector3 lineOfSightVectorKm = ulsRxPos - rlanPosn; + double distKmSquared = + (lineOfSightVectorKm).dot(lineOfSightVectorKm); + double distKm = lineOfSightVectorKm.len(); + double dAP = rlanPosn.len(); + double duls = ulsRxPos.len(); + double elevationAngleTxDeg = + 90.0 - + acos(rlanPosn.dot(lineOfSightVectorKm) / (dAP * distKm)) * + 180.0 / M_PI; + double elevationAngleRxDeg = + 90.0 - + acos(ulsRxPos.dot(-lineOfSightVectorKm) / (duls * distKm)) * + 180.0 / M_PI; + + // Use Haversine formula with average earth radius of 6371 km + double lon1Rad = rlanCoord.longitudeDeg * M_PI / 180.0; + double lat1Rad = rlanCoord.latitudeDeg * M_PI / 180.0; + double lon2Rad = uls->getRxLongitudeDeg() * M_PI / 180.0; + double lat2Rad = uls->getRxLatitudeDeg() * M_PI / 180.0; + double slat = sin((lat2Rad - lat1Rad) / 2); + double slon = sin((lon2Rad - lon1Rad) / 2); + double groundDistanceKm = 2 * CConst::averageEarthRadius * + asin(sqrt(slat * slat + + cos(lat1Rad) * cos(lat2Rad) * + slon * slon)) * + 1.0e-3; + + double win2DistKm; + if (_winner2UseGroundDistanceFlag) { + win2DistKm = groundDistanceKm; + } else { + win2DistKm = distKm; + } + + double fsplDistKm; + if (_fsplUseGroundDistanceFlag) { + fsplDistKm = groundDistanceKm; + } else { + fsplDistKm = distKm; + } + + if ((distKmSquared < maxRadiusKmSquared) && + (distKmSquared > exclusionDistKmSquared)) { + /**************************************************************************************/ + /* Determine propagation environment of FS, if needed. */ + /**************************************************************************************/ + CConst::NLCDLandCatEnum nlcdLandCatRx; + CConst::PropEnvEnum fsPropEnv; + if ((_applyClutterFSRxFlag) && + (uls->getRxHeightAboveTerrain() <= _maxFsAglHeight)) { + fsPropEnv = computePropEnv(uls->getRxLongitudeDeg(), + uls->getRxLatitudeDeg(), + nlcdLandCatRx); + } else { + fsPropEnv = CConst::unknownPropEnv; + } + /**************************************************************************************/ + + for (auto &channel : _channelList) { + for (int freqSegIdx = 0; + freqSegIdx < channel.segList.size(); + ++freqSegIdx) { + if (std::get<2>( + channel.segList[freqSegIdx]) != + BLACK) { + double chanStartFreq = + channel.freqMHzList + [freqSegIdx] * + 1.0e6; + double chanStopFreq = + channel.freqMHzList + [freqSegIdx + 1] * + 1.0e6; + + bool useACI = + (channel.type == INQUIRED_FREQUENCY ? + false : + _aciFlag); + CConst::SpectralAlgorithmEnum spectralAlgorithm = + (channel.type == INQUIRED_FREQUENCY ? + CConst::psdSpectralAlgorithm : + _channelResponseAlgorithm); + // LOGGER_INFO(logger) << "COMPUTING + // SPECTRAL OVERLAP FOR FSID = " << + // uls->getID(); + double spectralOverlapLossDB; + bool hasOverlap = + computeSpectralOverlapLoss( + &spectralOverlapLossDB, + chanStartFreq, + chanStopFreq, + uls->getStartFreq(), + uls->getStopFreq(), + useACI, + spectralAlgorithm); + + if (hasOverlap) { + // double bandwidth = + // chanStopFreq - + // chanStartFreq; + double chanCenterFreq = + (chanStartFreq + + chanStopFreq) / + 2; + + std::string + buildingPenetrationModelStr; + double buildingPenetrationCDF; + double buildingPenetrationDB = computeBuildingPenetration( + _buildingType, + elevationAngleTxDeg, + chanCenterFreq, + buildingPenetrationModelStr, + buildingPenetrationCDF); + + std::string txClutterStr; + std::string rxClutterStr; + std::string + pathLossModelStr; + double pathLossCDF; + double pathLoss; + std::string + pathClutterTxModelStr; + double pathClutterTxCDF; + double pathClutterTxDB; + std::string + pathClutterRxModelStr; + double pathClutterRxCDF; + double pathClutterRxDB; + + computePathLoss( + _pathLossModel, + false, + rlanPropEnv, + fsPropEnv, + nlcdLandCatTx, + nlcdLandCatRx, + distKm, + fsplDistKm, + win2DistKm, + chanCenterFreq, + rlanCoord + .longitudeDeg, + rlanCoord + .latitudeDeg, + rlanHtAboveTerrain, + elevationAngleTxDeg, + uls->getRxLongitudeDeg(), + uls->getRxLatitudeDeg(), + uls->getRxHeightAboveTerrain(), + elevationAngleRxDeg, + pathLoss, + pathClutterTxDB, + pathClutterRxDB, + pathLossModelStr, + pathLossCDF, + pathClutterTxModelStr, + pathClutterTxCDF, + pathClutterRxModelStr, + pathClutterRxCDF, + &txClutterStr, + &rxClutterStr, + &(uls->ITMHeightProfile), + &(uls->isLOSHeightProfile), + &(uls->isLOSSurfaceFrac) +#if DEBUG_AFC + , + uls->ITMHeightType +#endif + ); + + std::string + rxAntennaSubModelStr; + double angleOffBoresightDeg = + acos(uls->getAntennaPointing() + .dot(-(lineOfSightVectorKm + .normalized()))) * + 180.0 / M_PI; + double rxGainDB = uls->computeRxGain( + angleOffBoresightDeg, + elevationAngleRxDeg, + chanCenterFreq, + rxAntennaSubModelStr, + 0); + + double rxPowerDBW = + (_maxEIRP_dBm - + 30.0) - + _bodyLossDB - + buildingPenetrationDB - + pathLoss - + pathClutterTxDB - + pathClutterRxDB + + rxGainDB - + spectralOverlapLossDB - + _polarizationLossDB - + uls->getRxAntennaFeederLossDB(); + + double I2NDB = + rxPowerDBW - + uls->getNoiseLevelDBW(); + + double marginDB = + _IoverN_threshold_dB - + I2NDB; + + double eirpLimit_dBm = + _maxEIRP_dBm + + marginDB; + + if (eirpLimit_dBm < + std::min( + std::get<0>( + channel.segList + [freqSegIdx]), + std::get<1>( + channel.segList + [freqSegIdx]))) { + std::get<0>( + channel.segList + [freqSegIdx]) = + eirpLimit_dBm; + std::get<1>( + channel.segList + [freqSegIdx]) = + eirpLimit_dBm; + } + } + } + } + } + } + + if (uls->ITMHeightProfile) { + free(uls->ITMHeightProfile); + uls->ITMHeightProfile = (double *)NULL; + } + if (uls->isLOSHeightProfile) { + free(uls->isLOSHeightProfile); + uls->isLOSHeightProfile = (double *)NULL; + } + } + + for (bwIdx = 0; bwIdx < numBW; ++bwIdx) { + numBlack[bwIdx] = 0; + numGreen[bwIdx] = 0; + numYellow[bwIdx] = 0; + numRed[bwIdx] = 0; + } + + for (auto &channel : _channelList) { + for (int freqSegIdx = 0; freqSegIdx < channel.segList.size(); + ++freqSegIdx) { + if (channel.type == ChannelType::INQUIRED_CHANNEL) { + bwIdx = bw_index_map[channel.bandwidth(freqSegIdx)]; + double channelEirp = std::min( + std::get<0>(channel.segList[freqSegIdx]), + std::get<1>(channel.segList[freqSegIdx])); + if (std::get<2>(channel.segList[freqSegIdx]) == + BLACK) { + numBlack[bwIdx]++; + } else if (channelEirp == _maxEIRP_dBm) { + numGreen[bwIdx]++; + } else if (channelEirp >= _minEIRP_dBm) { + numYellow[bwIdx]++; + } else { + numRed[bwIdx]++; + } + } + } + } + + fprintf(fscan, + "%d,%d,%.6f,%.6f,%.1f,%.1f,%s,%s", + scanPtIdx, + rlanPosnIdx, + rlanCoord.longitudeDeg, + rlanCoord.latitudeDeg, + rlanHtAboveTerrain, + rlanTerrainHeight, + _terrainDataModel->getSourceName(rlanHeightSource).c_str(), + CConst::strPropEnvList->type_to_str(rlanPropEnv)); + + for (bwIdx = 0; bwIdx < numBW; ++bwIdx) { + fprintf(fscan, + ",%d,%d,%d,%d", + numBlack[bwIdx], + numRed[bwIdx], + numYellow[bwIdx], + numGreen[bwIdx]); + } + fprintf(fscan, "\n"); + fflush(fscan); + } + } + fclose(fscan); + + /**************************************************************************************/ + + _terrainDataModel->printStats(); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/* AfcManager::runExclusionZoneAnalysis() */ +/******************************************************************************************/ +void AfcManager::runExclusionZoneAnalysis() +{ +#if DEBUG_AFC + // std::vector fsidTraceList{2128, 3198, 82443}; + // std::vector fsidTraceList{64324}; + std::vector fsidTraceList {66423}; + std::string pathTraceFile = "path_trace.csv.gz"; +#endif + + LOGGER_INFO(logger) << "Executing AfcManager::runExclusionZoneAnalysis()"; + + int numContourPoints = 360; + + /**************************************************************************************/ + /* Allocate / Initialize Exclusion Zone Point Vector */ + /**************************************************************************************/ + _exclusionZone.resize(numContourPoints); + /**************************************************************************************/ + +#if 0 + /**************************************************************************************/ + /* Create list of RLAN bandwidths */ + /**************************************************************************************/ + std::vector rlanBWStrList = split(_rlanBWStr, ','); // Splits comma-separated list containing the different bandwidths + + _numChan = (int *)malloc(rlanBWStrList.size() * sizeof(int)); + + int bwIdx; + for (bwIdx = 0; bwIdx < (int)rlanBWStrList.size(); ++bwIdx) { + // Iterate over each bandwidth (not channels yet) + char *chptr; + double bandwidth = std::strtod(rlanBWStrList[bwIdx].c_str(), &chptr); + _rlanBWList.push_back(bandwidth); // Load the bandwidth list with the designated bandwidths + + int numChan = (int)floor((_wlanMaxFreq - _wlanMinFreq) / bandwidth + 1.0e-6); + _numChan[bwIdx] = numChan; + } + /**************************************************************************************/ +#endif + + int ulsIdx; + ULSClass *uls = findULSID(_exclusionZoneFSID, 0, ulsIdx); + std::string dbName = std::get<0>(_ulsDatabaseList[uls->getDBIdx()]); + + ChannelStruct channel = _channelList[_exclusionZoneRLANChanIdx]; + int freqSegIdx = 0; + + double bandwidth = (channel.bandwidth(freqSegIdx)) * 1.0e6; + + double chanStartFreq = channel.freqMHzList[freqSegIdx] * 1.0e6; + double chanStopFreq = channel.freqMHzList[freqSegIdx + 1] * 1.0e6; + bool useACI = (channel.type == INQUIRED_FREQUENCY ? false : _aciFlag); + CConst::SpectralAlgorithmEnum spectralAlgorithm = (channel.type == INQUIRED_FREQUENCY ? + CConst::psdSpectralAlgorithm : + _channelResponseAlgorithm); + // LOGGER_INFO(logger) << "COMPUTING SPECTRAL OVERLAP FOR FSID = " << uls->getID(); + double spectralOverlapLossDB; + bool hasOverlap = computeSpectralOverlapLoss(&spectralOverlapLossDB, + chanStartFreq, + chanStopFreq, + uls->getStartFreq(), + uls->getStopFreq(), + useACI, + spectralAlgorithm); + double chanCenterFreq = (chanStartFreq + chanStopFreq) / 2; + + if (!hasOverlap) { + throw std::runtime_error( + ErrStream() + << "ERROR: Specified RLAN spectrum does not overlap FS spectrum. FSID: " + << _exclusionZoneFSID << " goes from " << uls->getStartFreq() / 1.0e6 + << " MHz to " << uls->getStopFreq() / 1.0e6 << " MHz"); + } + LOGGER_INFO(logger) << "FSID = " << _exclusionZoneFSID << " found"; + LOGGER_INFO(logger) << "LON: " << uls->getRxLongitudeDeg(); + LOGGER_INFO(logger) << "LAT: " << uls->getRxLatitudeDeg(); + LOGGER_INFO(logger) << "SPECTRAL_OVERLAP_LOSS (dB) = " << spectralOverlapLossDB; + + /**************************************************************************************/ + /* Create excThrFile, useful for debugging */ + /**************************************************************************************/ + ExThrGzipCsv excthrGc(_excThrFile); + + /**************************************************************************************/ + +#if DEBUG_AFC + /**************************************************************************************/ + /* Create pathTraceFile, for debugging */ + /**************************************************************************************/ + TraceGzipCsv pathTraceGc(pathTraceFile); + /**************************************************************************************/ +#endif + +#if 0 + int ulsLonIdx; + int ulsLatIdx; + int regionIdx; + char ulsRxPropEnv; + _popGrid->findDeg(uls->getRxLongitudeDeg(), uls->getRxLatitudeDeg(), ulsLonIdx, ulsLatIdx, ulsRxPropEnv, regionIdx); + double ulsPopVal = _popGrid->getPop(ulsLonIdx, ulsLatIdx); + if (ulsPopVal == 0.0) { + ulsRxPropEnv = 'Z'; + } +#else + char ulsRxPropEnv = ' '; +#endif + + /**************************************************************************************/ + /* Compute Exclusion Zone */ + /**************************************************************************************/ + LOGGER_INFO(logger) << "Begin computing exclusion zone"; + + /******************************************************************************/ + /* Estimate dist for which FSPL puts I/N below threshold */ + /******************************************************************************/ + double pathLossDB = (_exclusionZoneRLANEIRPDBm - 30.0) - _bodyLossDB + uls->getRxGain() - + spectralOverlapLossDB - _polarizationLossDB - + uls->getRxAntennaFeederLossDB() - uls->getNoiseLevelDBW() - + _IoverN_threshold_dB; + + double dFSPL = exp(pathLossDB * log(10.0) / 20.0) * CConst::c / (4 * M_PI * chanCenterFreq); + /******************************************************************************/ + + double initialD0 = (dFSPL * 180.0 / + (CConst::earthRadius * M_PI * + cos(uls->getRxLatitudeDeg() * M_PI / 180.0))); + + const double minPossibleRadius = 10.0; + double minPossibleD = (minPossibleRadius * 180.0 / (CConst::earthRadius * M_PI)); + + double distKm0, distKm1, distKmM; + int exclPtIdx; + + for (exclPtIdx = 0; exclPtIdx < numContourPoints; exclPtIdx++) { + LOGGER_DEBUG(logger) + << "computing exlPtIdx: " << exclPtIdx << '/' << numContourPoints; + double cc = cos(exclPtIdx * 2 * M_PI / numContourPoints); + double ss = sin(exclPtIdx * 2 * M_PI / numContourPoints); + + bool cont; + + /******************************************************************************/ + /* Step 1: Compute margin at dFSPL, If this margin is not positive something */ + /* is seriously wrong. */ + /******************************************************************************/ + double margin0; + double d0 = initialD0; + do { + margin0 = computeIToNMargin(d0, + cc, + ss, + uls, + chanCenterFreq, + bandwidth, + chanStartFreq, + chanStopFreq, + spectralOverlapLossDB, + ulsRxPropEnv, + distKm0, + "", + nullptr); + + if (margin0 < 0.0) { + d0 *= 1.1; + cont = true; + printf("DBNAME = %s FSID = %d, EXCL_PT_IDX = %d, dFSPL = %.1f DIST " + "= %.1f margin = %.3f\n", + dbName.c_str(), + uls->getID(), + exclPtIdx, + dFSPL, + 1000 * distKm0, + margin0); + } else { + cont = false; + } + } while (cont); + /******************************************************************************/ + // printf("exclPtIdx = %d, dFSPL = %.3f margin = %.3f\n", exclPtIdx, dFSPL, + // margin0); + + bool minRadiusFlag = false; + /******************************************************************************/ + /* Step 2: Bound position for which margin = 0 */ + /******************************************************************************/ + double d1, margin1; + cont = true; + do { + d1 = d0 * 0.95; + margin1 = computeIToNMargin(d1, + cc, + ss, + uls, + chanCenterFreq, + bandwidth, + chanStartFreq, + chanStopFreq, + spectralOverlapLossDB, + ulsRxPropEnv, + distKm1, + "", + nullptr); + + if (d1 <= minPossibleD) { + d0 = d1; + margin0 = margin1; + distKm0 = distKm1; + minRadiusFlag = true; + cont = false; + } else if (margin1 >= 0.0) { + d0 = d1; + margin0 = margin1; + distKm0 = distKm1; + } else { + cont = false; + } + } while (cont); + // printf("Position Bounded [%.10f, %.10f]\n", d1, d0); + /**********************************************************************************/ + + if (!minRadiusFlag) { + /******************************************************************************/ + /* Step 3: Shrink interval to find where margin = 0 */ + /******************************************************************************/ + while (d0 - d1 > 1.0e-6) { + double dm = (d1 + d0) / 2; + double marginM = computeIToNMargin(dm, + cc, + ss, + uls, + chanCenterFreq, + bandwidth, + chanStartFreq, + chanStopFreq, + spectralOverlapLossDB, + ulsRxPropEnv, + distKmM, + "", + nullptr); + if (marginM < 0.0) { + d1 = dm; + margin1 = marginM; + distKm1 = distKmM; + } else { + d0 = dm; + margin0 = marginM; + distKm0 = distKmM; + } + } + /**********************************************************************************/ + } + + margin1 = computeIToNMargin(d1, + cc, + ss, + uls, + chanCenterFreq, + bandwidth, + chanStartFreq, + chanStopFreq, + spectralOverlapLossDB, + ulsRxPropEnv, + distKm1, + "Above Thr", + &excthrGc); + margin0 = computeIToNMargin(d0, + cc, + ss, + uls, + chanCenterFreq, + bandwidth, + chanStartFreq, + chanStopFreq, + spectralOverlapLossDB, + ulsRxPropEnv, + distKm0, + "Below Thr", + &excthrGc); + + double rlanLon = uls->getRxLongitudeDeg() + d0 * cc; + double rlanLat = uls->getRxLatitudeDeg() + d0 * ss; + + _exclusionZone[exclPtIdx] = std::make_pair(rlanLon, rlanLat); + + // LOGGER_DEBUG(logger) << std::setprecision(15) << exclusionZonePtLon << " " << + // exclusionZonePtLat; + } + LOGGER_INFO(logger) << "Done computing exclusion zone"; + + _exclusionZoneFSTerrainHeight = uls->getRxTerrainHeight(); + _exclusionZoneHeightAboveTerrain = uls->getRxHeightAboveTerrain(); + + writeKML(); + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/* AfcManager::computeIToNMargin() */ +/******************************************************************************************/ +double AfcManager::computeIToNMargin(double d, + double cc, + double ss, + ULSClass *uls, + double chanCenterFreq, + double bandwidth, + double chanStartFreq, + double chanStopFreq, + double spectralOverlapLossDB, + char ulsRxPropEnv, + double &distKmM, + std::string comment, + ExThrGzipCsv *excthrGc) +{ + Vector3 rlanPosnList[3]; + GeodeticCoord rlanCoordList[3]; + + double rlanLon, rlanLat, rlanHeight; + rlanLon = uls->getRxLongitudeDeg() + d * cc; + rlanLat = uls->getRxLatitudeDeg() + d * ss; + + double fsHeight = uls->getRxTerrainHeight() + uls->getRxHeightAboveTerrain(); + + double rlanHeightInput = std::get<2>(_rlanLLA); + double heightUncertainty = std::get<2>(_rlanUncerts_m); + + double rlanTerrainHeight, bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum rlanHeightSource; + _terrainDataModel->getTerrainHeight(rlanLon, + rlanLat, + rlanTerrainHeight, + bldgHeight, + lidarHeightResult, + rlanHeightSource); + if (_rlanHeightType == CConst::AMSLHeightType) { + rlanHeight = rlanHeightInput; + } else if (_rlanHeightType == CConst::AGLHeightType) { + rlanHeight = rlanHeightInput + rlanTerrainHeight; + } else { + throw std::runtime_error(ErrStream() << "ERROR: INVALID_VALUE _rlanHeightType = " + << _rlanHeightType); + } + + if (rlanHeight - heightUncertainty - rlanTerrainHeight < _minRlanHeightAboveTerrain) { + throw std::runtime_error( + ErrStream() + << std::string("ERROR: ItoN: Invalid RLAN parameter settings.") << std::endl + << std::string("RLAN Height = ") << rlanHeight << std::endl + << std::string("Height Uncertainty = ") << heightUncertainty << std::endl + << std::string("Terrain Height at RLAN Location = ") << rlanTerrainHeight + << std::endl + << std::string("RLAN is ") + << rlanHeight - heightUncertainty - rlanTerrainHeight + << " meters above terrain" << std::endl + << std::string("RLAN must be more than ") << _minRlanHeightAboveTerrain + << " meters above terrain" << std::endl); + } + + Vector3 rlanCenterPosn = EcefModel::geodeticToEcef(rlanLat, rlanLon, rlanHeight / 1000.0); + + Vector3 rlanPosn0; + if ((rlanHeight - heightUncertainty < fsHeight) && + (rlanHeight + heightUncertainty > fsHeight)) { + rlanPosn0 = EcefModel::geodeticToEcef(rlanLat, rlanLon, fsHeight / 1000.0); + } else { + rlanPosn0 = rlanCenterPosn; + } + + /**************************************************************************************/ + /* Determine propagation environment of RLAN using centerLat, centerLon, popGrid */ + /**************************************************************************************/ + CConst::NLCDLandCatEnum nlcdLandCatTx; + CConst::PropEnvEnum rlanPropEnv = computePropEnv(rlanLon, rlanLat, nlcdLandCatTx, false); + /**************************************************************************************/ + + /**************************************************************************************/ + /* Determine propagation environment of FS, if needed. */ + /**************************************************************************************/ + CConst::NLCDLandCatEnum nlcdLandCatRx; + CConst::PropEnvEnum fsPropEnv; + if ((_applyClutterFSRxFlag) && (uls->getRxHeightAboveTerrain() <= _maxFsAglHeight)) { + fsPropEnv = computePropEnv(uls->getRxLongitudeDeg(), + uls->getRxLatitudeDeg(), + nlcdLandCatRx); + } else { + fsPropEnv = CConst::unknownPropEnv; + } + /**************************************************************************************/ + + Vector3 upVec = rlanCenterPosn.normalized(); + const Vector3 ulsRxPos = uls->getRxPosition(); + + rlanPosnList[0] = rlanPosn0; // RLAN Position + rlanPosnList[1] = rlanCenterPosn + + (heightUncertainty / 1000.0) * + upVec; // RLAN Position raised by height uncertainty + rlanPosnList[2] = rlanCenterPosn - + (heightUncertainty / 1000.0) * + upVec; // RLAN Position lowered by height uncertainty + + rlanCoordList[0] = EcefModel::toGeodetic(rlanPosnList[0]); + rlanCoordList[1] = EcefModel::toGeodetic(rlanPosnList[1]); + rlanCoordList[2] = EcefModel::toGeodetic(rlanPosnList[2]); + + int numRlanPosn = ((heightUncertainty == 0.0) ? 1 : 3); + + CConst::ULSAntennaTypeEnum ulsRxAntennaType = uls->getRxAntennaType(); + + int rlanPosnIdx; + + double minMarginDB = 0.0; + for (rlanPosnIdx = 0; rlanPosnIdx < numRlanPosn; ++rlanPosnIdx) { + Vector3 rlanPosn = rlanPosnList[rlanPosnIdx]; + GeodeticCoord rlanCoord = rlanCoordList[rlanPosnIdx]; + Vector3 lineOfSightVectorKm = ulsRxPos - rlanPosn; + double distKm = lineOfSightVectorKm.len(); + double dAP = rlanPosn.len(); + double duls = ulsRxPos.len(); + double elevationAngleTxDeg = 90.0 - acos(rlanPosn.dot(lineOfSightVectorKm) / + (dAP * distKm)) * + 180.0 / M_PI; + double elevationAngleRxDeg = 90.0 - acos(ulsRxPos.dot(-lineOfSightVectorKm) / + (duls * distKm)) * + 180.0 / M_PI; + + // Use Haversine formula with average earth radius of 6371 km + double lon1Rad = rlanCoord.longitudeDeg * M_PI / 180.0; + double lat1Rad = rlanCoord.latitudeDeg * M_PI / 180.0; + double lon2Rad = uls->getRxLongitudeDeg() * M_PI / 180.0; + double lat2Rad = uls->getRxLatitudeDeg() * M_PI / 180.0; + double slat = sin((lat2Rad - lat1Rad) / 2); + double slon = sin((lon2Rad - lon1Rad) / 2); + double groundDistanceKm = 2 * CConst::averageEarthRadius * + asin(sqrt(slat * slat + + cos(lat1Rad) * cos(lat2Rad) * slon * slon)) * + 1.0e-3; + + double win2DistKm; + if (_winner2UseGroundDistanceFlag) { + win2DistKm = groundDistanceKm; + } else { + win2DistKm = distKm; + } + + double fsplDistKm; + if (_fsplUseGroundDistanceFlag) { + fsplDistKm = groundDistanceKm; + } else { + fsplDistKm = distKm; + } + + std::string buildingPenetrationModelStr; + double buildingPenetrationCDF; + double buildingPenetrationDB = + computeBuildingPenetration(_buildingType, + elevationAngleTxDeg, + chanCenterFreq, + buildingPenetrationModelStr, + buildingPenetrationCDF); + + std::string txClutterStr; + std::string rxClutterStr; + std::string pathLossModelStr; + double pathLossCDF; + double pathLoss; + std::string pathClutterTxModelStr; + double pathClutterTxCDF; + double pathClutterTxDB; + std::string pathClutterRxModelStr; + double pathClutterRxCDF; + double pathClutterRxDB; + + double rlanHtAboveTerrain = rlanCoord.heightKm * 1000.0 - rlanTerrainHeight; + + computePathLoss(_pathLossModel, + false, + (rlanPropEnv == CConst::unknownPropEnv ? CConst::barrenPropEnv : + rlanPropEnv), + fsPropEnv, + nlcdLandCatTx, + nlcdLandCatRx, + distKm, + fsplDistKm, + win2DistKm, + chanCenterFreq, + rlanCoord.longitudeDeg, + rlanCoord.latitudeDeg, + rlanHtAboveTerrain, + elevationAngleTxDeg, + uls->getRxLongitudeDeg(), + uls->getRxLatitudeDeg(), + uls->getRxHeightAboveTerrain(), + elevationAngleRxDeg, + pathLoss, + pathClutterTxDB, + pathClutterRxDB, + pathLossModelStr, + pathLossCDF, + pathClutterTxModelStr, + pathClutterTxCDF, + pathClutterRxModelStr, + pathClutterRxCDF, + &txClutterStr, + &rxClutterStr, + &(uls->ITMHeightProfile), + &(uls->isLOSHeightProfile), + &(uls->isLOSSurfaceFrac) +#if DEBUG_AFC + , + uls->ITMHeightType +#endif + ); + + std::string rxAntennaSubModelStr; + double angleOffBoresightDeg = acos(uls->getAntennaPointing().dot( + -(lineOfSightVectorKm.normalized()))) * + 180.0 / M_PI; + double rxGainDB = uls->computeRxGain(angleOffBoresightDeg, + elevationAngleRxDeg, + chanCenterFreq, + rxAntennaSubModelStr, + 0); + + double rxPowerDBW = (_exclusionZoneRLANEIRPDBm - 30.0) - _bodyLossDB - + buildingPenetrationDB - pathLoss - pathClutterTxDB - + pathClutterRxDB + rxGainDB - spectralOverlapLossDB - + _polarizationLossDB - uls->getRxAntennaFeederLossDB(); + + double I2NDB = rxPowerDBW - uls->getNoiseLevelDBW(); + + double marginDB = _IoverN_threshold_dB - I2NDB; + + if ((rlanPosnIdx == 0) || (marginDB < minMarginDB)) { + minMarginDB = marginDB; + distKmM = distKm; + } + + if (excthrGc && *excthrGc) { + double d1; + double d2; + double pathDifference; + double fresnelIndex = -1.0; + double ulsLinkDistance = uls->getLinkDistance(); + double ulsWavelength = CConst::c / + ((uls->getStartFreq() + uls->getStopFreq()) / 2); + if (ulsLinkDistance != -1.0) { + int numPR = uls->getNumPR(); + const Vector3 ulsTxPos = (numPR ? uls->getPR(numPR - 1).positionTx : + uls->getTxPosition()); + d1 = (ulsRxPos - rlanPosn).len() * 1000; + d2 = (ulsTxPos - rlanPosn).len() * 1000; + pathDifference = d1 + d2 - ulsLinkDistance; + fresnelIndex = pathDifference / (ulsWavelength / 2); + } else { + d1 = (ulsRxPos - rlanPosn).len() * 1000; + d2 = -1.0; + pathDifference = -1.0; + } + + std::string rxAntennaTypeStr; + if (ulsRxAntennaType == CConst::LUTAntennaType) { + rxAntennaTypeStr = std::string(uls->getRxAntenna()->get_strid()); + } else { + rxAntennaTypeStr = + std::string(CConst::strULSAntennaTypeList->type_to_str( + ulsRxAntennaType)) + + rxAntennaSubModelStr; + } + + std::string bldgTypeStr = (_fixedBuildingLossFlag ? "INDOOR_FIXED" : + _buildingType == CConst::noBuildingType ? + "OUTDOOR" : + _buildingType == + CConst::traditionalBuildingType ? + "TRADITIONAL" : + "THERMALLY_EFFICIENT"); + + excthrGc->fsid = uls->getID(); + excthrGc->dbName = std::get<0>(_ulsDatabaseList[uls->getDBIdx()]); + excthrGc->rlanPosnIdx = rlanPosnIdx; + excthrGc->callsign = uls->getCallsign(); + excthrGc->fsLon = uls->getRxLongitudeDeg(); + excthrGc->fsLat = uls->getRxLatitudeDeg(); + excthrGc->fsAgl = uls->getRxHeightAboveTerrain(); + excthrGc->fsTerrainHeight = uls->getRxTerrainHeight(); + excthrGc->fsTerrainSource = _terrainDataModel->getSourceName( + uls->getRxHeightSource()); + excthrGc->fsPropEnv = ulsRxPropEnv; + excthrGc->numPr = uls->getNumPR(); + excthrGc->rlanLon = rlanCoord.longitudeDeg; + excthrGc->rlanLat = rlanCoord.latitudeDeg; + excthrGc->rlanAgl = rlanCoord.heightKm * 1000.0 - rlanTerrainHeight; + excthrGc->rlanTerrainHeight = rlanTerrainHeight; + excthrGc->rlanTerrainSource = _terrainDataModel->getSourceName( + rlanHeightSource); + excthrGc->rlanPropEnv = CConst::strPropEnvList->type_to_str(rlanPropEnv); + excthrGc->rlanFsDist = distKm; + excthrGc->rlanFsGroundDist = groundDistanceKm; + excthrGc->rlanElevAngle = elevationAngleTxDeg; + excthrGc->boresightAngle = angleOffBoresightDeg; + excthrGc->rlanTxEirp = _exclusionZoneRLANEIRPDBm; + excthrGc->bodyLoss = _bodyLossDB; + excthrGc->rlanClutterCategory = txClutterStr; + excthrGc->fsClutterCategory = rxClutterStr; + excthrGc->buildingType = bldgTypeStr; + excthrGc->buildingPenetration = buildingPenetrationDB; + excthrGc->buildingPenetrationModel = buildingPenetrationModelStr; + excthrGc->buildingPenetrationCdf = buildingPenetrationCDF; + excthrGc->pathLoss = pathLoss; + excthrGc->pathLossModel = pathLossModelStr; + excthrGc->pathLossCdf = pathLossCDF; + excthrGc->pathClutterTx = pathClutterTxDB; + excthrGc->pathClutterTxMode = pathClutterTxModelStr; + excthrGc->pathClutterTxCdf = pathClutterTxCDF; + excthrGc->pathClutterRx = pathClutterRxDB; + excthrGc->pathClutterRxMode = pathClutterRxModelStr; + excthrGc->pathClutterRxCdf = pathClutterRxCDF; + excthrGc->rlanBandwidth = bandwidth * 1.0e-6; + excthrGc->rlanStartFreq = chanStartFreq * 1.0e-6; + excthrGc->rlanStopFreq = chanStopFreq * 1.0e-6; + excthrGc->ulsStartFreq = uls->getStartFreq() * 1.0e-6; + excthrGc->ulsStopFreq = uls->getStopFreq() * 1.0e-6; + excthrGc->antType = rxAntennaTypeStr; + excthrGc->antGainPeak = uls->getRxGain(); + excthrGc->fsGainToRlan = rxGainDB; + excthrGc->spectralOverlapLoss = spectralOverlapLossDB; + excthrGc->polarizationLoss = _polarizationLossDB; + excthrGc->fsRxFeederLoss = uls->getRxAntennaFeederLossDB(); + excthrGc->fsRxPwr = rxPowerDBW; + excthrGc->fsIN = rxPowerDBW - uls->getNoiseLevelDBW(); + excthrGc->ulsLinkDist = ulsLinkDistance; + excthrGc->rlanCenterFreq = chanCenterFreq; + excthrGc->fsTxToRlanDist = d2; + excthrGc->pathDifference = pathDifference; + excthrGc->ulsWavelength = ulsWavelength * 1000; + excthrGc->fresnelIndex = fresnelIndex; + excthrGc->comment = comment; + + excthrGc->completeRow(); + } + } + + if (uls->ITMHeightProfile) { + free(uls->ITMHeightProfile); + uls->ITMHeightProfile = (double *)NULL; + } + if (uls->isLOSHeightProfile) { + free(uls->isLOSHeightProfile); + uls->isLOSHeightProfile = (double *)NULL; + } + /**************************************************************************************/ + + return (minMarginDB); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/* AfcManager::writeKML() */ +/******************************************************************************************/ +void AfcManager::writeKML() +{ + int ulsIdx; + ULSClass *uls = findULSID(_exclusionZoneFSID, 0, ulsIdx); + double rlanHeightInput = std::get<2>(_rlanLLA); + + /**************************************************************************************/ + /* Create kmlFile, useful for debugging */ + /**************************************************************************************/ + ZXmlWriter kml_writer(_kmlFile); + auto &fkml = kml_writer.xml_writer; + /**************************************************************************************/ + fkml->writeStartDocument(); + fkml->writeStartElement("kml"); + fkml->writeAttribute("xmlns", "http://www.opengis.net/kml/2.2"); + fkml->writeStartElement("Document"); + fkml->writeTextElement("name", "AFC"); + fkml->writeTextElement("open", "1"); + fkml->writeTextElement("description", "Display Exclusion Zone Analysis Results"); + fkml->writeStartElement("Style"); + fkml->writeAttribute("id", "transBluePoly"); + fkml->writeStartElement("LineStyle"); + fkml->writeTextElement("width", "1.5"); + fkml->writeEndElement(); // LineStyle + fkml->writeStartElement("PolyStyle"); + fkml->writeTextElement("color", "7dff0000"); + fkml->writeEndElement(); // PolyStyle + fkml->writeEndElement(); // Style + + std::string dbName = std::get<0>(_ulsDatabaseList[uls->getDBIdx()]); + fkml->writeStartElement("Placemark"); + fkml->writeTextElement( + "name", + QString("FSID : %1_%s").arg(QString::fromStdString(dbName)).arg(uls->getID())); + fkml->writeTextElement("visibility", "0"); + fkml->writeStartElement("Point"); + fkml->writeTextElement("extrude", "1"); + fkml->writeTextElement("altitudeMode", "absolute"); + fkml->writeTextElement("coordinates", + QString::asprintf("%12.10f,%12.10f,%5.3f", + uls->getRxLongitudeDeg(), + uls->getRxLatitudeDeg(), + _exclusionZoneFSTerrainHeight + + _exclusionZoneHeightAboveTerrain)); + fkml->writeEndElement(); // Point; + fkml->writeEndElement(); // Placemark + + fkml->writeStartElement("Placemark"); + fkml->writeTextElement("name", "Exclusion Zone"); + fkml->writeTextElement("visibility", "1"); + fkml->writeTextElement("styleUrl", "#transBluePoly"); + fkml->writeStartElement("Polygon"); + fkml->writeTextElement("altitudeMode", "clampToGround"); + fkml->writeStartElement("outerBoundaryIs"); + fkml->writeStartElement("LinearRing"); + + QString excls_coords; + int exclPtIdx; + for (exclPtIdx = 0; exclPtIdx < (int)_exclusionZone.size(); ++exclPtIdx) { + double rlanLon = std::get<0>(_exclusionZone[exclPtIdx]); + double rlanLat = std::get<1>(_exclusionZone[exclPtIdx]); + double rlanHeight; + double rlanTerrainHeight, bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum heightSource; + _terrainDataModel->getTerrainHeight(rlanLon, + rlanLat, + rlanTerrainHeight, + bldgHeight, + lidarHeightResult, + heightSource); + if (_rlanHeightType == CConst::AMSLHeightType) { + rlanHeight = rlanHeightInput; + } else if (_rlanHeightType == CConst::AGLHeightType) { + rlanHeight = rlanHeightInput + rlanTerrainHeight; + } else { + throw std::runtime_error(ErrStream() << "ERROR: INVALID _rlanHeightType = " + << _rlanHeightType); + } + excls_coords.append( + QString::asprintf("%.10f,%.10f,%.5f\n", rlanLon, rlanLat, rlanHeight)); + } + fkml->writeTextElement("coordinates", excls_coords); + fkml->writeEndElement(); // LinearRing + fkml->writeEndElement(); // outerBoundaryIs + fkml->writeEndElement(); // Polygon + fkml->writeEndElement(); // Placemark + + fkml->writeEndElement(); // Document + fkml->writeEndElement(); // kml + fkml->writeEndDocument(); + + return; +} +/******************************************************************************************/ + +inline int mkcolor(int r, int g, int b) +{ + if ((r < 0) || (r > 255)) { + CORE_DUMP; + } + if ((g < 0) || (g > 255)) { + CORE_DUMP; + } + if ((b < 0) || (b > 255)) { + CORE_DUMP; + } + + int color = (r << 16) | (g << 8) | b; + + return (color); +} + +/******************************************************************************************/ +/* AfcManager::defineHeatmapColors() */ +/******************************************************************************************/ +void AfcManager::defineHeatmapColors() +{ + bool itonFlag = (_heatmapAnalysisStr == "iton"); + + int BlackColor = mkcolor(0, 0, 0); + int WhiteColor = mkcolor(255, 255, 255); + int LightGrayColor = mkcolor(211, 211, 211); + int DarkGrayColor = mkcolor(169, 169, 169); + int BlueColor = mkcolor(0, 0, 255); + int DarkBlueColor = mkcolor(0, 0, 139); + int GreenColor = mkcolor(0, 128, 0); + int DarkGreenColor = mkcolor(0, 100, 0); + int YellowColor = mkcolor(255, 255, 0); + int OrangeColor = mkcolor(255, 165, 0); + int RedColor = mkcolor(255, 0, 0); + int MaroonColor = mkcolor(128, 0, 0); + + /**************************************************************************************/ + /* Define color scheme */ + /**************************************************************************************/ + _heatmapColorList.push_back(BlackColor); + _heatmapColorList.push_back(WhiteColor); + + if (itonFlag) { + _heatmapColorList.push_back(LightGrayColor); + _heatmapIndoorThrList.push_back(_IoverN_threshold_dB - 20.0); + _heatmapColorList.push_back(DarkGrayColor); + _heatmapIndoorThrList.push_back(_IoverN_threshold_dB); + _heatmapColorList.push_back(BlueColor); + _heatmapIndoorThrList.push_back(_IoverN_threshold_dB + 3.0); + _heatmapColorList.push_back(DarkBlueColor); + _heatmapIndoorThrList.push_back(_IoverN_threshold_dB + 6.0); + _heatmapColorList.push_back(GreenColor); + _heatmapIndoorThrList.push_back(_IoverN_threshold_dB + 9.0); + _heatmapColorList.push_back(DarkGreenColor); + _heatmapIndoorThrList.push_back(_IoverN_threshold_dB + 12.0); + _heatmapColorList.push_back(YellowColor); + _heatmapIndoorThrList.push_back(_IoverN_threshold_dB + 15.0); + _heatmapColorList.push_back(OrangeColor); + _heatmapIndoorThrList.push_back(_IoverN_threshold_dB + 18.0); + _heatmapColorList.push_back(RedColor); + _heatmapIndoorThrList.push_back(_IoverN_threshold_dB + 21.0); + _heatmapColorList.push_back(MaroonColor); + + _heatmapOutdoorThrList = _heatmapIndoorThrList; + } else { + _heatmapColorList.push_back(GreenColor); + _heatmapIndoorThrList.push_back(_IoverN_threshold_dB); + _heatmapOutdoorThrList.push_back(_IoverN_threshold_dB); + _heatmapColorList.push_back(YellowColor); + _heatmapIndoorThrList.push_back(_IoverN_threshold_dB + _maxEIRP_dBm - + _minEIRPIndoor_dBm); + _heatmapOutdoorThrList.push_back(_IoverN_threshold_dB + _maxEIRP_dBm - + _minEIRPOutdoor_dBm); + _heatmapColorList.push_back(RedColor); + } + /**************************************************************************************/ +} +/******************************************************************************************/ + +/******************************************************************************************/ +/* AfcManager::getHeatmapColor() */ +/******************************************************************************************/ +std::string AfcManager::getHeatmapColor(double itonVal, bool indoorFlag, bool hexFlag) +{ + std::vector *thrList = (indoorFlag ? &_heatmapIndoorThrList : + &_heatmapOutdoorThrList); + + int n; + if (itonVal == std::numeric_limits::infinity()) { + n = 0; + } else if (itonVal == -std::numeric_limits::infinity()) { + n = 1; + } else { + int numThr = thrList->size(); + int k; + if (itonVal < (*thrList)[0]) { + k = 0; + } else if (itonVal >= (*thrList)[numThr - 1]) { + k = numThr; + } else { + int k0 = 0; + int k1 = numThr - 1; + while (k1 > k0 + 1) { + int km = (k0 + k1) / 2; + if (itonVal < (*thrList)[km]) { + k1 = km; + } else { + k0 = km; + } + } + k = k1; + } + n = k + 2; + } + + int color = _heatmapColorList[n]; + std::string colorStr; + + if (hexFlag) { + char hexstr[7]; + sprintf(hexstr, "%06x", color); + colorStr = std::string("#") + hexstr; + } else { + colorStr = to_string((color >> 16) & 0xFF) + " " + to_string((color >> 8) & 0xFF) + + " " + to_string(color & 0xFF); + } + + return (colorStr); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/* AfcManager::runHeatmapAnalysis() */ +/******************************************************************************************/ +void AfcManager::runHeatmapAnalysis() +{ + std::ostringstream errStr; + + LOGGER_INFO(logger) << "Executing AfcManager::runHeatmapAnalysis()"; + + if (_channelList.size() != 1) { + throw std::runtime_error(ErrStream() + << "ERROR: Number of channels = " << _channelList.size() + << " require exactly 1 channel for heatmap analysis"); + } + + ChannelStruct *channel = &(_channelList[0]); + int freqSegIdx = 0; + + double chanStartFreq = channel->freqMHzList[freqSegIdx] * 1.0e6; + double chanStopFreq = channel->freqMHzList[freqSegIdx + 1] * 1.0e6; + double chanCenterFreq = (chanStartFreq + chanStopFreq) / 2; + double chanBandwidth = chanStopFreq - chanStartFreq; + bool useACI = (channel->type == INQUIRED_FREQUENCY ? false : _aciFlag); + CConst::SpectralAlgorithmEnum spectralAlgorithm = (channel->type == INQUIRED_FREQUENCY ? + CConst::psdSpectralAlgorithm : + _channelResponseAlgorithm); + + _heatmapNumPtsLat = ceil((_heatmapMaxLat - _heatmapMinLat) * M_PI / 180.0 * + CConst::earthRadius / _heatmapRLANSpacing); + + double minAbsLat; + if ((_heatmapMinLat < 0.0) && (_heatmapMaxLat > 0.0)) { + minAbsLat = 0.0; + } else { + minAbsLat = std::min(fabs(_heatmapMinLat), fabs(_heatmapMaxLat)); + } + + _heatmapNumPtsLon = ceil((_heatmapMaxLon - _heatmapMinLon) * M_PI / 180.0 * + CConst::earthRadius * cos(minAbsLat * M_PI / 180.0) / + _heatmapRLANSpacing); + + int totNumProc = _heatmapNumPtsLon * _heatmapNumPtsLat; + LOGGER_INFO(logger) << "NUM_PTS_LON: " << _heatmapNumPtsLon; + LOGGER_INFO(logger) << "NUM_PTS_LAT: " << _heatmapNumPtsLat; + LOGGER_INFO(logger) << "TOT_PTS: " << totNumProc; + + _heatmapIToNThresholdDB = _IoverN_threshold_dB; + + /**************************************************************************************/ + /* Create List of FS's that have spectral overlap */ + /**************************************************************************************/ + int ulsIdx; + for (ulsIdx = 0; ulsIdx < _ulsList->getSize(); ulsIdx++) { + ULSClass *uls = (*_ulsList)[ulsIdx]; + double spectralOverlapLossDB; + bool hasOverlap = computeSpectralOverlapLoss(&spectralOverlapLossDB, + chanStartFreq, + chanStopFreq, + uls->getStartFreq(), + uls->getStopFreq(), + useACI, + spectralAlgorithm); + if (hasOverlap) { + _ulsIdxList.push_back( + ulsIdx); // Store the ULS indices that are used in analysis + } + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Allocate / Initialize heatmap */ + /**************************************************************************************/ + _heatmapIToNDB = (double **)malloc(_heatmapNumPtsLon * sizeof(double *)); + _heatmapIsIndoor = (bool **)malloc(_heatmapNumPtsLon * sizeof(bool *)); + + int lonIdx, latIdx; + for (lonIdx = 0; lonIdx < _heatmapNumPtsLon; ++lonIdx) { + _heatmapIToNDB[lonIdx] = (double *)malloc(_heatmapNumPtsLat * sizeof(double)); + _heatmapIsIndoor[lonIdx] = (bool *)malloc(_heatmapNumPtsLat * sizeof(bool)); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Create excThrFile, useful for debugging */ + /**************************************************************************************/ + ExThrGzipCsv excthrGc(_excThrFile); + /**************************************************************************************/ + + /**************************************************************************************/ + /* Compute Heatmap */ + /**************************************************************************************/ + const double exclusionDistKmSquared = (_exclusionDist / 1000.0) * (_exclusionDist / 1000.0); + const double maxRadiusKmSquared = (_maxRadius / 1000.0) * (_maxRadius / 1000.0); + + Vector3 rlanPosnList[3]; + GeodeticCoord rlanCoordList[3]; + _heatmapMaxRLANHeightAGL = quietNaN; + +#if DEBUG_AFC + char *tstr; + + time_t tStartHeatmap = time(NULL); + tstr = strdup(ctime(&tStartHeatmap)); + strtok(tstr, "\n"); + + LOGGER_INFO(logger) << "Begin Heatmap Scan: " << tstr; + + free(tstr); + +#endif + + int numPct = 100; + + if (numPct > totNumProc) { + numPct = totNumProc; + } + + bool itonFlag = (_heatmapAnalysisStr == "iton"); + + bool initFlag = false; + int numInvalid = 0; + int numProc = 0; + for (lonIdx = 0; lonIdx < _heatmapNumPtsLon; ++lonIdx) { + double rlanLon = (_heatmapMinLon * (2 * _heatmapNumPtsLon - 2 * lonIdx - 1) + + _heatmapMaxLon * (2 * lonIdx + 1)) / + (2 * _heatmapNumPtsLon); + for (latIdx = 0; latIdx < _heatmapNumPtsLat; ++latIdx) { + double rlanLat = (_heatmapMinLat * + (2 * _heatmapNumPtsLat - 2 * latIdx - 1) + + _heatmapMaxLat * (2 * latIdx + 1)) / + (2 * _heatmapNumPtsLat); + // LOGGER_DEBUG(logger) << "Heatmap point: (" << lonIdx << ", " << latIdx << + // ")"; + +#if DEBUG_AFC + auto t1 = std::chrono::high_resolution_clock::now(); +#endif + + double rlanHeight; + double rlanTerrainHeight, bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum rlanHeightSource; + _terrainDataModel->getTerrainHeight(rlanLon, + rlanLat, + rlanTerrainHeight, + bldgHeight, + lidarHeightResult, + rlanHeightSource); + + if (_heatmapIndoorOutdoorStr == "Outdoor") { + _buildingType = CConst::noBuildingType; + } else if (_heatmapIndoorOutdoorStr == "Indoor") { + _buildingType = CConst::traditionalBuildingType; + } else if (_heatmapIndoorOutdoorStr == "Database") { + if (lidarHeightResult == + MultibandRasterClass::HeightResult::BUILDING) { + _buildingType = CConst::traditionalBuildingType; + } else { + _buildingType = CConst::noBuildingType; + } + } + + double rlanEIRP_dBm = _maxEIRP_dBm; + double rlanHeightInput, heightUncertainty; + std::string rlanHeightType; + if (_buildingType == CConst::noBuildingType) { + if (itonFlag) { + rlanEIRP_dBm = _heatmapRLANOutdoorEIRPDBm; + } + rlanHeightInput = _heatmapRLANOutdoorHeight; + heightUncertainty = _heatmapRLANOutdoorHeightUncertainty; + rlanHeightType = _heatmapRLANOutdoorHeightType; + _bodyLossDB = _bodyLossOutdoorDB; + } else { + if (itonFlag) { + rlanEIRP_dBm = _heatmapRLANIndoorEIRPDBm; + } + rlanHeightInput = _heatmapRLANIndoorHeight; + heightUncertainty = _heatmapRLANIndoorHeightUncertainty; + rlanHeightType = _heatmapRLANIndoorHeightType; + _bodyLossDB = _bodyLossIndoorDB; + } + + if (rlanHeightType == "AMSL") { + rlanHeight = rlanHeightInput; + } else if (rlanHeightType == "AGL") { + rlanHeight = rlanHeightInput + rlanTerrainHeight; + } else { + throw std::runtime_error(ErrStream() + << "ERROR: INVALID_VALUE rlanHeightType = " + << rlanHeightType); + } + + if (rlanHeight - heightUncertainty - rlanTerrainHeight < + _minRlanHeightAboveTerrain) { + throw std::runtime_error( + ErrStream() + << std::string("ERROR: Heat Map: Invalid RLAN parameter " + "settings.") + << std::endl + << std::string("RLAN Height = ") << rlanHeight << std::endl + << std::string("Height Uncertainty = ") << heightUncertainty + << std::endl + << std::string("Terrain Height at RLAN Location = ") + << rlanTerrainHeight << std::endl + << std::string("RLAN is ") + << rlanHeight - heightUncertainty - rlanTerrainHeight + << " meters above terrain" << std::endl + << std::string("RLAN must be more than ") + << _minRlanHeightAboveTerrain << " meters above terrain" + << std::endl); + } + + CConst::NLCDLandCatEnum nlcdLandCatTx; + CConst::PropEnvEnum rlanPropEnv = computePropEnv(rlanLon, + rlanLat, + nlcdLandCatTx); + + rlanCoordList[0] = GeodeticCoord::fromLatLon( + rlanLat, + rlanLon, + (rlanHeight + heightUncertainty) / 1000.0); + rlanCoordList[1] = GeodeticCoord::fromLatLon(rlanLat, + rlanLon, + rlanHeight / 1000.0); + rlanCoordList[2] = GeodeticCoord::fromLatLon( + rlanLat, + rlanLon, + (rlanHeight - heightUncertainty) / 1000.0); + + rlanPosnList[0] = EcefModel::fromGeodetic(rlanCoordList[0]); + rlanPosnList[1] = EcefModel::fromGeodetic(rlanCoordList[1]); + rlanPosnList[2] = EcefModel::fromGeodetic(rlanCoordList[2]); + + Vector3 rlanCenterPosn = rlanPosnList[1]; + if ((lonIdx == _heatmapNumPtsLon / 2) && + (latIdx == _heatmapNumPtsLat / 2)) { + _heatmapRLANCenterPosn = rlanCenterPosn; + _heatmapRLANCenterLon = rlanLon; + _heatmapRLANCenterLat = rlanLat; + } + + int numRlanPosn = ((heightUncertainty == 0.0) ? 1 : 3); + + double maxIToNDB = -std::numeric_limits::infinity(); + channel->segList[freqSegIdx] = + std::make_tuple(std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + GREEN); + + if (numRlanPosn) { + GeodeticCoord rlanCoord = rlanCoordList[0]; + double rlanHeightAGL = (rlanCoord.heightKm * 1000) - + rlanTerrainHeight; + + if (std::isnan(_heatmapMaxRLANHeightAGL) || + (rlanHeightAGL > _heatmapMaxRLANHeightAGL)) { + _heatmapMaxRLANHeightAGL = rlanHeightAGL; + } + + int drIdx; + for (drIdx = 0; drIdx < (int)_deniedRegionList.size(); ++drIdx) { + DeniedRegionClass *dr = _deniedRegionList[drIdx]; + if (dr->intersect(rlanCoord.longitudeDeg, + rlanCoord.latitudeDeg, + 0.0, + rlanHeightAGL)) { + if (std::get<2>(channel->segList[freqSegIdx]) != + BLACK) { + bool hasOverlap = computeSpectralOverlapLoss( + (double *)NULL, + chanStartFreq, + chanStopFreq, + dr->getStartFreq(), + dr->getStopFreq(), + false, + CConst::psdSpectralAlgorithm); + if (hasOverlap) { + channel->segList[freqSegIdx] = + std::make_tuple( + -std::numeric_limits< + double>:: + infinity(), + -std::numeric_limits< + double>:: + infinity(), + BLACK); + maxIToNDB = std::numeric_limits< + double>::infinity(); + } + } + } + } + } + + int uIdx; + for (uIdx = 0; (uIdx < (int)_ulsIdxList.size()) && + (maxIToNDB != std::numeric_limits::infinity()); + uIdx++) { + ulsIdx = _ulsIdxList[uIdx]; + ULSClass *uls = (*_ulsList)[ulsIdx]; + + int numPR = uls->getNumPR(); + int numDiversity = (uls->getHasDiversity() ? 2 : 1); + + int segStart = (_passiveRepeaterFlag ? 0 : numPR); + + for (int segIdx = segStart; segIdx < numPR + 1; ++segIdx) { + for (int divIdx = 0; divIdx < numDiversity; ++divIdx) { + Vector3 ulsRxPos = + (segIdx == numPR ? + (divIdx == 0 ? + uls->getRxPosition() : + uls->getDiversityPosition()) : + uls->getPR(segIdx).positionRx); + double ulsRxLongitude = + (segIdx == numPR ? + uls->getRxLongitudeDeg() : + uls->getPR(segIdx).longitudeDeg); + double ulsRxLatitude = + (segIdx == numPR ? + uls->getRxLatitudeDeg() : + uls->getPR(segIdx).latitudeDeg); + + Vector3 lineOfSightVectorKm = ulsRxPos - + rlanCenterPosn; + double distKmSquared = + (lineOfSightVectorKm) + .dot(lineOfSightVectorKm); + +#if 0 + // For debugging, identifies anomalous ULS entries + if (uls->getLinkDistance() == -1) { + std::string dbName = std::get<0>(_ulsDatabaseList[uls->getDBIdx()]); + std::cout << dbName << "_" << uls->getID() << std::endl; + } +#endif + + if (distKmSquared <= exclusionDistKmSquared) { + channel->segList[freqSegIdx] = + std::make_tuple( + -std::numeric_limits< + double>::infinity(), + -std::numeric_limits< + double>::infinity(), + BLACK); + maxIToNDB = std::numeric_limits< + double>::infinity(); + } else if (distKmSquared < maxRadiusKmSquared) { + double ulsRxHeightAGL = + (segIdx == numPR ? + (divIdx == 0 ? + uls->getRxHeightAboveTerrain() : + uls->getDiversityHeightAboveTerrain()) : + uls->getPR(segIdx) + .heightAboveTerrainRx); + double ulsRxHeightAMSL = + (segIdx == numPR ? + (divIdx == 0 ? + uls->getRxHeightAMSL() : + uls->getDiversityHeightAMSL()) : + uls->getPR(segIdx) + .heightAMSLRx); + double ulsSegmentDistance = + (segIdx == numPR ? + uls->getLinkDistance() : + uls->getPR(segIdx) + .segmentDistance); + + /**************************************************************************************/ + /* Determine propagation environment of FS + * segment RX, if needed. */ + /**************************************************************************************/ + char ulsRxPropEnv = ' '; + CConst::NLCDLandCatEnum nlcdLandCatRx; + CConst::PropEnvEnum fsPropEnv; + if ((_applyClutterFSRxFlag) && + (ulsRxHeightAGL <= _maxFsAglHeight)) { + fsPropEnv = computePropEnv( + ulsRxLongitude, + ulsRxLatitude, + nlcdLandCatRx); + switch (fsPropEnv) { + case CConst::urbanPropEnv: + ulsRxPropEnv = 'U'; + break; + case CConst:: + suburbanPropEnv: + ulsRxPropEnv = 'S'; + break; + case CConst::ruralPropEnv: + ulsRxPropEnv = 'R'; + break; + case CConst::barrenPropEnv: + ulsRxPropEnv = 'B'; + break; + case CConst::unknownPropEnv: + ulsRxPropEnv = 'X'; + break; + default: + CORE_DUMP; + } + } else { + fsPropEnv = CConst::unknownPropEnv; + ulsRxPropEnv = ' '; + } + /**************************************************************************************/ + + Vector3 ulsAntennaPointing = + (segIdx == numPR ? + (divIdx == 0 ? + uls->getAntennaPointing() : + uls->getDiversityAntennaPointing()) : + uls->getPR(segIdx) + .pointing); + + // Use Haversine formula with average earth + // radius of 6371 km + double groundDistanceKm; + { + double lon1Rad = rlanLon * M_PI / + 180.0; + double lat1Rad = rlanLat * M_PI / + 180.0; + double lon2Rad = ulsRxLongitude * + M_PI / 180.0; + double lat2Rad = ulsRxLatitude * + M_PI / 180.0; + double slat = sin( + (lat2Rad - lat1Rad) / 2); + double slon = sin( + (lon2Rad - lon1Rad) / 2); + groundDistanceKm = + 2 * + CConst::averageEarthRadius * + asin(sqrt( + slat * slat + + cos(lat1Rad) * + cos(lat2Rad) * + slon * + slon)) * + 1.0e-3; + } + + for (int rlanHtIdx = 0; + rlanHtIdx < numRlanPosn; + ++rlanHtIdx) { + Vector3 rlanPosn = + rlanPosnList[rlanHtIdx]; + GeodeticCoord rlanCoord = + rlanCoordList[rlanHtIdx]; + lineOfSightVectorKm = ulsRxPos - + rlanPosn; + double distKm = + lineOfSightVectorKm.len(); + double win2DistKm; + if (_winner2UseGroundDistanceFlag) { + win2DistKm = + groundDistanceKm; + } else { + win2DistKm = distKm; + } + double fsplDistKm; + if (_fsplUseGroundDistanceFlag) { + fsplDistKm = + groundDistanceKm; + } else { + fsplDistKm = distKm; + } + + double dAP = rlanPosn.len(); + double duls = ulsRxPos.len(); + double elevationAngleTxDeg = + 90.0 - + acos(rlanPosn.dot( + lineOfSightVectorKm) / + (dAP * distKm)) * + 180.0 / M_PI; + double elevationAngleRxDeg = + 90.0 - + acos(ulsRxPos.dot( + -lineOfSightVectorKm) / + (duls * distKm)) * + 180.0 / M_PI; + + double rlanAngleOffBoresightRad; + double rlanDiscriminationGainDB; + if (_rlanAntenna) { + double cosAOB = + _rlanPointing.dot( + lineOfSightVectorKm) / + distKm; + if (cosAOB > 1.0) { + cosAOB = 1.0; + } else if (cosAOB < -1.0) { + cosAOB = -1.0; + } + rlanAngleOffBoresightRad = + acos(cosAOB); + rlanDiscriminationGainDB = + _rlanAntenna->gainDB( + rlanAngleOffBoresightRad); + } else { + rlanAngleOffBoresightRad = + 0.0; + rlanDiscriminationGainDB = + 0.0; + } + + double spectralOverlapLossDB; + bool hasOverlap = + computeSpectralOverlapLoss( + &spectralOverlapLossDB, + chanStartFreq, + chanStopFreq, + uls->getStartFreq(), + uls->getStopFreq(), + useACI, + spectralAlgorithm); + if (hasOverlap) { + std::string + buildingPenetrationModelStr; + double buildingPenetrationCDF; + double buildingPenetrationDB = computeBuildingPenetration( + _buildingType, + elevationAngleTxDeg, + chanCenterFreq, + buildingPenetrationModelStr, + buildingPenetrationCDF); + + std::string txClutterStr; + std::string rxClutterStr; + std::string + pathLossModelStr; + double pathLossCDF; + double pathLoss; + std::string + pathClutterTxModelStr; + double pathClutterTxCDF; + double pathClutterTxDB; + std::string + pathClutterRxModelStr; + double pathClutterRxCDF; + double pathClutterRxDB; + double rxGainDB; + double discriminationGain; + std::string + rxAntennaSubModelStr; + double angleOffBoresightDeg; + double rxPowerDBW; + double I2NDB; + double marginDB; + double eirpLimit_dBm; + double nearFieldOffsetDB; + double nearField_xdb; + double nearField_u; + double nearField_eff; + double reflectorD0; + double reflectorD1; + + double rlanHtAboveTerrain = + rlanCoord.heightKm * + 1000.0 - + rlanTerrainHeight; + + computePathLoss( + _pathLossModel, + false, + rlanPropEnv, + fsPropEnv, + nlcdLandCatTx, + nlcdLandCatRx, + distKm, + fsplDistKm, + win2DistKm, + chanCenterFreq, + rlanCoord + .longitudeDeg, + rlanCoord + .latitudeDeg, + rlanHtAboveTerrain, + elevationAngleTxDeg, + uls->getRxLongitudeDeg(), + uls->getRxLatitudeDeg(), + uls->getRxHeightAboveTerrain(), + elevationAngleRxDeg, + pathLoss, + pathClutterTxDB, + pathClutterRxDB, + pathLossModelStr, + pathLossCDF, + pathClutterTxModelStr, + pathClutterTxCDF, + pathClutterRxModelStr, + pathClutterRxCDF, + &txClutterStr, + &rxClutterStr, + &(uls->ITMHeightProfile), + &(uls->isLOSHeightProfile), + &(uls->isLOSSurfaceFrac) +#if DEBUG_AFC + , + uls->ITMHeightType +#endif + ); + + angleOffBoresightDeg = + acos(uls->getAntennaPointing() + .dot(-(lineOfSightVectorKm + .normalized()))) * + 180.0 / M_PI; + if (segIdx == numPR) { + rxGainDB = uls->computeRxGain( + angleOffBoresightDeg, + elevationAngleRxDeg, + chanCenterFreq, + rxAntennaSubModelStr, + divIdx); + } else { + discriminationGain = + uls->getPR(segIdx) + .computeDiscriminationGain( + angleOffBoresightDeg, + elevationAngleRxDeg, + chanCenterFreq, + reflectorD0, + reflectorD1); + rxGainDB = + uls->getPR(segIdx) + .effectiveGain + + discriminationGain; + } + + nearFieldOffsetDB = 0.0; + nearField_xdb = quietNaN; + nearField_u = quietNaN; + nearField_eff = quietNaN; + if (segIdx == numPR) { + if (_nearFieldAdjFlag && + (distKm * + 1000.0 < + uls->getRxNearFieldDistLimit()) && + (angleOffBoresightDeg < + 90.0)) { + bool unii5Flag = computeSpectralOverlapLoss( + (double *) + NULL, + uls->getStartFreq(), + uls->getStopFreq(), + 5925.0e6, + 6425.0e6, + false, + CConst::psdSpectralAlgorithm); + double Fc; + if (unii5Flag) { + Fc = 6175.0e6; + } else { + Fc = 6700.0e6; + } + nearField_eff = + uls->getRxNearFieldAntEfficiency(); + double D = + uls->getRxNearFieldAntDiameter(); + + nearField_xdb = + 10.0 * + log10(CConst::c * + distKm * + 1000.0 / + (2 * + Fc * + D * + D)); + nearField_u = + (Fc * + D * + sin(angleOffBoresightDeg * + M_PI / + 180.0) / + CConst::c); + + nearFieldOffsetDB = _nfa->computeNFA( + nearField_xdb, + nearField_u, + nearField_eff); + } + } + + rxPowerDBW = + (rlanEIRP_dBm - + 30.0) + + rlanDiscriminationGainDB - + _bodyLossDB - + buildingPenetrationDB - + pathLoss - + pathClutterTxDB - + pathClutterRxDB + + rxGainDB + + nearFieldOffsetDB - + spectralOverlapLossDB - + _polarizationLossDB - + uls->getRxAntennaFeederLossDB(); + + I2NDB = rxPowerDBW - + uls->getNoiseLevelDBW(); + + if ((maxIToNDB == + -std::numeric_limits< + double>:: + infinity()) || + (I2NDB > maxIToNDB)) { + maxIToNDB = I2NDB; + _heatmapIsIndoor[lonIdx][latIdx] = + (_buildingType == + CConst::noBuildingType ? + false : + true); + } + + if (excthrGc && + (std::isnan( + rxPowerDBW) || + (I2NDB > + _visibilityThreshold) || + (distKm * 1000 < + _closeInDist))) { + double d1; + double d2; + double pathDifference; + double fresnelIndex = + -1.0; + double ulsLinkDistance = + uls->getLinkDistance(); + double ulsWavelength = + CConst::c / + ((uls->getStartFreq() + + uls->getStopFreq()) / + 2); + if (ulsSegmentDistance != + -1.0) { + const Vector3 ulsTxPos = + (segIdx ? + uls->getPR(segIdx - + 1) + .positionTx : + uls->getTxPosition()); + d1 = (ulsRxPos - + rlanPosn) + .len() * + 1000; + d2 = (ulsTxPos - + rlanPosn) + .len() * + 1000; + pathDifference = + d1 + + d2 - + ulsSegmentDistance; + fresnelIndex = + pathDifference / + (ulsWavelength / + 2); + } else { + d1 = (ulsRxPos - + rlanPosn) + .len() * + 1000; + d2 = -1.0; + pathDifference = + -1.0; + } + + std::string + rxAntennaTypeStr; + if (segIdx == + numPR) { + CConst::ULSAntennaTypeEnum ulsRxAntennaType = + uls->getRxAntennaType(); + if (ulsRxAntennaType == + CConst::LUTAntennaType) { + rxAntennaTypeStr = std::string( + uls->getRxAntenna() + ->get_strid()); + } else { + rxAntennaTypeStr = + std::string( + CConst::strULSAntennaTypeList + ->type_to_str( + ulsRxAntennaType)) + + rxAntennaSubModelStr; + } + } else { + if (uls->getPR(segIdx) + .type == + CConst::backToBackAntennaPRType) { + CConst::ULSAntennaTypeEnum ulsRxAntennaType = + uls->getPR(segIdx) + .antennaType; + if (ulsRxAntennaType == + CConst::LUTAntennaType) { + rxAntennaTypeStr = std::string( + uls->getPR(segIdx) + .antenna + ->get_strid()); + } else { + rxAntennaTypeStr = + std::string( + CConst::strULSAntennaTypeList + ->type_to_str( + ulsRxAntennaType)) + + rxAntennaSubModelStr; + } + } else { + rxAntennaTypeStr = + ""; + } + } + + std::string bldgTypeStr = + (_fixedBuildingLossFlag ? + "I" + "N" + "D" + "O" + "O" + "R" + "_" + "F" + "I" + "X" + "E" + "D" : + _buildingType == + CConst::noBuildingType ? + "O" + "U" + "T" + "D" + "O" + "O" + "R" : + _buildingType == + CConst::traditionalBuildingType ? + "T" + "R" + "A" + "D" + "I" + "T" + "I" + "O" + "N" + "A" + "L" : + "T" + "H" + "E" + "R" + "M" + "A" + "L" + "L" + "Y" + "_" + "E" + "F" + "F" + "I" + "C" + "I" + "E" + "N" + "T"); + + excthrGc.fsid = + uls->getID(); + excthrGc.region = + uls->getRegion(); + excthrGc.dbName = std::get< + 0>( + _ulsDatabaseList + [uls->getDBIdx()]); + excthrGc.rlanPosnIdx = + rlanHtIdx; + excthrGc.callsign = + uls->getCallsign(); + excthrGc.fsLon = + uls->getRxLongitudeDeg(); + excthrGc.fsLat = + uls->getRxLatitudeDeg(); + excthrGc.fsAgl = + divIdx == 0 ? + uls->getRxHeightAboveTerrain() : + uls->getDiversityHeightAboveTerrain(); + excthrGc.fsTerrainHeight = + uls->getRxTerrainHeight(); + excthrGc.fsTerrainSource = + _terrainDataModel + ->getSourceName( + uls->getRxHeightSource()); + excthrGc.fsPropEnv = + ulsRxPropEnv; + excthrGc.numPr = + uls->getNumPR(); + excthrGc.divIdx = + divIdx; + excthrGc.segIdx = + segIdx; + excthrGc.segRxLon = + ulsRxLongitude; + excthrGc.segRxLat = + ulsRxLatitude; + + if ((segIdx < + numPR) && + (uls->getPR(segIdx) + .type == + CConst::billboardReflectorPRType)) { + PRClass &pr = uls->getPR( + segIdx); + excthrGc.refThetaIn = + pr.reflectorThetaIN; + excthrGc.refKs = + pr.reflectorKS; + excthrGc.refQ = + pr.reflectorQ; + excthrGc.refD0 = + reflectorD0; + excthrGc.refD1 = + reflectorD1; + } + + excthrGc.rlanLon = + rlanCoord + .longitudeDeg; + excthrGc.rlanLat = + rlanCoord + .latitudeDeg; + excthrGc.rlanAgl = + rlanCoord.heightKm * + 1000.0 - + rlanTerrainHeight; + excthrGc.rlanTerrainHeight = + rlanTerrainHeight; + excthrGc.rlanTerrainSource = + _terrainDataModel + ->getSourceName( + rlanHeightSource); + excthrGc.rlanPropEnv = + CConst::strPropEnvList + ->type_to_str( + rlanPropEnv); + excthrGc.rlanFsDist = + distKm; + excthrGc.rlanFsGroundDist = + groundDistanceKm; + excthrGc.rlanElevAngle = + elevationAngleTxDeg; + excthrGc.boresightAngle = + angleOffBoresightDeg; + excthrGc.rlanTxEirp = + rlanEIRP_dBm; + if (_rlanAntenna) { + excthrGc.rlanAntennaModel = + _rlanAntenna + ->get_strid(); + excthrGc.rlanAOB = + rlanAngleOffBoresightRad * + 180.0 / + M_PI; + } else { + excthrGc.rlanAntennaModel = + ""; + excthrGc.rlanAOB = + -1.0; + } + excthrGc.rlanDiscriminationGainDB = + rlanDiscriminationGainDB; + excthrGc.bodyLoss = + _bodyLossDB; + excthrGc.rlanClutterCategory = + txClutterStr; + excthrGc.fsClutterCategory = + rxClutterStr; + excthrGc.buildingType = + bldgTypeStr; + excthrGc.buildingPenetration = + buildingPenetrationDB; + excthrGc.buildingPenetrationModel = + buildingPenetrationModelStr; + excthrGc.buildingPenetrationCdf = + buildingPenetrationCDF; + excthrGc.pathLoss = + pathLoss; + excthrGc.pathLossModel = + pathLossModelStr; + excthrGc.pathLossCdf = + pathLossCDF; + excthrGc.pathClutterTx = + pathClutterTxDB; + excthrGc.pathClutterTxMode = + pathClutterTxModelStr; + excthrGc.pathClutterTxCdf = + pathClutterTxCDF; + excthrGc.pathClutterRx = + pathClutterRxDB; + excthrGc.pathClutterRxMode = + pathClutterRxModelStr; + excthrGc.pathClutterRxCdf = + pathClutterRxCDF; + excthrGc.rlanBandwidth = + (chanStopFreq - + chanStartFreq) * + 1.0e-6; + excthrGc.rlanStartFreq = + chanStartFreq * + 1.0e-6; + excthrGc.rlanStopFreq = + chanStopFreq * + 1.0e-6; + excthrGc.ulsStartFreq = + uls->getStartFreq() * + 1.0e-6; + excthrGc.ulsStopFreq = + uls->getStopFreq() * + 1.0e-6; + excthrGc.antType = + rxAntennaTypeStr; + excthrGc.antCategory = + CConst::strAntennaCategoryList + ->type_to_str( + segIdx == numPR ? + uls->getRxAntennaCategory() : + uls->getPR(segIdx) + .antCategory); + excthrGc.antGainPeak = + uls->getRxGain(); + + if (segIdx != + numPR) { + excthrGc.prType = + CConst::strPRTypeList + ->type_to_str( + uls->getPR(segIdx) + .type); + excthrGc.prEffectiveGain = + uls->getPR(segIdx) + .effectiveGain; + excthrGc.prDiscrinminationGain = + discriminationGain; + } + + excthrGc.fsGainToRlan = + rxGainDB; + if (!std::isnan( + nearField_xdb)) { + excthrGc.fsNearFieldXdb = + nearField_xdb; + } + if (!std::isnan( + nearField_u)) { + excthrGc.fsNearFieldU = + nearField_u; + } + if (!std::isnan( + nearField_eff)) { + excthrGc.fsNearFieldEff = + nearField_eff; + } + excthrGc.fsNearFieldOffset = + nearFieldOffsetDB; + excthrGc.spectralOverlapLoss = + spectralOverlapLossDB; + excthrGc.polarizationLoss = + _polarizationLossDB; + excthrGc.fsRxFeederLoss = + uls->getRxAntennaFeederLossDB(); + excthrGc.fsRxPwr = + rxPowerDBW; + excthrGc.fsIN = + I2NDB; + // excthrGc.eirpLimit + // = eirpLimit_dBm; + excthrGc.fsSegDist = + ulsSegmentDistance; + excthrGc.rlanCenterFreq = + chanCenterFreq; + excthrGc.fsTxToRlanDist = + d2; + excthrGc.pathDifference = + pathDifference; + excthrGc.ulsWavelength = + ulsWavelength * + 1000; + excthrGc.fresnelIndex = + fresnelIndex; + + excthrGc.completeRow(); + } + } + } + + if (uls->ITMHeightProfile) { + free(uls->ITMHeightProfile); + uls->ITMHeightProfile = (double *) + NULL; + } + if (uls->isLOSHeightProfile) { + free(uls->isLOSHeightProfile); + uls->isLOSHeightProfile = (double *) + NULL; + } + } + } + } + } + _heatmapIToNDB[lonIdx][latIdx] = maxIToNDB; + + if (maxIToNDB == -std::numeric_limits::infinity()) { + numInvalid++; + if (numInvalid <= 100) { + errStr << "At position LON = " << rlanLon + << " LAT = " << rlanLat + << " there are no FS receivers within " + << (_maxRadius / 1000) + << " Km of RLAN that have spectral overlap with " + "RLAN"; + LOGGER_INFO(logger) << errStr.str(); + errStr.str(""); + errStr.clear(); + } + } + + if (!initFlag) { + _heatmapMinIToNDB = maxIToNDB; + _heatmapMaxIToNDB = maxIToNDB; + initFlag = true; + } else if (maxIToNDB < _heatmapMinIToNDB) { + _heatmapMinIToNDB = maxIToNDB; + } else if (maxIToNDB > _heatmapMaxIToNDB) { + _heatmapMaxIToNDB = maxIToNDB; + } + + numProc++; + +#if DEBUG_AFC + auto t2 = std::chrono::high_resolution_clock::now(); + + std::cout << " [" << numProc << " / " << totNumProc << "] " + << " Elapsed Time = " << std::setprecision(6) + << std::chrono::duration_cast>(t2 - + t1) + .count() + << std::endl + << std::flush; + +#endif + } + } + + if (numInvalid) { + errStr << "There were a total of " << numInvalid + << " RLAN locations for which there are no FS receivers within " + << (_maxRadius / 1000) << " Km that have nonzero spectral overlap" + << std::endl; + LOGGER_WARN(logger) << errStr.str(); + statusMessageList.push_back(errStr.str()); + errStr.str(""); + errStr.clear(); + } + +#if DEBUG_AFC + time_t tEndHeatmap = time(NULL); + tstr = strdup(ctime(&tEndHeatmap)); + strtok(tstr, "\n"); + + int elapsedTime = (int)(tEndHeatmap - tStartHeatmap); + + int et = elapsedTime; + int elapsedTimeSec = et % 60; + et = et / 60; + int elapsedTimeMin = et % 60; + et = et / 60; + int elapsedTimeHour = et % 24; + et = et / 24; + int elapsedTimeDay = et; + + LOGGER_INFO(logger) << "End Processing Heatmap " << tstr + << " Elapsed time = " << (tEndHeatmap - tStartHeatmap) + << " sec = " << elapsedTimeDay << " days " << elapsedTimeHour + << " hours " << elapsedTimeMin << " min " << elapsedTimeSec << " sec."; + + free(tstr); +#endif + /**************************************************************************************/ + + /**************************************************************************************/ + /* Open KML File and write header */ + /**************************************************************************************/ + FILE *fkml = (FILE *)NULL; + if (!(fkml = fopen("/tmp/doc.kml", "wb"))) { + throw std::runtime_error("ERROR"); + } + + if (fkml) { + fprintf(fkml, "\n"); + fprintf(fkml, "\n"); + fprintf(fkml, "\n"); + fprintf(fkml, "AFC Heatmap\n"); + fprintf(fkml, "1\n"); + fprintf(fkml, "Display Heatmap Analysis Results\n"); + fprintf(fkml, "\n"); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* KML Header */ + /**************************************************************************************/ + if (fkml) { + fprintf(fkml, " \n"); + + fprintf(fkml, " \n"); + + fprintf(fkml, " \n"); + + fprintf(fkml, " \n"); + + fprintf(fkml, " \n"); + + fprintf(fkml, " \n"); + + fprintf(fkml, " \n"); + + fprintf(fkml, " \n"); + + fprintf(fkml, " \n"); + + fprintf(fkml, " \n"); + + fprintf(fkml, " \n"); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* KML Show FS */ + /**************************************************************************************/ + if (fkml) { + fprintf(fkml, " \n"); + fprintf(fkml, " FS\n"); + for (int uIdx = 0; uIdx < (int)_ulsIdxList.size(); uIdx++) { + ulsIdx = _ulsIdxList[uIdx]; + ULSClass *uls = (*_ulsList)[ulsIdx]; + std::string dbName = std::get<0>(_ulsDatabaseList[uls->getDBIdx()]); + int addPlacemarks = 1; + std::string placemarkStyleStr = "#yellowPlacemark"; + std::string polyStyleStr = "#yellowPoly"; + std::string visibilityStr = "1"; + + fprintf(fkml, " \n"); + fprintf(fkml, + " %s_%d\n", + dbName.c_str(), + uls->getID()); + + int numPR = uls->getNumPR(); + for (int segIdx = 0; segIdx < numPR + 1; ++segIdx) { + Vector3 ulsTxPosn = (segIdx == 0 ? + uls->getTxPosition() : + uls->getPR(segIdx - 1).positionTx); + double ulsTxLongitude = + (segIdx == 0 ? uls->getTxLongitudeDeg() : + uls->getPR(segIdx - 1).longitudeDeg); + double ulsTxLatitude = (segIdx == 0 ? + uls->getTxLatitudeDeg() : + uls->getPR(segIdx - 1).latitudeDeg); + double ulsTxHeight = (segIdx == 0 ? + uls->getTxHeightAMSL() : + uls->getPR(segIdx - 1).heightAMSLTx); + + Vector3 ulsRxPosn = (segIdx == numPR ? + uls->getRxPosition() : + uls->getPR(segIdx).positionRx); + double ulsRxLongitude = (segIdx == numPR ? + uls->getRxLongitudeDeg() : + uls->getPR(segIdx).longitudeDeg); + double ulsRxLatitude = (segIdx == numPR ? + uls->getRxLatitudeDeg() : + uls->getPR(segIdx).latitudeDeg); + double ulsRxHeight = (segIdx == numPR ? + uls->getRxHeightAMSL() : + uls->getPR(segIdx).heightAMSLRx); + + bool txLocFlag = (!std::isnan(ulsTxPosn.x())) && + (!std::isnan(ulsTxPosn.y())) && + (!std::isnan(ulsTxPosn.z())); + + double linkDistKm; + if (!txLocFlag) { + linkDistKm = 1.0; + Vector3 segPointing = (segIdx == numPR ? + uls->getAntennaPointing() : + uls->getPR(segIdx).pointing); + ulsTxPosn = ulsRxPosn + linkDistKm * segPointing; + } else { + linkDistKm = (ulsTxPosn - ulsRxPosn).len(); + } + + if ((segIdx == 0) && (addPlacemarks) && (txLocFlag)) { + fprintf(fkml, " \n"); + fprintf(fkml, + " %s %s_%d\n", + "TX", + dbName.c_str(), + uls->getID()); + fprintf(fkml, + " 1\n"); + fprintf(fkml, + " %s\n", + placemarkStyleStr.c_str()); + fprintf(fkml, " \n"); + fprintf(fkml, + " " + "absolute\n"); + fprintf(fkml, + " " + "%.10f,%.10f,%.2f\n", + ulsTxLongitude, + ulsTxLatitude, + ulsTxHeight); + fprintf(fkml, " \n"); + fprintf(fkml, " \n"); + } + + double beamWidthDeg = uls->computeBeamWidth(3.0); + double beamWidthRad = beamWidthDeg * (M_PI / 180.0); + + Vector3 zvec = (ulsTxPosn - ulsRxPosn).normalized(); + Vector3 xvec = (Vector3(zvec.y(), -zvec.x(), 0.0)).normalized(); + Vector3 yvec = zvec.cross(xvec); + + int numCvgPoints = 32; + + std::vector ptList; + double cvgTheta = beamWidthRad; + int cvgPhiIdx; + for (cvgPhiIdx = 0; cvgPhiIdx < numCvgPoints; ++cvgPhiIdx) { + double cvgPhi = 2 * M_PI * cvgPhiIdx / numCvgPoints; + Vector3 cvgIntPosn = ulsRxPosn + + linkDistKm * (zvec * cos(cvgTheta) + + (xvec * cos(cvgPhi) + + yvec * sin(cvgPhi)) * + sin(cvgTheta)); + + GeodeticCoord cvgIntPosnGeodetic = + EcefModel::ecefToGeodetic(cvgIntPosn); + ptList.push_back(cvgIntPosnGeodetic); + } + if (addPlacemarks) { + std::string nameStr; + if (segIdx == numPR) { + nameStr = "RX"; + } else { + nameStr = "PR " + std::to_string(segIdx + 1); + ; + } + fprintf(fkml, " \n"); + fprintf(fkml, + " %s %s_%d\n", + nameStr.c_str(), + dbName.c_str(), + uls->getID()); + fprintf(fkml, + " 1\n"); + fprintf(fkml, + " %s\n", + placemarkStyleStr.c_str()); + fprintf(fkml, " \n"); + fprintf(fkml, + " " + "absolute\n"); + fprintf(fkml, + " " + "%.10f,%.10f,%.2f\n", + ulsRxLongitude, + ulsRxLatitude, + ulsRxHeight); + fprintf(fkml, " \n"); + fprintf(fkml, " \n"); + } + + if (true) { + fprintf(fkml, " \n"); + fprintf(fkml, + " Beamcone_%d/n", + segIdx + 1); + + for (cvgPhiIdx = 0; cvgPhiIdx < numCvgPoints; ++cvgPhiIdx) { + fprintf(fkml, " \n"); + fprintf(fkml, + " p%d\n", + cvgPhiIdx); + fprintf(fkml, + " " + "%s\n", + visibilityStr.c_str()); + fprintf(fkml, + " %s\n", + placemarkStyleStr.c_str()); + fprintf(fkml, " \n"); + fprintf(fkml, + " " + "0\n"); + fprintf(fkml, + " " + "absolute\n"); + fprintf(fkml, + " \n"); + fprintf(fkml, + " \n"); + fprintf(fkml, + " " + "\n"); + + fprintf(fkml, + "%.10f,%.10f,%.2f\n", + ulsRxLongitude, + ulsRxLatitude, + ulsRxHeight); + + GeodeticCoord pt = ptList[cvgPhiIdx]; + fprintf(fkml, + "%.10f,%.10f,%.2f\n", + pt.longitudeDeg, + pt.latitudeDeg, + pt.heightKm * 1000.0); + + pt = ptList[(cvgPhiIdx + 1) % numCvgPoints]; + fprintf(fkml, + "%.10f,%.10f,%.2f\n", + pt.longitudeDeg, + pt.latitudeDeg, + pt.heightKm * 1000.0); + + fprintf(fkml, + "%.10f,%.10f,%.2f\n", + ulsRxLongitude, + ulsRxLatitude, + ulsRxHeight); + + fprintf(fkml, + " " + "\n"); + fprintf(fkml, + " \n"); + fprintf(fkml, + " \n"); + fprintf(fkml, " \n"); + fprintf(fkml, " \n"); + } + fprintf(fkml, " \n"); + } + } + fprintf(fkml, " \n"); + } + fprintf(fkml, " \n"); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* KML Show Denied Regions */ + /**************************************************************************************/ + if (fkml) { + fprintf(fkml, " \n"); + fprintf(fkml, " Denied Region\n"); + + int drIdx; + for (drIdx = 0; drIdx < (int)_deniedRegionList.size(); ++drIdx) { + DeniedRegionClass *dr = _deniedRegionList[drIdx]; + DeniedRegionClass::TypeEnum drType = dr->getType(); + std::string pfx; + switch (drType) { + case DeniedRegionClass::RASType: + pfx = "RAS_"; + break; + case DeniedRegionClass::userSpecifiedType: + pfx = "USER_SPEC_"; + break; + default: + CORE_DUMP; + break; + } + + fprintf(fkml, " \n"); + fprintf(fkml, + " %s_%d\n", + pfx.c_str(), + dr->getID()); + + int numPtsCircle = 32; + int rectIdx, numRect; + double rectLonStart, rectLonStop, rectLatStart, rectLatStop; + double circleRadius, longitudeCenter, latitudeCenter; + double drTerrainHeight, drBldgHeight, drHeightAGL; + Vector3 drCenterPosn; + Vector3 drUpVec; + Vector3 drEastVec; + Vector3 drNorthVec; + QString dr_coords; + MultibandRasterClass::HeightResult drLidarHeightResult; + CConst::HeightSourceEnum drHeightSource; + DeniedRegionClass::GeometryEnum drGeometry = dr->getGeometry(); + switch (drGeometry) { + case DeniedRegionClass::rectGeometry: + case DeniedRegionClass::rect2Geometry: + numRect = ((RectDeniedRegionClass *)dr)->getNumRect(); + for (rectIdx = 0; rectIdx < numRect; rectIdx++) { + std::tie(rectLonStart, + rectLonStop, + rectLatStart, + rectLatStop) = + ((RectDeniedRegionClass *)dr) + ->getRect(rectIdx); + + fprintf(fkml, " \n"); + fprintf(fkml, + " " + "RECT_%d\n", + rectIdx); + fprintf(fkml, + " " + "1\n"); + fprintf(fkml, + " " + "#transBluePoly\n"); + fprintf(fkml, " \n"); + fprintf(fkml, + " " + "0\n"); + fprintf(fkml, + " " + "0\n"); + fprintf(fkml, + " " + "clampToGround\n"); + fprintf(fkml, + " " + "\n"); + fprintf(fkml, + " " + "\n"); + fprintf(fkml, + " " + "\n"); + + fprintf(fkml, + "%.10f,%.10f,%.2f\n", + rectLonStart, + rectLatStart, + 0.0); + fprintf(fkml, + "%.10f,%.10f,%.2f\n", + rectLonStop, + rectLatStart, + 0.0); + fprintf(fkml, + "%.10f,%.10f,%.2f\n", + rectLonStop, + rectLatStop, + 0.0); + fprintf(fkml, + "%.10f,%.10f,%.2f\n", + rectLonStart, + rectLatStop, + 0.0); + fprintf(fkml, + "%.10f,%.10f,%.2f\n", + rectLonStart, + rectLatStart, + 0.0); + + fprintf(fkml, + " " + "\n"); + fprintf(fkml, + " " + "\n"); + fprintf(fkml, + " " + "\n"); + fprintf(fkml, " \n"); + fprintf(fkml, " \n"); + } + break; + case DeniedRegionClass::circleGeometry: + case DeniedRegionClass::horizonDistGeometry: + circleRadius = ((CircleDeniedRegionClass *)dr) + ->computeRadius( + _heatmapMaxRLANHeightAGL); + longitudeCenter = ((CircleDeniedRegionClass *)dr) + ->getLongitudeCenter(); + latitudeCenter = ((CircleDeniedRegionClass *)dr) + ->getLatitudeCenter(); + drHeightAGL = dr->getHeightAGL(); + _terrainDataModel->getTerrainHeight(longitudeCenter, + latitudeCenter, + drTerrainHeight, + drBldgHeight, + drLidarHeightResult, + drHeightSource); + + drCenterPosn = EcefModel::geodeticToEcef( + latitudeCenter, + longitudeCenter, + (drTerrainHeight + drHeightAGL) / 1000.0); + drUpVec = drCenterPosn.normalized(); + drEastVec = (Vector3(-drUpVec.y(), drUpVec.x(), 0.0)) + .normalized(); + drNorthVec = drUpVec.cross(drEastVec); + + fprintf(fkml, " \n"); + fprintf(fkml, " CIRCLE\n"); + fprintf(fkml, + " 1\n"); + fprintf(fkml, + " " + "#transBluePoly\n"); + fprintf(fkml, " \n"); + fprintf(fkml, + " 0\n"); + fprintf(fkml, + " " + "0\n"); + fprintf(fkml, + " " + "clampToGround\n"); + fprintf(fkml, + " \n"); + fprintf(fkml, " \n"); + fprintf(fkml, + " \n"); + + for (int ptIdx = 0; ptIdx <= numPtsCircle; ++ptIdx) { + double phi = 2 * M_PI * ptIdx / numPtsCircle; + Vector3 circlePtPosn = drCenterPosn + + (circleRadius / 1000) * + (drEastVec * + cos(phi) + + drNorthVec * + sin(phi)); + + GeodeticCoord circlePtPosnGeodetic = + EcefModel::ecefToGeodetic(circlePtPosn); + + fprintf(fkml, + "%.10f,%.10f,%.2f\n", + circlePtPosnGeodetic.longitudeDeg, + circlePtPosnGeodetic.latitudeDeg, + 0.0); + } + + fprintf(fkml, + " \n"); + fprintf(fkml, + " \n"); + fprintf(fkml, + " \n"); + fprintf(fkml, " \n"); + fprintf(fkml, " \n"); + + break; + default: + CORE_DUMP; + break; + } + fprintf(fkml, " \n"); + } + fprintf(fkml, " \n"); + } + /**************************************************************************************/ + + int lonRegionIdx; + int latRegionIdx; + int interp = 9; // must be odd + int numRegionLon = (int)floor(((double)_heatmapNumPtsLon) * interp / 500.0) + 1; + int numRegionLat = (int)floor(((double)_heatmapNumPtsLat) * interp / 500.0) + 1; + + /**************************************************************************************/ + /* KML Heatmap */ + /**************************************************************************************/ + if (fkml) { + int startLonIdx, stopLonIdx; + int startLatIdx, stopLatIdx; + int lonN = (_heatmapNumPtsLon - 1) / numRegionLon; + int lonq = (_heatmapNumPtsLon - 1) % numRegionLon; + int latN = (_heatmapNumPtsLat - 1) / numRegionLat; + int latq = (_heatmapNumPtsLat - 1) % numRegionLat; + + fprintf(fkml, " \n"); + fprintf(fkml, " Heatmap\n"); + + for (lonRegionIdx = 0; lonRegionIdx < numRegionLon; lonRegionIdx++) { + if (lonRegionIdx < lonq) { + startLonIdx = (lonN + 1) * lonRegionIdx; + stopLonIdx = (lonN + 1) * (lonRegionIdx + 1); + } else { + startLonIdx = lonN * lonRegionIdx + lonq; + stopLonIdx = lonN * (lonRegionIdx + 1) + lonq; + } + + for (latRegionIdx = 0; latRegionIdx < numRegionLat; latRegionIdx++) { + if (latRegionIdx < latq) { + startLatIdx = (latN + 1) * latRegionIdx; + stopLatIdx = (latN + 1) * (latRegionIdx + 1); + } else { + startLatIdx = latN * latRegionIdx + latq; + stopLatIdx = latN * (latRegionIdx + 1) + latq; + } + + /**************************************************************************************/ + /* Create PPM File */ + /**************************************************************************************/ + FILE *fppm; + if (!(fppm = fopen("/tmp/image.ppm", "wb"))) { + throw std::runtime_error("ERROR"); + } + fprintf(fppm, "P3\n"); + fprintf(fppm, + "%d %d %d\n", + (stopLonIdx - startLonIdx + 1), + (stopLatIdx - startLatIdx + 1), + 255); + + for (latIdx = stopLatIdx; latIdx >= startLatIdx; --latIdx) { + for (lonIdx = startLonIdx; lonIdx <= stopLonIdx; ++lonIdx) { + if (lonIdx) { + fprintf(fppm, " "); + } + fprintf(fppm, + "%s", + getHeatmapColor( + _heatmapIToNDB[lonIdx][latIdx], + _heatmapIsIndoor[lonIdx][latIdx], + false) + .c_str()); + } + fprintf(fppm, "\n"); + } + + fclose(fppm); + /**************************************************************************************/ + + std::string pngFile = "/tmp/image_" + std::to_string(lonRegionIdx) + + "_" + std::to_string(latRegionIdx) + ".png"; + + std::string command = "convert /tmp/image.ppm " + pngFile; + std::cout << "COMMAND: " << command << std::endl; + system(command.c_str()); + + /**************************************************************************************/ + /* Write to KML File */ + /**************************************************************************************/ + fprintf(fkml, "\n"); + fprintf(fkml, + " Region: %d_%d\n", + lonRegionIdx, + latRegionIdx); + fprintf(fkml, " %d\n", 1); + fprintf(fkml, " 80ffffff\n"); + fprintf(fkml, " \n"); + fprintf(fkml, + " image_%d_%d.png\n", + lonRegionIdx, + latRegionIdx); + fprintf(fkml, " \n"); + fprintf(fkml, " \n"); + fprintf(fkml, + " %.8f\n", + (_heatmapMinLat * (_heatmapNumPtsLat - 1 - stopLatIdx) + + _heatmapMaxLat * (stopLatIdx + 1)) / + _heatmapNumPtsLat); + fprintf(fkml, + " %.8f\n", + (_heatmapMinLat * (_heatmapNumPtsLat - startLatIdx) + + _heatmapMaxLat * startLatIdx) / + _heatmapNumPtsLat); + fprintf(fkml, + " %.8f\n", + (_heatmapMinLon * (_heatmapNumPtsLon - 1 - stopLonIdx) + + _heatmapMaxLon * (stopLonIdx + 1)) / + _heatmapNumPtsLon); + fprintf(fkml, + " %.8f\n", + (_heatmapMinLon * (_heatmapNumPtsLon - startLonIdx) + + _heatmapMaxLon * startLonIdx) / + _heatmapNumPtsLon); + fprintf(fkml, " \n"); + fprintf(fkml, "\n"); + /**************************************************************************************/ + } + } + fprintf(fkml, " \n"); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Close KML File */ + /**************************************************************************************/ + if (fkml) { + fprintf(fkml, "\n"); + fprintf(fkml, "\n"); + + fclose(fkml); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Zip files into output KMZ file */ + /**************************************************************************************/ + if (fkml) { + std::string command = "zip -j " + _kmlFile + " /tmp/doc.kml"; + for (lonRegionIdx = 0; lonRegionIdx < numRegionLon; lonRegionIdx++) { + for (latRegionIdx = 0; latRegionIdx < numRegionLat; latRegionIdx++) { + command += " /tmp/image_" + std::to_string(lonRegionIdx) + "_" + + std::to_string(latRegionIdx) + ".png"; + } + } + // std::cout << "COMMAND: " << command.c_str() << std::endl; + system(command.c_str()); + } + /**************************************************************************************/ + + _terrainDataModel->printStats(); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/* AfcManager::printUserInputs */ +/******************************************************************************************/ +void AfcManager::printUserInputs() +{ + if (!AfcManager::_createDebugFiles) { + return; + } + if (_responseCode != CConst::successResponseCode) { + return; + } + QStringList msg; + + LOGGER_INFO(logger) << "printing user inputs " << _userInputsFile; + GzipCsv inputGc(_userInputsFile); + + double lat, lon, alt, minor, major, height_uncert; + std::tie(lat, lon, alt) = _rlanLLA; + std::tie(minor, major, height_uncert) = _rlanUncerts_m; + + auto f2s = [](double f) { + return boost::lexical_cast(f); + }; + + if (inputGc) { + inputGc.writeRow({"ANALYSIS_TYPE", _analysisType}); + inputGc.writeRow({"SERIAL_NUMBER", _serialNumber.toStdString()}); + inputGc.writeRow({"LATITUDE (DEG)", f2s(lat)}); + inputGc.writeRow({"LONGITUDE (DEG)", f2s(lon)}); + inputGc.writeRow({"ANTENNA_HEIGHT (M)", f2s(alt)}); + inputGc.writeRow({"SEMI-MAJOR_AXIS (M)", f2s(major)}); + inputGc.writeRow({"SEMI-MINOR_AXIS (M)", f2s(minor)}); + inputGc.writeRow({"HEIGHT_UNCERTAINTY (M)", f2s(height_uncert)}); + inputGc.writeRow({"ORIENTATION (DEG)", f2s(_rlanOrientation_deg)}); + inputGc.writeRow({"HEIGHT_TYPE", + (_rlanHeightType == CConst::AMSLHeightType ? "AMSL" : + _rlanHeightType == CConst::AGLHeightType ? "AGL" : + "INVALID")}); + inputGc.writeRow({"ALLOW_SCAN_PTS_IN_UNC_REG", + (_allowScanPtsInUncRegFlag ? "true" : "false")}); + inputGc.writeRow({"INDOOR/OUTDOOR", + (_rlanType == RLAN_INDOOR ? "indoor" : + _rlanType == RLAN_OUTDOOR ? "outdoor" : + "error")}); + inputGc.writeRow({"CERT_INDOOR", (_certifiedIndoor ? "True" : "False")}); + inputGc.writeRow( + {"CHANNEL_RESPONSE_ALGORITHM", + CConst::strSpectralAlgorithmList->type_to_str(_channelResponseAlgorithm)}); + + // inputGc.writeRow({ "ULS_DATABASE", _inputULSDatabaseStr } ); + inputGc.writeRow({"AP/CLIENT_PROPAGATION_ENVIRO", + CConst::strPropEnvMethodList->type_to_str(_propEnvMethod)}); + inputGc.writeRow({"AP/CLIENT_MIN_EIRP_INDOOR (DBM)", f2s(_minEIRPIndoor_dBm)}); + inputGc.writeRow({"AP/CLIENT_MIN_EIRP_OUTDOOR (DBM)", f2s(_minEIRPOutdoor_dBm)}); + inputGc.writeRow({"AP/CLIENT_MAX_EIRP (DBM)", f2s(_maxEIRP_dBm)}); + + inputGc.writeRow( + {"BUILDING_PENETRATION_LOSS_MODEL", _buildingLossModel.toStdString()}); + inputGc.writeRow( + {"BUILDING_TYPE", + (_buildingType == CConst::traditionalBuildingType ? "traditional" : + _buildingType == CConst::thermallyEfficientBuildingType ? + "thermally efficient" : + "no building type")}); + inputGc.writeRow({"BUILDING_PENETRATION_CONFIDENCE", f2s(_confidenceBldg2109)}); + inputGc.writeRow({"BUILDING_PENETRATION_LOSS_FIXED_VALUE (DB)", + f2s(_fixedBuildingLossValue)}); + inputGc.writeRow({"POLARIZATION_LOSS (DB)", f2s(_polarizationLossDB)}); + inputGc.writeRow({"RLAN_BODY_LOSS_INDOOR (DB)", f2s(_bodyLossIndoorDB)}); + inputGc.writeRow({"RLAN_BODY_LOSS_OUTDOOR (DB)", f2s(_bodyLossOutdoorDB)}); + inputGc.writeRow({"I/N_THRESHOLD", f2s(_IoverN_threshold_dB)}); + inputGc.writeRow( + {"FS_RECEIVER_DEFAULT_ANTENNA", + CConst::strULSAntennaTypeList->type_to_str(_ulsDefaultAntennaType)}); + inputGc.writeRow( + {"RLAN_ITM_TX_CLUTTER_METHOD", + CConst::strITMClutterMethodList->type_to_str(_rlanITMTxClutterMethod)}); + + inputGc.writeRow({"PROPAGATION_MODEL", + CConst::strPathLossModelList->type_to_str(_pathLossModel)}); + inputGc.writeRow({"WINNER_II_PROB_LOS_THRESHOLD", f2s(_winner2ProbLOSThr)}); + inputGc.writeRow({"WINNER_II_LOS_CONFIDENCE", f2s(_confidenceWinner2LOS)}); + inputGc.writeRow({"WINNER_II_NLOS_CONFIDENCE", f2s(_confidenceWinner2NLOS)}); + inputGc.writeRow( + {"WINNER_II_COMBINED_CONFIDENCE", f2s(_confidenceWinner2Combined)}); + inputGc.writeRow({"WINNER_II_HGT_FLAG", (_closeInHgtFlag ? "true" : "false")}); + inputGc.writeRow({"WINNER_II_HGT_LOS", f2s(_closeInHgtLOS)}); + inputGc.writeRow({"ITM_CONFIDENCE", f2s(_confidenceITM)}); + inputGc.writeRow({"ITM_RELIABILITY", f2s(_reliabilityITM)}); + inputGc.writeRow({"P.2108_CONFIDENCE", f2s(_confidenceClutter2108)}); + inputGc.writeRow({"WINNER_II_USE_GROUND_DISTANCE", + (_winner2UseGroundDistanceFlag ? "true" : "false")}); + inputGc.writeRow({"FSPL_USE_GROUND_DISTANCE", + (_fsplUseGroundDistanceFlag ? "true" : "false")}); + inputGc.writeRow( + {"PASSIVE_REPEATER_FLAG", (_passiveRepeaterFlag ? "true" : "false")}); + inputGc.writeRow({"RX ANTENNA FEEDER LOSS IDU (DB)", f2s(_rxFeederLossDBIDU)}); + inputGc.writeRow({"RX ANTENNA FEEDER LOSS ODU (DB)", f2s(_rxFeederLossDBODU)}); + inputGc.writeRow( + {"RX ANTENNA FEEDER LOSS UNKNOWN (DB)", f2s(_rxFeederLossDBUnknown)}); + inputGc.writeRow({"RAIN_FOREST_FILE", _rainForestFile}); + inputGc.writeRow({"SRTM DIRECTORY", _srtmDir}); + inputGc.writeRow({"DEP DIRECTORY", _depDir}); + inputGc.writeRow({"CDSM DIRECTORY", _cdsmDir}); + inputGc.writeRow({"CDSM LOS THREHOLD", f2s(_cdsmLOSThr)}); + if (_analysisType == "ExclusionZoneAnalysis") { + double chanCenterFreq = _wlanMinFreq + (_exclusionZoneRLANChanIdx + 0.5) * + _exclusionZoneRLANBWHz; + + inputGc.writeRow({"EXCLUSION_ZONE_FSID", f2s(_exclusionZoneFSID)}); + inputGc.writeRow( + {"EXCLUSION_ZONE_RLAN_BW (Hz)", f2s(_exclusionZoneRLANBWHz)}); + inputGc.writeRow( + {"EXCLUSION_ZONE_RLAN_CENTER_FREQ (Hz)", f2s(chanCenterFreq)}); + inputGc.writeRow( + {"EXCLUSION_ZONE_RLAN_EIRP (dBm)", f2s(_exclusionZoneRLANEIRPDBm)}); + + } else if (_analysisType == "HeatmapAnalysis") { + if (_inquiredChannels.size() != 1) { + throw std::runtime_error(ErrStream() + << "ERROR: Number of channels = " + << _inquiredChannels.size() + << " require exactly 1 channel for " + "heatmap analysis"); + } + if (_inquiredChannels[0].second.size() != 1) { + throw std::runtime_error(ErrStream() + << "ERROR: Number of channels = " + << _inquiredChannels.size() + << " require exactly 1 channel for " + "heatmap analysis"); + } + int operatingClass = _inquiredChannels[0].first; + int channelCfi = _inquiredChannels[0].second[0]; + + inputGc.writeRow({"HEATMAP_CHANNEL_OPERATING_CLASS", + std::to_string(operatingClass)}); + inputGc.writeRow({"HEATMAP_CHANNEL_CFI", std::to_string(channelCfi)}); + inputGc.writeRow({"HEATMAP_MIN_LON (DEG)", f2s(_heatmapMinLon)}); + inputGc.writeRow({"HEATMAP_MIN_LAT (DEG)", f2s(_heatmapMaxLon)}); + inputGc.writeRow({"HEATMAP_RLAN_SPACING (m)", f2s(_heatmapRLANSpacing)}); + inputGc.writeRow({"HEATMAP_INDOOR_OUTDOOR_STR", _heatmapIndoorOutdoorStr}); + + inputGc.writeRow( + {"HEATMAP_RLAN_INDOOR_EIRP (dBm)", f2s(_heatmapRLANIndoorEIRPDBm)}); + inputGc.writeRow( + {"HEATMAP_RLAN_INDOOR_HEIGHT_TYPE", _heatmapRLANIndoorHeightType}); + inputGc.writeRow( + {"HEATMAP_RLAN_INDOOR_HEIGHT (m)", f2s(_heatmapRLANIndoorHeight)}); + inputGc.writeRow({"HEATMAP_RLAN_INDOOR_HEIGHT_UNCERTAINTY (m)", + f2s(_heatmapRLANIndoorHeightUncertainty)}); + + inputGc.writeRow({"HEATMAP_RLAN_OUTDOOR_EIRP (dBm)", + f2s(_heatmapRLANOutdoorEIRPDBm)}); + inputGc.writeRow({"HEATMAP_RLAN_OUTDOOR_HEIGHT_TYPE", + _heatmapRLANOutdoorHeightType}); + inputGc.writeRow({"HEATMAP_RLAN_OUTDOOR_HEIGHT (m)", + f2s(_heatmapRLANOutdoorHeight)}); + inputGc.writeRow({"HEATMAP_RLAN_OUTDOOR_HEIGHT_UNCERTAINTY (m)", + f2s(_heatmapRLANOutdoorHeightUncertainty)}); + } + inputGc.writeRow({"REPORT_UNAVAILABLE_SPECTRUM", + (std::isnan(_reportUnavailPSDdBmPerMHz) ? "false" : "true")}); + if (!std::isnan(_reportUnavailPSDdBmPerMHz)) { + inputGc.writeRow({"REPORT_UNAVAIL_PSD_DBM_PER_MHZ", + f2s(_reportUnavailPSDdBmPerMHz)}); + } + inputGc.writeRow({"INQUIRED_FREQUENCY_MAX_PSD_DBM_PER_MHZ", + f2s(_inquiredFrequencyMaxPSD_dBmPerMHz)}); + inputGc.writeRow({"VISIBILITY_THRESHOLD", f2s(_visibilityThreshold)}); + inputGc.writeRow( + {"PRINT_SKIPPED_LINKS_FLAG", (_printSkippedLinksFlag ? "true" : "false")}); + inputGc.writeRow({"ROUND_PSD_EIRP_FLAG", (_roundPSDEIRPFlag ? "true" : "false")}); + inputGc.writeRow({"ACI_FLAG", (_aciFlag ? "true" : "false")}); + } + LOGGER_DEBUG(logger) << "User inputs written to userInputs.csv"; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/* AfcManager::computePropEnv */ +/******************************************************************************************/ +CConst::PropEnvEnum AfcManager::computePropEnv(double lonDeg, + double latDeg, + CConst::NLCDLandCatEnum &nlcdLandCat, + bool errorFlag) const +{ + // If user uses a density map versus a constant environmental input + int lonIdx; + int latIdx; + CConst::PropEnvEnum propEnv; + nlcdLandCat = CConst::unknownNLCDLandCat; + if (_propEnvMethod == CConst::nlcdPointPropEnvMethod) { + unsigned int landcat = cgNlcd->valueAt(latDeg, lonDeg); + + switch (landcat) { + case 23: + case 24: + propEnv = CConst::urbanPropEnv; + break; + case 21: + case 22: + propEnv = CConst::suburbanPropEnv; + break; + case 41: + case 43: + case 90: + nlcdLandCat = CConst::deciduousTreesNLCDLandCat; + propEnv = CConst::ruralPropEnv; + break; + case 42: + nlcdLandCat = CConst::coniferousTreesNLCDLandCat; + propEnv = CConst::ruralPropEnv; + break; + case 52: + case 82: + nlcdLandCat = CConst::highCropFieldsNLCDLandCat; + propEnv = CConst::ruralPropEnv; + break; + case 11: + case 12: + case 31: + case 51: + case 71: + case 72: + case 73: + case 74: + case 81: + case 95: + nlcdLandCat = CConst::noClutterNLCDLandCat; + propEnv = CConst::ruralPropEnv; + break; + default: + nlcdLandCat = CConst::villageCenterNLCDLandCat; + propEnv = CConst::ruralPropEnv; + break; + } + } else if (_propEnvMethod == CConst::popDensityMapPropEnvMethod) { + int regionIdx; + char propEnvChar; + _popGrid->findDeg(lonDeg, latDeg, lonIdx, latIdx, propEnvChar, regionIdx); + + switch (propEnvChar) { + case 'U': + propEnv = CConst::urbanPropEnv; + break; + case 'S': + propEnv = CConst::suburbanPropEnv; + break; + case 'R': + propEnv = CConst::ruralPropEnv; + nlcdLandCat = CConst::villageCenterNLCDLandCat; + break; + case 'B': + propEnv = CConst::barrenPropEnv; + break; + case 'X': + propEnv = CConst::unknownPropEnv; + break; + default: + propEnv = CConst::unknownPropEnv; + break; + } + + if (_rainForestPolygon) { + int xIdx = (int)floor(lonDeg / _regionPolygonResolution + 0.5); + int yIdx = (int)floor(latDeg / _regionPolygonResolution + 0.5); + + if (_rainForestPolygon->in_bdy_area(xIdx, yIdx)) { + nlcdLandCat = CConst::tropicalRainForestNLCDLandCat; + } + } + + // For constant set environments: + } else if (_propEnvMethod == CConst::urbanPropEnvMethod) { + propEnv = CConst::urbanPropEnv; + } else if (_propEnvMethod == CConst::suburbanPropEnvMethod) { + propEnv = CConst::suburbanPropEnv; + } else if (_propEnvMethod == CConst::ruralPropEnvMethod) { + propEnv = CConst::ruralPropEnv; + } else { + throw std::runtime_error("Error in selecting propagation environment"); + } + + if ((propEnv == CConst::unknownPropEnv) && (errorFlag)) { + throw std::runtime_error( + ErrStream() + << "ERROR: RLAN Location LAT = " << latDeg << " LON = " << lonDeg + << " outside Simulation Region defined by population density file"); + } + + return (propEnv); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/* AfcManager::computeClutter452HtEl */ +/* See ITU-R p.452-16, Section 4.5.3 */ +/******************************************************************************************/ +double AfcManager::computeClutter452HtEl(double txHeightM, + double distKm, + double elevationAngleDeg) const +{ + // Input values + double d_k = 0.07; // Distance (km) from nominal clutter point to antenna + double h_a = 5.0; // Nominal clutter height (m) above local ground level + + // double distKm = 1; Test case + // double frequency = 6; // GHz per ITU-R p.452; this is unused if using F_fc=1 + + // Calculate elevationAngleClutterLimitDeg + double tanVal = (h_a - txHeightM) / + (d_k * 1e3); // Tan value of elevation AngleClutterLimitDeg + double elevationAngleClutterLimitDeg = + atan(tanVal) * 180 / M_PI; // Returns elevationAngleClutterLimit in degrees + + // Calculate calculate clutter loss + double htanVal = 6 * (txHeightM / h_a - 0.625); // htan angle + // double F_fc = 0.25 + 0.375 * (1 + tanh(7.5 * (frequency - 0.5))); // Use this equation + // for non-6GHz frequencies + double F_fc = 1; // Near 6GHz frequency, this is always approximately 1 + double A_h = 10.25 * F_fc * exp(-1 * d_k) * (1 - tanh(htanVal)) - 0.33; // Clutter loss + + if ((elevationAngleDeg <= elevationAngleClutterLimitDeg) && + (distKm > d_k * 10)) // If signal is below clutter + return (A_h); // Set path clutter loss + else + return (0.0); // Otherwise, no clutter loss +} +/**************************************************************************************/ + +/**************************************************************************************/ +/* AfcManager::setConstInputs() */ +/**************************************************************************************/ +void AfcManager::setConstInputs(const std::string &tempDir) +{ + QDir tempBuild = QDir(); + if (!tempBuild.exists(QString::fromStdString(tempDir))) { + tempBuild.mkdir(QString::fromStdString(tempDir)); + } + + SearchPaths::init(); + + /**************************************************************************************/ + /* Constant Parameters */ + /**************************************************************************************/ + + _minRlanHeightAboveTerrain = 1.5; + RlanRegionClass::minRlanHeightAboveTerrain = _minRlanHeightAboveTerrain; + + _maxRadius = 150.0e3; + _exclusionDist = 1.0; + + _illuminationEfficiency = 1.0; + _closeInDist = 1.0e3; // Radius in which close in path loss model is used + _closeInPathLossModel = "WINNER2"; // Close in path loss model is used + + _wlanMinFreqMHz = 5925; + _wlanMaxFreqMHz = 7125; + + // Hardcode to US for now. + // When multiple countries are supported this need to come from AFC Configuration + for (auto &opClass : OpClass::USOpClass) { + _opClass.push_back(opClass); + } + + for (auto &opClass : OpClass::PSDOpClassList) { + _psdOpClassList.push_back(opClass); + } + + _wlanMinFreq = _wlanMinFreqMHz * 1.0e6; + _wlanMaxFreq = _wlanMaxFreqMHz * 1.0e6; + + _filterSimRegionOnly = false; + + // _rlanBWStr = "20.0e6,40.0e6,80.0e6,160.0e6"; // Channel bandwidths in Hz + + _regionPolygonResolution = 1.0e-5; + + if (AfcManager::_createDebugFiles) { + _excThrFile = QDir(QString::fromStdString(tempDir)) + .filePath("exc_thr.csv.gz") + .toStdString(); + _fsAnomFile = QDir(QString::fromStdString(tempDir)) + .filePath("fs_anom.csv.gz") + .toStdString(); + _userInputsFile = QDir(QString::fromStdString(tempDir)) + .filePath("userInputs.csv.gz") + .toStdString(); + } else { + _excThrFile = ""; + _fsAnomFile = ""; + _userInputsFile = ""; + } + if (AfcManager::_createSlowDebugFiles) { + _eirpGcFile = + QDir(QString::fromStdString(tempDir)).filePath("eirp.csv.gz").toStdString(); + } + if (AfcManager::_createKmz) { + _kmlFile = + QDir(QString::fromStdString(tempDir)).filePath("results.kmz").toStdString(); + } + /**************************************************************************************/ +} +/**************************************************************************************/ + +/**************************************************************************************/ +/* AfcManager::computeInquiredFreqRangesPSD() */ +/**************************************************************************************/ +void AfcManager::computeInquiredFreqRangesPSD(std::vector &psdFreqRangeList) +{ + for (auto &channel : _channelList) { + if (channel.type == INQUIRED_FREQUENCY) { + psdFreqRangeClass psdFreqRange; + for (int freqSegIdx = 0; freqSegIdx < channel.segList.size(); + ++freqSegIdx) { + if (freqSegIdx == 0) { + psdFreqRange.freqMHzList.push_back(channel.freqMHzList[0]); + } + int freqSegStartFreqMHz = channel.freqMHzList[freqSegIdx]; + int freqSegStopFreqMHz = channel.freqMHzList[freqSegIdx + 1]; + ChannelColor chanColor = std::get<2>(channel.segList[freqSegIdx]); + if ((chanColor != BLACK) && (chanColor != RED)) { + double segEIRPLimitA = std::get<0>( + channel.segList[freqSegIdx]); + double segEIRPLimitB = std::get<1>( + channel.segList[freqSegIdx]); + double psdA = segEIRPLimitA - + 10.0 * log10((double)channel.bandwidth( + freqSegIdx)); + double psdB = segEIRPLimitB - + 10.0 * log10((double)channel.bandwidth( + freqSegIdx)); + + if (_roundPSDEIRPFlag) { + // PSD value rounded down to nearest multiple of 0.1 + // dB + psdA = std::floor(psdA * 10) / 10.0; + psdB = std::floor(psdB * 10) / 10.0; + double prevPSD = psdA; + int prevFreq = freqSegStartFreqMHz; + while (prevFreq < freqSegStopFreqMHz) { + if (prevPSD == psdB) { + psdFreqRange.freqMHzList.push_back( + freqSegStopFreqMHz); + psdFreqRange.psd_dBm_MHzList + .push_back(psdB); + prevPSD = psdB; + prevFreq = freqSegStopFreqMHz; + } else { + int n = (int)floor( + fabs(psdB - prevPSD) / 0.1 + + 0.5); + int m = freqSegStopFreqMHz - + prevFreq; + double freqMHzVal = prevFreq + + (m + n - 1) / n; + double psdVal = + (psdA * (freqSegStopFreqMHz - + freqMHzVal) + + psdB * (freqMHzVal - + freqSegStartFreqMHz)) / + (freqSegStopFreqMHz - + freqSegStartFreqMHz); + psdVal = std::floor(psdVal * 10) / + 10.0; + double minPSD = std::min(prevPSD, + psdVal); + psdFreqRange.freqMHzList.push_back( + freqMHzVal); + psdFreqRange.psd_dBm_MHzList + .push_back(minPSD); + prevPSD = psdVal; + prevFreq = freqMHzVal; + } + } + } else { + double prevPSD = psdA; + if (prevPSD == psdB) { + psdFreqRange.freqMHzList.push_back( + freqSegStopFreqMHz); + psdFreqRange.psd_dBm_MHzList.push_back( + psdB); + prevPSD = psdB; + } else { + for (int freqMHzVal = + freqSegStartFreqMHz + 1; + freqMHzVal <= freqSegStopFreqMHz; + ++freqMHzVal) { + double psdVal = + (psdA * (freqSegStopFreqMHz - + freqMHzVal) + + psdB * (freqMHzVal - + freqSegStartFreqMHz)) / + (freqSegStopFreqMHz - + freqSegStartFreqMHz); + double minPSD = std::min(prevPSD, + psdVal); + psdFreqRange.freqMHzList.push_back( + freqMHzVal); + psdFreqRange.psd_dBm_MHzList + .push_back(minPSD); + prevPSD = psdVal; + } + } + } + } else { + psdFreqRange.freqMHzList.push_back(freqSegStopFreqMHz); + psdFreqRange.psd_dBm_MHzList.push_back(quietNaN); + } + } + + int segIdx; + for (segIdx = psdFreqRange.psd_dBm_MHzList.size() - 2; segIdx >= 0; + --segIdx) { + if (psdFreqRange.psd_dBm_MHzList[segIdx] == + psdFreqRange.psd_dBm_MHzList[segIdx + 1]) { + psdFreqRange.psd_dBm_MHzList.erase( + psdFreqRange.psd_dBm_MHzList.begin() + segIdx + 1); + psdFreqRange.freqMHzList.erase( + psdFreqRange.freqMHzList.begin() + segIdx + 1); + } + } + + psdFreqRangeList.push_back(psdFreqRange); + } + } + + return; +} +/**************************************************************************************/ + +/**************************************************************************************/ +/* AfcManager::createChannelList() */ +/**************************************************************************************/ +void AfcManager::createChannelList() +{ + // add channel plan to channel list + int totalNumChan = 0; + + /**********************************************************************************/ + /* Check that inquired frequency ranges are valid and translate into sorted */ + /* list of disjoint segments. */ + /**********************************************************************************/ + std::vector> freqSegmentList; + for (auto &freqRange : _inquiredFrequencyRangesMHz) { + auto inquiredStartFreqMHz = freqRange.first; + auto inquiredStopFreqMHz = freqRange.second; + + if (inquiredStopFreqMHz <= inquiredStartFreqMHz) { + LOGGER_DEBUG(logger) << "Inquired Freq Range INVALID: [" + << inquiredStartFreqMHz << ", " << inquiredStopFreqMHz + << "] stop freq must be > start freq" << std::endl; + _responseCode = CConst::unsupportedSpectrumResponseCode; + return; + } + + std::vector> overlapBandList = + calculateOverlapBandList(_allowableFreqBandList, + inquiredStartFreqMHz, + inquiredStopFreqMHz); + + if (overlapBandList.size()) { + for (int segIdx = 0; segIdx < (int)overlapBandList.size(); ++segIdx) { + int startFreq = overlapBandList[segIdx].first; + int stopFreq = overlapBandList[segIdx].second; + + int idxA = 0; + bool overlapA; + while ((idxA < freqSegmentList.size()) && + (startFreq > freqSegmentList[idxA].second)) { + idxA++; + } + overlapA = ((idxA < freqSegmentList.size()) && + (startFreq >= freqSegmentList[idxA].first)); + + int idxB = 0; + bool overlapB; + while ((idxB < freqSegmentList.size()) && + (stopFreq > freqSegmentList[idxB].second)) { + idxB++; + } + overlapB = ((idxB < freqSegmentList.size()) && + (stopFreq >= freqSegmentList[idxB].first)); + + int start = overlapA ? freqSegmentList[idxA].first : startFreq; + int stop = overlapB ? freqSegmentList[idxB].second : stopFreq; + int delStart = idxA; + int delStop = overlapB ? idxB : idxB - 1; + + if (delStop >= delStart) { + freqSegmentList.erase(freqSegmentList.begin() + delStart, + freqSegmentList.begin() + delStop + + 1); + } + freqSegmentList.insert(freqSegmentList.begin() + delStart, + std::make_pair(start, stop)); + } + } else { + // the start/stop frequencies are not valid + LOGGER_DEBUG(logger) + << "Inquired Freq Range INVALID: [" << inquiredStartFreqMHz << ", " + << inquiredStopFreqMHz << "]" << std::endl; + _responseCode = CConst::unsupportedSpectrumResponseCode; + return; + } + } + /**********************************************************************************/ + +#if DEBUG_AFC && 0 + std::cout << "freqSegmentList contains:" << std::endl; + for (int i = 0; i < freqSegmentList.size(); i++) + std::cout << " [" << freqSegmentList[i].first << "," << freqSegmentList[i].second + << "]" << std::endl; + std::cout << '\n'; +#endif + + if (freqSegmentList.size()) { + ChannelStruct channel; + channel.operatingClass = -1; + channel.index = -1; + channel.type = INQUIRED_FREQUENCY; + + for (int segIdx = 0; segIdx < (int)freqSegmentList.size(); ++segIdx) { + channel.freqMHzList.push_back(freqSegmentList[segIdx].first); + channel.freqMHzList.push_back(freqSegmentList[segIdx].second); + + if (segIdx) { + channel.segList.push_back(std::make_tuple( + 0.0, + 0.0, + BLACK)); // Between segments not used, initialized to BLACK + } + channel.segList.push_back( + std::make_tuple(0.0, + 0.0, + GREEN)); // Everything initialized to GREEN + } + _channelList.push_back(channel); + } + + for (auto &channelPair : _inquiredChannels) { + LOGGER_DEBUG(logger) + << "creating channels for operating class " << channelPair.first; + + int numChan; + numChan = 0; + + // Iterate each operating classes and add all channels of given operating class + for (auto &opClass : _opClass) { + // Skip of classes of not in inquired channel list + if (opClass.opClass != channelPair.first) { + continue; + } + + for (auto &cfi : opClass.channels) { + bool includeChannel; + + includeChannel = false; + + // If channel indexes are provided check for channel validity. + // If channel indexes are not provided then include all channels of + // given operating class. + if (channelPair.second.size() != 0) { + for (auto inquired_cfi : channelPair.second) { + if (inquired_cfi == cfi) { + includeChannel = true; + break; + } + } + } else { + includeChannel = true; + } + + if (includeChannel) { + ChannelStruct channel; + int startFreqMHz = (opClass.startFreq + 5 * cfi) - + (opClass.bandWidth >> 1); + int stopFreqMHz = startFreqMHz + opClass.bandWidth; + channel.freqMHzList.push_back(startFreqMHz); + channel.freqMHzList.push_back(stopFreqMHz); + + // Include channel if it is within the frequency bands in + // AFC config + if (containsChannel(_allowableFreqBandList, + startFreqMHz, + stopFreqMHz)) { + channel.operatingClass = opClass.opClass; + channel.index = cfi; + channel.segList.push_back(std::make_tuple( + 0.0, + 0.0, + GREEN)); // Everything initialized to GREEN + channel.type = ChannelType::INQUIRED_CHANNEL; + _channelList.push_back(channel); + numChan++; + totalNumChan++; + LOGGER_DEBUG(logger) + << "added " << numChan << " channels"; + } + } + } + if (numChan == 0) { + LOGGER_DEBUG(logger) << "Inquired Channel INVALID Operating Class: " + << channelPair.first << std::endl; + _responseCode = CConst::unsupportedSpectrumResponseCode; + return; + } + } + + if (totalNumChan == 0) { + LOGGER_DEBUG(logger) + << "Missing valid Inquired channel and frequency " << std::endl; + _responseCode = CConst::missingParamResponseCode; + return; + } + } +} +/**************************************************************************************/ + +/**************************************************************************************/ +/* AfcManager::splitFrequencyRanges() */ +/**************************************************************************************/ +void AfcManager::splitFrequencyRanges() +{ + std::set fsChannelEdgesMhz; + for (int ulsIdx = 0; ulsIdx < _ulsList->getSize(); ++ulsIdx) { + auto uls = (*_ulsList)[ulsIdx]; + fsChannelEdgesMhz.insert((int)floor((uls->getStartFreq() + 1.0) * 1.0e-6)); + fsChannelEdgesMhz.insert((int)ceil((uls->getStopFreq() - 1.0) * 1.0e-6)); + } + + int drIdx; + for (drIdx = 0; drIdx < (int)_deniedRegionList.size(); ++drIdx) { + DeniedRegionClass *dr = _deniedRegionList[drIdx]; + fsChannelEdgesMhz.insert((int)floor((dr->getStartFreq() + 1.0) * 1.0e-6)); + fsChannelEdgesMhz.insert((int)ceil((dr->getStopFreq() - 1.0) * 1.0e-6)); + } + + int numFsFreq = fsChannelEdgesMhz.size(); + + if (numFsFreq == 0) { + return; + } + std::set::iterator fsChannelEdgesMhzIt = fsChannelEdgesMhz.begin(); + int minFsFreq = *std::next(fsChannelEdgesMhzIt, 0); + int maxFsFreq = *std::next(fsChannelEdgesMhzIt, numFsFreq - 1); + + for (int chanIdx = 0; chanIdx < (int)_channelList.size(); ++chanIdx) { + ChannelStruct *channel = &(_channelList[chanIdx]); + if (channel->type == INQUIRED_FREQUENCY) { + int segIdx = 0; + while (segIdx < channel->segList.size()) { + ChannelColor segColor = std::get<2>(channel->segList[segIdx]); + if (segColor != BLACK) { + int chanStartFreqMHz = channel->freqMHzList[segIdx]; + int chanStopFreqMHz = channel->freqMHzList[segIdx + 1]; + + int aIdx = -2; + int bIdx = -2; + + if ((maxFsFreq <= chanStartFreqMHz) || + (minFsFreq >= chanStopFreqMHz)) { + // Do nothing + } else { + aIdx = -1; + std::set::iterator aIt = fsChannelEdgesMhzIt; + while (*aIt <= chanStartFreqMHz) { + aIdx++; + aIt++; + }; + bIdx = numFsFreq; + std::set::reverse_iterator bIt = + fsChannelEdgesMhz.rbegin(); + while (*bIt >= chanStopFreqMHz) { + bIdx--; + bIt++; + } + int numIns = bIdx - aIdx - 1; + + if (numIns > 0) { + for (int insIdx = 0; insIdx < numIns; + ++insIdx) { + channel->freqMHzList.insert( + channel->freqMHzList + .begin() + + segIdx + 1 + insIdx, + *aIt); + channel->segList.insert( + channel->segList.begin() + + segIdx + insIdx, + std::make_tuple(0.0, + 0.0, + GREEN)); + // Everything initialized to GREEN + aIt++; + } + } + segIdx += numIns; + } + } + segIdx++; + } + +#if DEBUG_AFC && 0 + std::cout << "NUM_SEG: " << channel->segList.size() << std::endl; + for (segIdx = 0; segIdx < channel->segList.size(); ++segIdx) { + ChannelColor segColor = std::get<2>(channel->segList[segIdx]); + std::string colorStr; + switch (segColor) { + case BLACK: + colorStr = "BLACK"; + break; + case RED: + colorStr = "RED"; + break; + case YELLOW: + colorStr = "YELLOW"; + break; + case GREEN: + colorStr = "GREEN"; + break; + default: + CORE_DUMP; + break; + } + std::cout << "SEG " << segIdx << ": " + << channel->freqMHzList[segIdx] << " - " + << channel->freqMHzList[segIdx + 1] << " " << colorStr + << std::endl; + } +#endif + } + } +} +/**************************************************************************************/ + +/**************************************************************************************/ +/* AfcManager::containsChannel() */ +/**************************************************************************************/ +bool AfcManager::containsChannel(const std::vector &freqBandList, + int chanStartFreqMHz, + int chanStopFreqMHz) +{ + auto segmentList = std::vector> { + std::make_pair(chanStartFreqMHz, chanStopFreqMHz)}; + + for (auto &freqBand : freqBandList) { + int bandStart = freqBand.getStartFreqMHz(); + int bandStop = freqBand.getStopFreqMHz(); + int segIdx = 0; + while (segIdx < (int)segmentList.size()) { + int segStart = segmentList[segIdx].first; + int segStop = segmentList[segIdx].second; + + if ((bandStop <= segStart) || (bandStart >= segStop)) { + // No overlap, do nothing + segIdx++; + } else if ((bandStart <= segStart) && (bandStop >= segStop)) { + // Remove segment + segmentList.erase(segmentList.begin() + segIdx); + } else if (bandStart <= segStart) { + // Clip bottom of segment + segmentList[segIdx] = std::make_pair(bandStop, segStop); + segIdx++; + } else if (bandStop >= segStop) { + // Clip top of segment + segmentList[segIdx] = std::make_pair(segStart, bandStart); + segIdx++; + } else { + // Split Segment + segmentList[segIdx] = std::make_pair(segStart, bandStart); + segmentList.insert(segmentList.begin() + segIdx + 1, + std::make_pair(bandStop, segStop)); + segIdx += 2; + } + } + } + + bool containsFlag = (segmentList.size() == 0); + + return (containsFlag); +} +/**************************************************************************************/ + +/**************************************************************************************/ +/* AfcManager::calculateOverlapBandList() */ +/**************************************************************************************/ +std::vector> AfcManager::calculateOverlapBandList( + const std::vector &freqBandList, + int chanStartFreqMHz, + int chanStopFreqMHz) +{ + std::vector> overlapBandList; + + for (auto &freqBand : freqBandList) { + int bandStart = freqBand.getStartFreqMHz(); + int bandStop = freqBand.getStopFreqMHz(); + int overlapStart = std::max(bandStart, chanStartFreqMHz); + int overlapStop = std::min(bandStop, chanStopFreqMHz); + + if (overlapStart < overlapStop) { + int segIdxA = -1; + int segIdxB = -1; + for (int segIdx = overlapBandList.size() - 1; + (segIdx >= 0) && (segIdxA == -1); + --segIdx) { + if (overlapBandList[segIdx].first <= overlapStart) { + segIdxA = segIdx; + } + } + for (int segIdx = 0; + segIdx < ((int)overlapBandList.size()) && (segIdxB == -1); + ++segIdx) { + if (overlapBandList[segIdx].second >= overlapStop) { + segIdxB = segIdx; + } + } + if ((segIdxA == -1) && (segIdxB == -1)) { + overlapBandList.clear(); + overlapBandList.push_back( + std::make_pair(overlapStart, overlapStop)); + } else if (segIdxA == -1) { + if (segIdxB) { + overlapBandList.erase(overlapBandList.begin(), + overlapBandList.begin() + segIdxB); + } + if (overlapStop < overlapBandList[0].first) { + overlapBandList.insert(overlapBandList.begin(), + std::make_pair(overlapStart, + overlapStop)); + } else { + overlapBandList[0].first = overlapStart; + } + } else if (segIdxB == -1) { + if (segIdxA + 1 < (int)overlapBandList.size()) { + overlapBandList.erase(overlapBandList.begin() + segIdxA + 1, + overlapBandList.end()); + } + if (overlapStart > overlapBandList[segIdxA].second) { + overlapBandList.push_back( + std::make_pair(overlapStart, overlapStop)); + } else { + overlapBandList[segIdxA].second = overlapStop; + } + } else if (segIdxA < segIdxB) { + int startEraseIdx; + int stopEraseIdx; + if (overlapStart > overlapBandList[segIdxA].second) { + startEraseIdx = segIdxA + 1; + } else { + startEraseIdx = segIdxA; + overlapStart = overlapBandList[segIdxA].first; + } + if (overlapStop < overlapBandList[segIdxB].first) { + stopEraseIdx = segIdxB - 1; + } else { + stopEraseIdx = segIdxB; + overlapStop = overlapBandList[segIdxB].second; + } + overlapBandList.erase(overlapBandList.begin() + startEraseIdx, + overlapBandList.begin() + stopEraseIdx + 1); + overlapBandList.insert(overlapBandList.begin() + startEraseIdx, + std::make_pair(overlapStart, overlapStop)); + } + } + } + + return (overlapBandList); +} +/**************************************************************************************/ + +#if DEBUG_AFC +/******************************************************************************************/ +/* AfcManager::runTestITM(std::string inputFile) */ +/******************************************************************************************/ +void AfcManager::runTestITM(std::string inputFile) +{ + LOGGER_INFO(logger) << "Executing AfcManager::runTestITM()"; + + #if 1 + extern void point_to_point(double elev[], + double tht_m, + double rht_m, + double eps_dielect, + double sgm_conductivity, + double eno_ns_surfref, + double frq_mhz, + int radio_climate, + int pol, + double conf, + double rel, + double &dbloss, + std::string &strmode, + int &errnum); + #endif + + int linenum, fIdx; + std::string line, strval; + char *chptr; + std::string str; + std::string reasonIgnored; + std::ostringstream errStr; + + double rlanLon = -91.43291667; + double rlanLat = 41.4848611111; + + double fsLon = -91.74102778; + double fsLat = 41.96444444; + double fsHeightAGL = 108.5; + + double frequencyMHz = 6175.0; + + double groundDistanceKm; + { + double lon1Rad = rlanLon * M_PI / 180.0; + double lat1Rad = rlanLat * M_PI / 180.0; + double lon2Rad = fsLon * M_PI / 180.0; + double lat2Rad = fsLat * M_PI / 180.0; + + double slat = sin((lat2Rad - lat1Rad) / 2); + double slon = sin((lon2Rad - lon1Rad) / 2); + groundDistanceKm = 2 * CConst::averageEarthRadius * + asin(sqrt(slat * slat + + cos(lat1Rad) * cos(lat2Rad) * slon * slon)) * + 1.0e-3; + } + + int terrainHeightFieldIdx = -1; + + std::vector fieldIdxList; + std::vector fieldLabelList; + fieldIdxList.push_back(&terrainHeightFieldIdx); + fieldLabelList.push_back("Terrain Height (m)"); + fieldIdxList.push_back(&terrainHeightFieldIdx); + fieldLabelList.push_back("TERRAIN_HEIGHT_AMSL (m)"); + + double terrainHeight; + + std::vector heightList; + + int fieldIdx; + + if (inputFile.empty()) { + throw std::runtime_error("ERROR: No ITM Test File specified"); + } + + LOGGER_INFO(logger) << "Reading Winner2 Test File: " << inputFile; + + FILE *fin; + if (!(fin = fopen(inputFile.c_str(), "rb"))) { + str = std::string("ERROR: Unable to open ITM Test File \"") + inputFile + + std::string("\"\n"); + throw std::runtime_error(str); + } + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + linenum = 0; + bool foundLabelLine = false; + while (fgetline(fin, line, false)) { + linenum++; + std::vector fieldList = splitCSV(line); + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if ((lineType == unknownLineType) && (!foundLabelLine)) { + lineType = labelLineType; + foundLabelLine = 1; + } + if ((lineType == unknownLineType) && (foundLabelLine)) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + bool found; + std::string field; + switch (lineType) { + case labelLineType: + for (fieldIdx = 0; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + + // std::cout << "FIELD: \"" << field << "\"" << std::endl; + + found = false; + for (fIdx = 0; + (fIdx < (int)fieldLabelList.size()) && (!found); + fIdx++) { + if (field == fieldLabelList.at(fIdx)) { + *fieldIdxList.at(fIdx) = fieldIdx; + found = true; + } + } + } + + for (fIdx = 0; fIdx < (int)fieldIdxList.size(); fIdx++) { + if (*fieldIdxList.at(fIdx) == -1) { + errStr << "ERROR: Invalid Winner2 Test Input file " + "\"" + << inputFile << "\" label line missing \"" + << fieldLabelList.at(fIdx) << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + } + + break; + case dataLineType: + /**************************************************************************/ + /* terrainHeight */ + /**************************************************************************/ + strval = fieldList.at(terrainHeightFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Invalid ITM Test Input file \"" + << inputFile << "\" line " << linenum + << " missing Terrain Height (m)" << std::endl; + throw std::runtime_error(errStr.str()); + } + terrainHeight = std::strtod(strval.c_str(), &chptr); + /**************************************************************************/ + + heightList.push_back(terrainHeight); + + break; + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + CORE_DUMP; + break; + } + } + + int numpts = heightList.size(); + double *heightProfile = (double *)calloc(sizeof(double), numpts + 2); + + heightProfile[0] = numpts - 1; + heightProfile[1] = (groundDistanceKm / (numpts - 1)) * 1000.0; + + double eps_dielect = _itmEpsDielect; + double sgm_conductivity = _itmSgmConductivity; + double surfaceRefractivity = _ituData->getSurfaceRefractivityValue((rlanLat + fsLat) / 2, + (rlanLon + fsLon) / 2); + + int radioClimate = _ituData->getRadioClimateValue(rlanLat, rlanLon); + int radioClimateTmp = _ituData->getRadioClimateValue(fsLat, fsLon); + if (radioClimateTmp < radioClimate) { + radioClimate = radioClimateTmp; + } + + int pol = _itmPolarization; + double conf = _confidenceITM; + double rel = _reliabilityITM; + + int errnum; + double pathLoss; + std::string strmode; + // char strmode[50]; + + std::cout << "PATH_PROFILE: " << inputFile << std::endl; + std::cout << "GROUND DISTANCE (Km): " << groundDistanceKm << std::endl; + std::cout << "NUMPTS: " << numpts << std::endl; + std::cout << "DELTA (m): " << heightProfile[1] << std::endl; + + std::cout << "RLAN LON (deg): " << rlanLon << std::endl; + std::cout << "RLAN LAT (deg): " << rlanLat << std::endl; + std::cout << "FS LON (deg): " << fsLon << std::endl; + std::cout << "FS LAT (deg): " << fsLat << std::endl; + std::cout << "FS HEIGHT AGL (m) : " << fsHeightAGL << std::endl; + + std::cout << "eps_dielect: " << eps_dielect << std::endl; + std::cout << "sgm_conductivity: " << sgm_conductivity << std::endl; + std::cout << "surfaceRefractivity: " << surfaceRefractivity << std::endl; + std::cout << "frequencyMHz: " << frequencyMHz << std::endl; + std::cout << "radioClimate: " << radioClimate << std::endl; + std::cout << "pol: " << pol << std::endl; + std::cout << "conf: " << conf << std::endl; + std::cout << "rel: " << rel << std::endl; + + std::cout << std::endl; + + int numRlanHeight = 2; + double rlanHeightAGLStart = 10.0; + double rlanHeightAGLStop = 11.0; + + for (bool reverseFlag : {false}) { + for (int rlanHeightIdx = 0; rlanHeightIdx < numRlanHeight; ++rlanHeightIdx) { + double rlanHeightAGL = (rlanHeightAGLStart * + (numRlanHeight - 1 - rlanHeightIdx) + + rlanHeightAGLStop * rlanHeightIdx) / + (numRlanHeight - 1); + std::cout << "RLAN HEIGHT AGL (m) : " << rlanHeightAGL << std::endl; + + for (int i = 0; i < numpts; ++i) { + heightProfile[2 + i] = heightList[reverseFlag ? numpts - 1 - i : i]; + } + + double txHeightAGL; + double rxHeightAGL; + + if (reverseFlag) { + txHeightAGL = fsHeightAGL; + rxHeightAGL = rlanHeightAGL; + } else { + txHeightAGL = rlanHeightAGL; + rxHeightAGL = fsHeightAGL; + } + + point_to_point(heightProfile, + txHeightAGL, + rxHeightAGL, + eps_dielect, + sgm_conductivity, + surfaceRefractivity, + frequencyMHz, + radioClimate, + pol, + conf, + rel, + pathLoss, + strmode, + errnum); + + std::cout << "REVERSE_FLAG: " << reverseFlag << std::endl; + std::cout << "MODE: " << strmode << std::endl; + std::cout << "ERRNUM: " << errnum << std::endl; + std::cout << "PATH_LOSS (DB): " << pathLoss << std::endl; + std::cout << "PT," << inputFile << "," << rlanHeightAGL << "," << pathLoss + << std::endl; + } + std::cout << "PT,,," << std::endl; + } + + return; +} +/******************************************************************************************/ +#endif + +#if DEBUG_AFC +/******************************************************************************************/ +/* AfcManager::runTestWinner2() */ +/******************************************************************************************/ +void AfcManager::runTestWinner2(std::string inputFile, std::string outputFile) +{ + LOGGER_INFO(logger) << "Executing AfcManager::runTestWinner2()"; + + int linenum, fIdx, validFlag; + std::string line, strval; + char *chptr; + std::string str; + std::string reasonIgnored; + std::ostringstream errStr; + + int regionFieldIdx = -1; + int distanceFieldIdx = -1; + int hbFieldIdx = -1; + int hmFieldIdx = -1; + int frequencyFieldIdx = -1; + int confidenceFieldIdx = -1; + + std::vector fieldIdxList; + std::vector fieldLabelList; + fieldIdxList.push_back(®ionFieldIdx); + fieldLabelList.push_back("Region"); + fieldIdxList.push_back(&distanceFieldIdx); + fieldLabelList.push_back("Distance (m)"); + fieldIdxList.push_back(&hbFieldIdx); + fieldLabelList.push_back("hb (m)"); + fieldIdxList.push_back(&hmFieldIdx); + fieldLabelList.push_back("hm (m)"); + fieldIdxList.push_back(&frequencyFieldIdx); + fieldLabelList.push_back("Frequency (GHz)"); + fieldIdxList.push_back(&confidenceFieldIdx); + fieldLabelList.push_back("Confidence"); + + CConst::PropEnvEnum propEnv; + double distance; + double hb; + double hm; + double frequency; + double confidence; + + int winner2LOSValue = 0; + _winner2UnknownLOSMethod = CConst::PLOSCombineWinner2UnknownLOSMethod; + double plLOS, plNLOS, plCombined; + double zval, probLOS, pathLossCDF; + double sigmaLOS, sigmaNLOS, sigmaCombined; + std::string pathLossModelStr; + + int fieldIdx; + + if (inputFile.empty()) { + throw std::runtime_error("ERROR: No Winner2 Test File specified"); + } + + LOGGER_INFO(logger) << "Reading Winner2 Test File: " << inputFile; + + FILE *fin; + if (!(fin = fopen(inputFile.c_str(), "rb"))) { + str = std::string("ERROR: Unable to open Winner2 Test File \"") + inputFile + + std::string("\"\n"); + throw std::runtime_error(str); + } + + FILE *fout; + if (!(fout = fopen(outputFile.c_str(), "wb"))) { + errStr << std::string("ERROR: Unable to open Winner2 Test Output File \"") + + outputFile + std::string("\"\n"); + throw std::runtime_error(errStr.str()); + } + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + linenum = 0; + bool foundLabelLine = false; + while (fgetline(fin, line, false)) { + linenum++; + std::vector fieldList = splitCSV(line); + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if ((lineType == unknownLineType) && (!foundLabelLine)) { + lineType = labelLineType; + foundLabelLine = 1; + } + if ((lineType == unknownLineType) && (foundLabelLine)) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + bool found; + std::string field; + switch (lineType) { + case labelLineType: + for (fieldIdx = 0; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + + // std::cout << "FIELD: \"" << field << "\"" << std::endl; + + found = false; + for (fIdx = 0; + (fIdx < (int)fieldLabelList.size()) && (!found); + fIdx++) { + if (field == fieldLabelList.at(fIdx)) { + *fieldIdxList.at(fIdx) = fieldIdx; + found = true; + } + } + } + + for (fIdx = 0; fIdx < (int)fieldIdxList.size(); fIdx++) { + if (*fieldIdxList.at(fIdx) == -1) { + errStr << "ERROR: Invalid Winner2 Test Input file " + "\"" + << inputFile << "\" label line missing \"" + << fieldLabelList.at(fIdx) << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + } + + fprintf(fout, + "%s,%s,%s,%s,%s\n", + line.c_str(), + "afc_probLOS", + "afc_plLOS", + "afc_plNLOS", + "afc_plCombined"); + + break; + case dataLineType: + /**************************************************************************/ + /* REGION (propEnv) */ + /**************************************************************************/ + strval = fieldList.at(regionFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Invalid Winner2 Test Input file \"" + << inputFile << "\" line " << linenum + << " missing REGION" << std::endl; + throw std::runtime_error(errStr.str()); + } + + propEnv = (CConst::PropEnvEnum)CConst::strPropEnvList + ->str_to_type(strval, validFlag); + + if (!validFlag) { + errStr << "ERROR: Invalid Winner2 Test Input file \"" + << inputFile << "\" line " << linenum + << " INVALID REGION = " << strval << std::endl; + throw std::runtime_error(errStr.str()); + } + /**************************************************************************/ + + /**************************************************************************/ + /* distance */ + /**************************************************************************/ + strval = fieldList.at(distanceFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Invalid Winner2 Test Input file \"" + << inputFile << "\" line " << linenum + << " missing Distance (m)" << std::endl; + throw std::runtime_error(errStr.str()); + } + distance = std::strtod(strval.c_str(), &chptr); + /**************************************************************************/ + + /**************************************************************************/ + /* hb */ + /**************************************************************************/ + strval = fieldList.at(hbFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Invalid Winner2 Test Input file \"" + << inputFile << "\" line " << linenum + << " missing hb" << std::endl; + throw std::runtime_error(errStr.str()); + } + hb = std::strtod(strval.c_str(), &chptr); + /**************************************************************************/ + + /**************************************************************************/ + /* hm */ + /**************************************************************************/ + strval = fieldList.at(hmFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Invalid Winner2 Test Input file \"" + << inputFile << "\" line " << linenum + << " missing hm" << std::endl; + throw std::runtime_error(errStr.str()); + } + hm = std::strtod(strval.c_str(), &chptr); + /**************************************************************************/ + + /**************************************************************************/ + /* frequency */ + /**************************************************************************/ + strval = fieldList.at(frequencyFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Invalid Winner2 Test Input file \"" + << inputFile << "\" line " << linenum + << " missing Frequency (GHz)" << std::endl; + throw std::runtime_error(errStr.str()); + } + frequency = std::strtod(strval.c_str(), &chptr) * + 1.0e9; // Convert GHz to Hz + /**************************************************************************/ + + /**************************************************************************/ + /* confidence */ + /**************************************************************************/ + strval = fieldList.at(confidenceFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Invalid Winner2 Test Input file \"" + << inputFile << "\" line " << linenum + << " missing Confidence" << std::endl; + throw std::runtime_error(errStr.str()); + } + confidence = std::strtod(strval.c_str(), &chptr); + /**************************************************************************/ + + zval = -qerfi(confidence); + _zwinner2Combined = zval; + if (propEnv == CConst::urbanPropEnv) { + plLOS = Winner2_C2urban_LOS(distance, + hb, + hm, + frequency, + zval, + sigmaLOS, + pathLossCDF); + plNLOS = Winner2_C2urban_NLOS(distance, + hb, + hm, + frequency, + zval, + sigmaNLOS, + pathLossCDF); + plCombined = Winner2_C2urban(distance, + hb, + hm, + frequency, + sigmaCombined, + pathLossModelStr, + pathLossCDF, + probLOS, + winner2LOSValue); + } else if (propEnv == CConst::suburbanPropEnv) { + plLOS = Winner2_C1suburban_LOS(distance, + hb, + hm, + frequency, + zval, + sigmaLOS, + pathLossCDF); + plNLOS = Winner2_C1suburban_NLOS(distance, + hb, + hm, + frequency, + zval, + sigmaNLOS, + pathLossCDF); + plCombined = Winner2_C1suburban(distance, + hb, + hm, + frequency, + sigmaCombined, + pathLossModelStr, + pathLossCDF, + probLOS, + winner2LOSValue); + } else if (propEnv == CConst::ruralPropEnv) { + plLOS = Winner2_D1rural_LOS(distance, + hb, + hm, + frequency, + zval, + sigmaLOS, + pathLossCDF); + plNLOS = Winner2_D1rural_NLOS(distance, + hb, + hm, + frequency, + zval, + sigmaNLOS, + pathLossCDF); + plCombined = Winner2_D1rural(distance, + hb, + hm, + frequency, + sigmaCombined, + pathLossModelStr, + pathLossCDF, + probLOS, + winner2LOSValue); + } else { + CORE_DUMP; + } + + if (distance < 50.0) { + probLOS = 1.0; + plCombined = plLOS; + } + + fprintf(fout, + "%s,%.3f,%3f,%.3f,%.3f\n", + line.c_str(), + probLOS, + plLOS, + plNLOS, + plCombined); + + break; + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + CORE_DUMP; + break; + } + } + + if (fin) { + fclose(fin); + } + if (fout) { + fclose(fout); + } + + return; +} +/******************************************************************************************/ +#endif + +#if DEBUG_AFC +/******************************************************************************************/ +/* AfcManager::runAnalyzeNLCD() */ +/******************************************************************************************/ +void AfcManager::runAnalyzeNLCD() +{ + LOGGER_INFO(logger) << "Executing AfcManager::runAnalyzeNLCD()"; + + Vector3 esPosn, satPosn, apPosn; + Vector3 d, projd, eastVec, northVec, zVec; + Vector3 esEast, esNorth, esZ, esBoresight; + std::string str; + std::ostringstream errStr; + std::vector probList; + + #define DBG_POSN 0 + + FILE *fkml = (FILE *)NULL; + /**************************************************************************************/ + /* Open KML File and write header */ + /**************************************************************************************/ + if (1) { + if (!(fkml = fopen("/tmp/doc.kml", "wb"))) { + errStr << std::string("ERROR: Unable to open kmlFile \"") + "/tmp/doc.kml" + + std::string("\"\n"); + throw std::runtime_error(errStr.str()); + } + } else { + fkml = (FILE *)NULL; + } + + if (fkml) { + fprintf(fkml, "\n"); + fprintf(fkml, "\n"); + fprintf(fkml, "\n"); + fprintf(fkml, " \n"); + fprintf(fkml, " Analyze NLCD\n"); + fprintf(fkml, " 1\n"); + fprintf(fkml, + " %s : Show NLCD categories.\n", + "TEST"); + fprintf(fkml, "\n"); + fprintf(fkml, " \n"); + fprintf(fkml, " \n"); + fprintf(fkml, " \n"); + fprintf(fkml, " \n"); + fprintf(fkml, " normal\n"); + fprintf(fkml, " #style0\n"); + fprintf(fkml, " \n"); + fprintf(fkml, " \n"); + fprintf(fkml, " highlight\n"); + fprintf(fkml, " #style\n"); + fprintf(fkml, " \n"); + fprintf(fkml, " \n"); + fprintf(fkml, " \n"); + } + /**************************************************************************************/ + + GdalTransform::BoundRect nlcdBr(cgNlcd->boundRect()); + std::cout << " NLCD_TOP_RIGHT: " << nlcdBr.lonDegMax << " " << nlcdBr.latDegMax + << std::endl; + std::cout << " NLCD_BOTTOM_LEFT: " << nlcdBr.lonDegMin << " " << nlcdBr.latDegMin + << std::endl; + + std::vector colorList; + + int i; + for (i = 0; i < 255; i++) { + std::string color; + switch (i) { + case 21: + color = "221 201 201"; + break; + case 22: + color = "216 147 130"; + break; + case 23: + color = "237 0 0"; + break; + case 31: + color = "178 173 163"; + break; + case 32: + color = "249 249 249"; + break; + case 41: + color = "104 170 99"; + break; + case 42: + color = " 28 99 48"; + break; + case 43: + color = "181 201 142"; + break; + case 52: + color = "204,186,124"; + break; + + case 1: + color = " 0 249 0"; + break; + case 11: + color = " 71 107 160"; + break; + case 12: + color = "209 221 249"; + break; + case 24: + color = "170 0 0"; + break; + case 51: + color = "165 140 48"; + break; + case 71: + color = "226 226 193"; + break; + case 72: + color = "201 201 119"; + break; + case 73: + color = "153 193 71"; + break; + case 74: + color = "119 173 147"; + break; + case 81: + color = "219 216 61"; + break; + case 82: + color = "170 112 40"; + break; + case 90: + color = "186 216 234"; + break; + case 91: + color = "181 211 229"; + break; + case 92: + color = "181 211 229"; + break; + case 93: + color = "181 211 229"; + break; + case 94: + color = "181 211 229"; + break; + case 95: + color = "112 163 186"; + break; + + default: + color = "255 255 255"; + break; + } + colorList.push_back(color); + } + + char *tstr; + time_t t0 = time(NULL); + tstr = strdup(ctime(&t0)); + strtok(tstr, "\n"); + std::cout << tstr << " : ITERATION START." << std::endl; + free(tstr); + + /**************************************************************************************/ + /* Write FS RLAN Regions to KML */ + /**************************************************************************************/ + double resolutionLon = (30.0 / CConst::earthRadius) * 180.0 / M_PI; + double resolutionLat = (30.0 / CConst::earthRadius) * 180.0 / M_PI; + + std::vector imageFileList; + + /**************************************************************************************/ + /* Define regions */ + /**************************************************************************************/ + int maxPtsPerRegion = 5000; + + double longitudeDegStart = _popGrid->getMinLonDeg(); + double latitudeDegStart = _popGrid->getMinLatDeg(); + + int numLon = (_popGrid->getMaxLonDeg() - longitudeDegStart) / resolutionLon; + int numLat = (_popGrid->getMaxLatDeg() - latitudeDegStart) / resolutionLat; + + int numRegionLon = (numLon + maxPtsPerRegion - 1) / maxPtsPerRegion; + int numRegionLat = (numLat + maxPtsPerRegion - 1) / maxPtsPerRegion; + + int lonRegionIdx; + int latRegionIdx; + int startLonIdx, stopLonIdx; + int startLatIdx, stopLatIdx; + int lonN = numLon / numRegionLon; + int lonq = numLon % numRegionLon; + int latN = numLat / numRegionLat; + int latq = numLat % numRegionLat; + /**************************************************************************************/ + + std::cout << " NUM_REGION_LON: " << numRegionLon << std::endl; + std::cout << " NUM_REGION_LAT: " << numRegionLat << std::endl; + + int interpolationFactor = 1; + int interpLon, interpLat; + + fprintf(fkml, " \n"); + fprintf(fkml, " %s\n", "NLCD"); + fprintf(fkml, " 1\n"); + + for (lonRegionIdx = 0; lonRegionIdx < numRegionLon; lonRegionIdx++) { + if (lonRegionIdx < lonq) { + startLonIdx = (lonN + 1) * lonRegionIdx; + stopLonIdx = (lonN + 1) * lonRegionIdx + lonN; + } else { + startLonIdx = lonN * lonRegionIdx + lonq; + stopLonIdx = lonN * lonRegionIdx + lonq + lonN - 1; + } + + for (latRegionIdx = 0; latRegionIdx < numRegionLat; latRegionIdx++) { + if (latRegionIdx < latq) { + startLatIdx = (latN + 1) * latRegionIdx; + stopLatIdx = (latN + 1) * latRegionIdx + latN; + } else { + startLatIdx = latN * latRegionIdx + latq; + stopLatIdx = latN * latRegionIdx + latq + latN - 1; + } + + /**************************************************************************************/ + /* Create PPM File */ + /**************************************************************************************/ + FILE *fppm; + if (!(fppm = fopen("/tmp/image.ppm", "wb"))) { + throw std::runtime_error("ERROR"); + } + fprintf(fppm, "P3\n"); + fprintf(fppm, + "%d %d %d\n", + (stopLonIdx - startLonIdx + 1) * interpolationFactor, + (stopLatIdx - startLatIdx + 1) * interpolationFactor, + 255); + + int latIdx, lonIdx; + for (latIdx = stopLatIdx; latIdx >= startLatIdx; --latIdx) { + double latDeg = latitudeDegStart + (latIdx + 0.5) * resolutionLon; + for (interpLat = interpolationFactor - 1; interpLat >= 0; + --interpLat) { + for (lonIdx = startLonIdx; lonIdx <= stopLonIdx; ++lonIdx) { + double lonDeg = longitudeDegStart + + (lonIdx + 0.5) * resolutionLon; + + unsigned int landcat = + (unsigned int)cgNlcd->valueAt(latDeg, + lonDeg); + + // printf("LON = %.6f LAT = %.6f LANDCAT = %d\n", + // lonDeg, latDeg, landcat); + + std::string colorStr = colorList[landcat]; + + for (interpLon = 0; interpLon < interpolationFactor; + interpLon++) { + if (lonIdx || interpLon) { + fprintf(fppm, " "); + } + fprintf(fppm, "%s", colorStr.c_str()); + } + } + fprintf(fppm, "\n"); + } + } + + fclose(fppm); + /**************************************************************************************/ + + std::string pngFile = std::string("/tmp/image") + "_" + + std::to_string(lonRegionIdx) + "_" + + std::to_string(latRegionIdx) + ".png"; + + imageFileList.push_back(pngFile); + + std::string command = "convert /tmp/image.ppm -transparent white " + + pngFile; + std::cout << "COMMAND: " << command << std::endl; + system(command.c_str()); + + /**************************************************************************************/ + /* Write to KML File */ + /**************************************************************************************/ + fprintf(fkml, "\n"); + fprintf(fkml, + " Region: %d_%d\n", + lonRegionIdx, + latRegionIdx); + fprintf(fkml, " %d\n", 1); + fprintf(fkml, " C0ffffff\n"); + fprintf(fkml, " \n"); + fprintf(fkml, + " image_%d_%d.png\n", + lonRegionIdx, + latRegionIdx); + fprintf(fkml, " \n"); + fprintf(fkml, " \n"); + fprintf(fkml, + " %.8f\n", + latitudeDegStart + (stopLatIdx + 1) * resolutionLat); + fprintf(fkml, + " %.8f\n", + latitudeDegStart + (startLatIdx)*resolutionLat); + fprintf(fkml, + " %.8f\n", + longitudeDegStart + (stopLonIdx + 1) * resolutionLon); + fprintf(fkml, + " %.8f\n", + longitudeDegStart + (startLonIdx)*resolutionLon); + fprintf(fkml, " \n"); + fprintf(fkml, "\n"); + /**************************************************************************************/ + } + } + + fprintf(fkml, " \n"); + /**************************************************************************************/ + + /**************************************************************************************/ + /* Write end of KML and close */ + /**************************************************************************************/ + if (fkml) { + fprintf(fkml, " \n"); + fprintf(fkml, "\n"); + fclose(fkml); + + std::string command; + + std::cout << "CLEARING KMZ FILE: " << std::endl; + command = "rm -fr " + _kmlFile; + system(command.c_str()); + + command = "zip -j " + _kmlFile + " /tmp/doc.kml "; + + for (int imageIdx = 0; imageIdx < ((int)imageFileList.size()); imageIdx++) { + command += " " + imageFileList[imageIdx]; + } + std::cout << "COMMAND: " << command.c_str() << std::endl; + system(command.c_str()); + } + /**************************************************************************************/ + + delete _popGrid; + + _popGrid = (PopGridClass *)NULL; +} +/******************************************************************************************/ +#endif diff --git a/src/afc-engine/AfcManager.h b/src/afc-engine/AfcManager.h new file mode 100644 index 0000000..56352a8 --- /dev/null +++ b/src/afc-engine/AfcManager.h @@ -0,0 +1,837 @@ +// mainUtilities.h -- Reads in and writes JSON files, assigns values to +// InputParameters structure for main.cpp and exports calculations from main.cpp +#ifndef INCLUDE_AFCMANAGER_H +#define INCLUDE_AFCMANAGER_H + +#ifndef DEBUG_AFC + #define DEBUG_AFC 0 +#endif + +#define USE_BUILDING_RASTER 1 + +// Standard library +#include +#include +#define _USE_MATH_DEFINES +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// AFC Engine +#include "antenna.h" +#include "cconst.h" +#include "EcefModel.h" +#include "freq_band.h" +#include "global_fn.h" +#include "GdalHelpers.h" +#include "denied_region.h" +#include "readITUFiles.hpp" +#include "str_type.h" +#include "UlsDatabase.h" +#include "uls.h" +#include "UlsMeasurementAnalysis.h" +#include "nfa.h" +#include "prtable.h" +#include "terrain.h" +#include "CachedGdal.h" +// Loggers +#include "afclogging/ErrStream.h" +#include "afclogging/Logging.h" +#include "afclogging/LoggingConfig.h" +// rat common +#include "ratcommon/SearchPaths.h" +#include "ratcommon/CsvWriter.h" +#include "ratcommon/FileHelpers.h" +#include "ratcommon/GzipStream.h" +#include "ratcommon/ZipWriter.h" +#include "ratcommon/GzipCsv.h" +// Qt +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// Uses OGR from GDAL v1.11 +#include "ogrsf_frmts.h" +#include "data_if.h" + +#include "AfcDefinitions.h" + +template +class ListClass; +class ULSClass; +class PopGridClass; +class AntennaClass; +class RlanRegionClass; +class ExThrGzipCsv; + +namespace OpClass +{ +class OpClass +{ + public: + const int opClass; + const int bandWidth; + const int startFreq; + const std::vector channels; +}; +} + +class AfcManager +{ + public: + AfcManager(); + ~AfcManager(); + + bool isNull() + { // Checks if there are NaN values in any of the required inputs + double lat, lon, alt, minorUncert, majorUncert, heightUncert; + std::tie(lat, lon, alt) = _rlanLLA; + std::tie(minorUncert, majorUncert, heightUncert) = _rlanUncerts_m; + return (((_rlanUncertaintyRegionType == RLANBoundary::ELLIPSE || + _rlanUncertaintyRegionType == RLANBoundary::RADIAL_POLY) && + (std::isnan(lat) || std::isnan(lon))) || + (_analysisType != "HeatmapAnalysis" && std::isnan(alt)) || + (_rlanUncertaintyRegionType == RLANBoundary::ELLIPSE && + (std::isnan(minorUncert) || std::isnan(majorUncert))) || + std::isnan(heightUncert) || std::isnan(_minEIRP_dBm) || + std::isnan(_maxEIRP_dBm) || std::isnan(_IoverN_threshold_dB) || + std::isnan(_bodyLossDB) || std::isnan(_polarizationLossDB) || + (_rlanUncertaintyRegionType == RLANBoundary::ELLIPSE && + std::isnan(_rlanOrientation_deg)) || + (_ulsDatabaseList.size() == 0) || std::isnan((int)_buildingType) || + std::isnan(_confidenceBldg2109) || + _pathLossModel == CConst::unknownPathLossModel); // || + // std::isnan(_confidenceClutter2108) || std::isnan(_confidenceWinner2) || + // std::isnan(_confidenceITM) || std::isnan(_winner2ProbLOSThr)); + } + + void setAnalysisType(std::string analysisTypeVal) + { + _analysisType = analysisTypeVal; + return; + } + void setStateRoot(std::string stateRootVal) + { + _stateRoot = stateRootVal; + return; + } + + // Read all of the database information into the AfcManager + void initializeDatabases(); + + // Start assigning the values from json object into the vars in struct + void importGUIjson(const std::string &inputJSONpath); + + // Read configuration parameters for AFC + void importConfigAFCjson(const std::string &inputJSONpath, + const std::string &tempDir); + + /* create OGR layer for generateExclusionZoneJson, generateMapDataGeoJson, */ + OGRLayer *createGeoJSONLayer(const char *tmpPath, GDALDataset **dataSet); + + // create JSON object for exclusion zone to be sent to GUI + QJsonDocument generateExclusionZoneJson(); + + // create JSON object for new AFC specification + QJsonDocument generateRatAfcJson(); + + // add building database bounds to OGR layer + void addBuildingDatabaseTiles(OGRLayer *layer); + + // add denied regions to OGR layer + void addDeniedRegions(OGRLayer *layer); + + // add heatmap to OGR layer + void addHeatmap(OGRLayer *layer); + + // Export calculations and FS locations in geoJSON format for GUI + void exportGUIjson(const QString &exportJsonPath, const std::string &tempDir); + + // Generate map data geojson file + void generateMapDataGeoJson(const std::string &tempDir); + + // Print the user inputs for testing and debugging + void printUserInputs(); + + ULSClass *findULSID(int ulsID, int dbIdx, int &ulsIdx); + + // ***** Perform AFC Engine computations ***** + void compute(); // Computes all the necessary losses and stores them into the output + // member variables below + + double computeClutter452HtEl( + double txHeightM, + double distKm, + double elevationAngleRad) const; // Clutter loss from ITU-R p.452 + + std::tuple computeBeamConeLatLon( + ULSClass *uls, + LatLon rxLatLonVal, + LatLon txLatLonVal); // Calculates and stores the beam cone coordinates + + // Support command line interface with AFC Engine + void setCmdLineParams(std::string &inputFilePath, + std::string &configFilePath, + std::string &outputFilePath, + std::string &tempDir, + std::string &logLevel, + int argc, + char **argv); + + void setConstInputs(const std::string &tempDir); // set inputs not specified by user + + void setFixedBuildingLossFlag(bool fixedBuildingLossFlag) + { + _fixedBuildingLossFlag = fixedBuildingLossFlag; + } + void setFixedBuildingLossValue(double fixedBuildingLossValue) + { + _fixedBuildingLossValue = fixedBuildingLossValue; + } + + void clearData(); + void clearULSList(); + + void readULSData( + const std::vector> &ulsDatabaseList, + PopGridClass *popGrid, + int linkDirection, + double minFreq, + double maxFreq, + bool removeMobileFlag, + CConst::SimulationEnum simulationFlag, + const double &minLat = -90, + const double &maxLat = 90, + const double &minLon = -180, + const double &maxLon = 180); // Reads a database for FS stations information + double getAngleFromDMS(std::string dmsStr); + int findULSAntenna(std::string strval); + bool computeSpectralOverlapLoss( + double *spectralOverlapLossDBptr, + double sigStartFreq, + double sigStopFreq, + double rxStartFreq, + double rxStopFreq, + bool aciFlag, + CConst::SpectralAlgorithmEnum spectralAlgorithm) const; + + void readDeniedRegionData(std::string filename); + + void computePathLoss(CConst::PathLossModelEnum pathLossModel, + bool itmFSPLFlag, + CConst::PropEnvEnum propEnv, + CConst::PropEnvEnum propEnvRx, + CConst::NLCDLandCatEnum nlcdLandCatTx, + CConst::NLCDLandCatEnum nlcdLandCatRx, + double distKm, + double fsplDistKm, + double win2DistKm, + double frequency, + double txLongitudeDeg, + double txLatitudeDeg, + double txHeightM, + double elevationAngleTxDeg, + double rxLongitudeDeg, + double rxLatitudeDeg, + double rxHeightM, + double elevationAngleRxDeg, + double &pathLoss, + double &pathClutterTxDB, + double &pathClutterRxDB, + std::string &pathLossModelStr, + double &pathLossCDF, + std::string &pathClutterTxModelStr, + double &pathClutterTxCDF, + std::string &pathClutterRxModelStr, + double &pathClutterRxCDF, + std::string *txClutterStrPtr, + std::string *rxClutterStrPtr, + double **ITMProfilePtr, + double **isLOSProfilePtr, + double *isLOSSurfaceFracPtr +#if DEBUG_AFC + , + std::vector &ITMHeightType +#endif + ) const; + + double q(double Z) const; + double computeBuildingPenetration(CConst::BuildingTypeEnum buildingType, + double elevationAngleDeg, + double frequency, + std::string &buildingPenetrationModelStr, + double &buildingPenetrationCDF) const; + + double computeNearFieldLoss(double frequency, + double maxGain, + double distance) const; + + // Winner II models + double Winner2_C1suburban_LOS(double distance, + double hBS, + double hMS, + double frequency, + double zval, + double &sigma, + double &pathLossCDF) const; + double Winner2_C1suburban_NLOS(double distance, + double hBS, + double hMS, + double frequency, + double zval, + double &sigma, + double &pathLossCDF) const; + double Winner2_C2urban_LOS(double distance, + double hBS, + double hMS, + double frequency, + double zval, + double &sigma, + double &pathLossCDF) const; + double Winner2_C2urban_NLOS(double distance, + double hBS, + double hMS, + double frequency, + double zval, + double &sigma, + double &pathLossCDF) const; + double Winner2_D1rural_LOS(double distance, + double hBS, + double hMS, + double frequency, + double zval, + double &sigma, + double &pathLossCDF) const; + double Winner2_D1rural_NLOS(double distance, + double hBS, + double hMS, + double frequency, + double zval, + double &sigma, + double &pathLossCDF) const; + + double Winner2_C1suburban(double distance, + double hBS, + double hMS, + double frequency, + double &sigma, + std::string &pathLossModelStr, + double &pathLossCDF, + double &probLOS, + int losValue) const; + double Winner2_C2urban(double distance, + double hBS, + double hMS, + double frequency, + double &sigma, + std::string &pathLossModelStr, + double &pathLossCDF, + double &probLOS, + int losValue) const; + double Winner2_D1rural(double distance, + double hBS, + double hMS, + double frequency, + double &sigma, + std::string &pathLossModelStr, + double &pathLossCDF, + double &probLOS, + int losValue) const; + + void computeInquiredFreqRangesPSD( + std::vector + &psdFreqRangeList); // Compute list of psdSegments for each inquired + // frequency range + + void defineHeatmapColors(); + std::string getHeatmapColor(double itonVal, bool indoorFlag, bool hexFlag); + + private: + void importGUIjsonVersion1_4(const QJsonObject &jsonObj); + + void runPointAnalysis(); + std::vector getSortedUls(); + void runScanAnalysis(); + void runExclusionZoneAnalysis(); + void runHeatmapAnalysis(); + void writeKML(); + void createChannelList(); + void splitFrequencyRanges(); + bool containsChannel(const std::vector &freqBandList, + int chanStartFreqMHz, + int chanStopFreqMHz); + // Returns 1 is successful, 0 of cfi invalid + std::vector> calculateOverlapBandList( + const std::vector &freqBandList, + int chanStartFreqMHz, + int chanStopFreqMHz); + + void fixFSTerrain(); + CConst::PropEnvEnum computePropEnv(double lonDeg, + double latDeg, + CConst::NLCDLandCatEnum &nlcdLandCat, + bool errorFlag = true) const; + double computeIToNMargin(double d, + double cc, + double ss, + ULSClass *uls, + double chanCenterFreq, + double bandwidth, + double chanStartFreq, + double chanStopFreq, + double spectralOverlapLossDB, + char ulsRxPropEnv, + double &distKmM, + std::string comment, + ExThrGzipCsv *excthrGc); + + /**************************************************************************************/ + /* Input Parameters */ + /**************************************************************************************/ + std::string _srtmDir; // Directory that contains SRTM terrain height files + std::string _cdsmDir; // Directory that contains CDSM (Canadian Digital Surface + // Model) height files + std::string _depDir; // Directory that contains 3DEP terrain height files + std::string _globeDir; // Directory that contains NOAA GLOBE terrain data files + std::string _lidarDir; // Directory that contains LiDAR multiband raster files. + + std::string + _regionDir; // Directory that contains polygons for each simulation region + + std::string + _worldPopulationFile; // GDAL file (tiff) containing population density data + std::string _nlcdFile; // GDAL file contining NLCD data + std::string _rainForestFile; // KML file contining rain forest polygon + std::string _nfaTableFile; // File containing near field adjustment tabular data + // described in R2-AIP-17 + std::string _radioClimateFile; // ITU radio climate data + std::string _surfRefracFile; // ITU surface refractivity data + + std::vector> _ulsDatabaseList; + // List of tuples where each tuple contains database name and database sqlite file + // name. + + std::string _analysisType; // Parsed Analysis Type: "AP-AFC", + // "ExclusionZoneAnalysis", "HeatmapAnalysis"; + std::string _stateRoot; // Parsed path of fbrat state root + std::string _mntPath; // Parsed path to share with GeoData and config data" + bool _createKmz; + bool _createDebugFiles; + bool _createSlowDebugFiles; + bool _certifiedIndoor; + + AfcDataIf *_dataIf; + + RLANBoundary _rlanUncertaintyRegionType = + RLANBoundary::NO_BOUNDARY; // specifies the type of horizontal uncertainty + // region being used (ellipse, linear polygon, or + // radial polygon) + DoubleTriplet _rlanLLA = + std::make_tuple(quietNaN, + quietNaN, + quietNaN); // lat (deg) (NaN if not ellipse), lon (deg) (NaN + // if not ellipse), height (m) + DoubleTriplet _rlanUncerts_m = std::make_tuple( + quietNaN, + quietNaN, + quietNaN); // minor uncertainity (NaN if not ellipse), major uncertainity + // (NaN if not ellipse), height uncertainty + std::vector _rlanLinearPolygon = std::vector(); + std::vector _rlanRadialPolygon = std::vector(); + bool _allowScanPtsInUncRegFlag; + + CConst::ScanRegionMethodEnum _scanRegionMethod; + + int _scanres_points_per_degree; + double _scanres_xy, _scanres_ht; + bool _indoorFixedHeightAMSL; + + double _maxVerticalUncertainty; + double _maxHorizontalUncertaintyDistance; + + // Method used to treat RLAN uncertainty region scan points that have an AGL height + // less than _minRlanHeightAboveTerrain DiscardScanPointBelowGroundMethod : Discard + // these scan points TruncateScanPointBelowGroundMethod : Set the AGL height if + // these scan points to _minRlanHeightAboveTerrain + CConst::ScanPointBelowGroundMethodEnum _scanPointBelowGroundMethod; + + double _minEIRPIndoor_dBm = quietNaN; // minimum Indoor RLAN EIRP (in dBm) + double _minEIRPOutdoor_dBm = quietNaN; // minimum Outdoor RLAN EIRP (in dBm) + double _minEIRP_dBm; // minimum RLAN EIRP (in dBm) + double _maxEIRP_dBm; // maximum RLAN EIRP (in dBm) + double _minPSD_dBmPerMHz = quietNaN; // minimum RLAN PSD (in dBm/Hz) + + double _reportUnavailPSDdBmPerMHz; // Unavailable channels inside uncertinty volume + // or in uncertainty footprint are reported at + // this level; + + double _IoverN_threshold_dB; // IoverN not to exceed this value for a viable channel + double _bodyLossIndoorDB; // Indoor body Loss (dB) + double _bodyLossOutdoorDB; // Outdoor body Loss (dB) + double _polarizationLossDB; // Polarization Loss (dB) + double _rlanOrientation_deg; // Orientation (deg) of ellipse clockwise from North in + // [-90, 90] + RLANType _rlanType; + CConst::HeightTypeEnum + _rlanHeightType; // Above Mean Sea Level (AMSL), Above Ground Level (AGL) + QString _serialNumber; + QString _requestId; + QString _rulesetId; + QString _guiJsonVersion; + + std::vector> _inquiredFrequencyRangesMHz = + std::vector>(); // list of low-high frequencies in MHz + + // first part of pair is global operating class and the second is a list of the + // channel indicies for that operating class + std::vector>> _inquiredChannels = + std::vector>>(); + + QString _buildingLossModel; + CConst::BuildingTypeEnum _buildingType; // Defaults to traditionalBuildingType + + bool _fixedBuildingLossFlag; // If set, use fixed building loss value, otherwise run + // P.2109 + double _fixedBuildingLossValue; // Building loss value to use if + // _fixedBuildingLossFlag is set + + double _confidenceBldg2109; // Statistical confidence for P.2109 building + // penetration loss + double _confidenceClutter2108; // Statistical confidence for P.2108 clutter loss + double _confidenceWinner2LOS; // Statistical confidence for Winner2 LOS path loss + // model + double _confidenceWinner2NLOS; // Statistical confidence for Winner2 NLOS path loss + // model + double _confidenceWinner2Combined; // Statistical confidence for Winner2 combined + // path loss model + double _confidenceITM; // Statistical confidence for ITM path loss model + double _reliabilityITM; // Statistical reliability for ITM path loss model + + CConst::LOSOptionEnum _winner2LOSOption; // Method used to determine LOS for Winner2 + // LOS Unknown, always use _winner2UnknownLOSMethod + // BldgDataWinner2LOSOption : use building data + // ForceLOSWinner2LOSOption : Always use LOS + // ForceNLOSWinner2LOSOption : Always use NLOS + + CConst::SpectralAlgorithmEnum _channelResponseAlgorithm; + // pwrSpectralAlgorithm : use power method + // psdSpectralAlgorithm : use psd method + + CConst::Winner2UnknownLOSMethodEnum + _winner2UnknownLOSMethod; // Method used to compute Winner2 PL when LOS not + // known + // PLOSCombineWinner2UnknownLOSMethod : Compute probLOS, then combine + // PLOSThresholdWinner2UnknownLOSMethod : Compute probLOS, use LOS if exceeds + // _winner2ProbLOSThr + + double _winner2ProbLOSThr; // Winner2 prob LOS threshold, if probLOS exceeds + // threshold, use LOS model, otherwise use NLOS + // bool _winner2CombineFlag; // Whether or not to combine LOS and NLOS + // path loss values in Winner2. bool _winner2BldgLOSFlag; // If set, + // use building data to determine if winner2 LOS or NLOS model is used + + bool _winner2UseGroundDistanceFlag; // If set, use ground distance in winner2 model + bool _fsplUseGroundDistanceFlag; // If set, use ground distance for FSPL + + QString _inputULSDatabaseStr; // ULS Database being used + + CConst::PropEnvMethodEnum + _propEnvMethod; // Method for determining propagation environment (e.g. + // Population Density Map) + + double _rxFeederLossDBIDU; // User-inputted ULS receiver feeder loss for IDU + // Architecture + double _rxFeederLossDBODU; // User-inputted ULS receiver feeder loss for ODU + // Architecture + double _rxFeederLossDBUnknown; // User-inputted ULS receiver feeder loss for Unknown + // Architecture + + std::vector + _noisePSDFreqList; // Freq list for specification of noise PSD (Hz) + std::vector _noisePSDList; // Noise PSD for each band determined by + // _noisePSDFreqList (dBW/Hz) + + double _itmEpsDielect; + double _itmSgmConductivity; + int _itmPolarization; + double _itmMinSpacing; // Min spacing, in meters, between points in ITM path profile + int _itmMaxNumPts; // Max number of points to use in ITM path profile + + QJsonObject _deviceDesc; // parsed device description to be returned in response + + int _exclusionZoneFSID; // FSID to use for Exclusion Zone Analysis + int _exclusionZoneRLANChanIdx; // RLAN channel Index to use for Exclusion Zone + // Analysis + double _exclusionZoneRLANBWHz; // RLAN bandwidth (Hz) to use for Exclusion Zone + // Analysis + double _exclusionZoneRLANEIRPDBm; // RLAN EIRP (dBm) to use for Exclusion Zone + // Analysis + + double _heatmapMinLon; // Min Lon for region in which Heatmap Analysis is performed + double _heatmapMaxLon; // Max Lon for region in which Heatmap Analysis is performed + double _heatmapMinLat; // Min Lat for region in which Heatmap Analysis is performed + double _heatmapMaxLat; // Max Lat for region in which Heatmap Analysis is performed + double _heatmapRLANSpacing; // Maximum spacing (m) between points in Heatmap + // Analysis + std::string _heatmapIndoorOutdoorStr; // Can be: "Indoor", "Outdoor", "Database" + std::string _heatmapAnalysisStr; // Can be: "iton", "availability" + int _heatmapFSID; // Set to FSID for single FS analysis, -1 otherwise + + std::vector _heatmapColorList; // Color list for Heatmap Analysis + std::vector _heatmapIndoorThrList; // I/N threshold list for indoor RLAN in + // Heatmap Analysis + std::vector _heatmapOutdoorThrList; // I/N threshold list for indoor RLAN in + // Heatmap Analysis + + double _heatmapRLANIndoorEIRPDBm; // RLAN Indoor EIRP (dBm) to use for Heatmap + // Analysis + std::string _heatmapRLANIndoorHeightType; // Above Mean Sea Level (AMSL), Above + // Ground Level (AGL) for Indoor RLAN's + double _heatmapRLANIndoorHeight; // RLAN Indoor Height (m) to use for Heatmap + // Analysis + double _heatmapRLANIndoorHeightUncertainty; // RLAN Indoor Height Uncertainty (m) to + // use for Heatmap Analysis + + double _heatmapRLANOutdoorEIRPDBm; // RLAN Outdoor EIRP (dBm) to use for Heatmap + // Analysis + std::string + _heatmapRLANOutdoorHeightType; // Above Mean Sea Level (AMSL), Above Ground + // Level (AGL) for OutIndoor RLAN's + double _heatmapRLANOutdoorHeight; // RLAN Outdoor Height (m) to use for Heatmap + // Analysis + double _heatmapRLANOutdoorHeightUncertainty; // RLAN Outdoor Height Uncertainty (m) + // to use for Heatmap Analysis + + bool _applyClutterFSRxFlag; + bool _allowRuralFSClutterFlag; + double _fsConfidenceClutter2108; + double _maxFsAglHeight; + + CConst::ITMClutterMethodEnum _rlanITMTxClutterMethod; + + std::vector + _allowableFreqBandList; // List of allowable freq bands. For USA, + // correspond to UNII-5 and UNII-7 + std::string _mapDataGeoJsonFile; // File to write map data geojson + std::string _deniedRegionFile; // File containing data on denied geographic regions + double _inquiredFrequencyMaxPSD_dBmPerMHz; // Max PSD for inquired frequency + // analysis + + AntennaClass *_rlanAntenna; + Vector3 _rlanPointing; + double _rlanAzimuthPointing, _rlanElevationPointing; + /**************************************************************************************/ + + /**************************************************************************************/ + /* Constant Parameters */ + /**************************************************************************************/ + bool _useBDesignFlag = false; // Force use B-Design3D building data in Manhattan + bool _useLiDAR = false; // flag to enable use of LiDAR files for computation + bool _use3DEP = false; // flag to enable use of 3DEP 10m terrain data + + double _cdsmLOSThr; // Fraction of points in path profile below which path is + // considered to not have CDSM data + + double _minRlanHeightAboveTerrain; // Min height above terrain for RLAN + + double _maxRadius; // Max link distance to consider, links longer are ignored + double _exclusionDist; // Min link distance to consider, links shorter are ignored + + bool _nearFieldAdjFlag; // If set compute near field loss, otherwise near field loss + // is 0 + bool _passiveRepeaterFlag; // If set compute passive repeaters, otherwise ignore + // passive repeaters + bool _reportErrorRlanHeightLowFlag; // If set, report an error when all scan points + // are below _minRlanHeightAboveTerrain + double _illuminationEfficiency; // Illumination Efficiency value to use for near + // field loss calculation", + bool _closeInHgtFlag; // Whether or not to force LOS when mobile height above + // closeInHgtLOS for close in model", + double _closeInHgtLOS; // RLAN height above which prob of LOS = 100% for close in + // model", + double _closeInDist; // Radius in which close in path loss model is used + std::string _closeInPathLossModel; // Close in path loss model is used + bool _pathLossClampFSPL; // If set, when path loss < fspl, clamp to fspl value + bool _printSkippedLinksFlag; // If set, links that are skipped in the analysis + // because using FSPL does not limit I/N performance + // are still printed in exc_thr file. This + bool _roundPSDEIRPFlag; // If set, round down PSD and EIRP values in output json to + // nearest multiple of 0.1 dB. is useful for debugging, but + // depending on visibility threshold setting may impact + // execution speed. + + int _wlanMinFreqMHz; // Min Frequency for WiFi system (integer in MHz) + int _wlanMaxFreqMHz; // Max Frequency for WiFi system (integer in MHz) + double _wlanMinFreq; // Min Frequency for WiFi system (double in Hz) + double _wlanMaxFreq; // Max Frequency for WiFi system (double in Hz) + std::vector _opClass; + std::vector _psdOpClassList; + + std::string _regionStr; // Comma separated list of names of regions in sim, corresp + // to pop density file + std::string _regionPolygonFileList; // Comma separated list of KML files, one for + // each region in simulation + std::vector + _regionPolygonList; // Polygon list, multiple polygons for each region + double _regionPolygonResolution; // Resolution to use for polygon vertices, 1.0e-5 + // corresp to 1.11m, should not have to change + PolygonClass *_rainForestPolygon; // Polygon that defines rain forest region + + double _densityThrUrban; // Population density threshold above which region is + // considered URBAN + double _densityThrSuburban; // Population density above this thr and below urban thr + // is SUBURBAN + double _densityThrRural; // Population density above this thr and below suburban thr + // is RURAL + + bool _removeMobile; // If set to true, mobile entries are removed when reading ULS + // file + + bool _filterSimRegionOnly; // Filter ULS file only for in/out of simulation region + + CConst::ULSAntennaTypeEnum + _ulsDefaultAntennaType; // Default ULS antenna type to use when antenna + // pattern is not otherwise specified. + + // std::string _rlanBWStr; // Comma separated list of RLAN + // bandwidths (Hz), "b0,b1,b2" + + double _visibilityThreshold; // I/N threshold to determine whether or not an RLAN is + // visible to an FS + std::string _excThrFile; // Generate file containing data for wifi devices where + // single entry I/N > visibility Threshold + std::string _eirpGcFile; // Generate file containing data for EIRP computation + std::string _fsAnomFile; // Generate file containing anomalous FS entries + std::string _userInputsFile; // Generate file containing user inputs + std::string _kmlFile; // Generate kml file showing simulation results, primarily for + // debugging + std::string _fsAnalysisListFile; // File containing list of FS used in the analysis + int _maxLidarRegionLoadVal; + /**************************************************************************************/ + + /**************************************************************************************/ + /* Data */ + /**************************************************************************************/ + TerrainClass *_terrainDataModel; // Contains building/terrain data, auto falls back + // to SRTM -> Population + + double _bodyLossDB; // Body Loss (dB) + + std::vector _regionNameList; + std::vector _regionIDList; + int _numRegion; + + PopGridClass *_popGrid; // Population data stored in here after being read in for a + // particular city/region + + std::vector _rlanBWList; // In this case four elements (20MHz, 40MHz, etc.) + + ListClass + *_ulsList; // List of the FS stations that are being used in the analysis + + std::vector + _deniedRegionList; // List of the denied regions. This includes RAS (Radio + // Astronomy Stations) and other regions for which + // channels are denied. + + std::shared_ptr> cgNlcd; // NLCD data accessor + + std::vector _antennaList; + + CConst::PathLossModelEnum _pathLossModel; + + double _zbldg2109; + double _zclutter2108; + double _fsZclutter2108; + double _zwinner2LOS; + double _zwinner2NLOS; + double _zwinner2Combined; + + std::vector + _ulsIdxList; // Stores the indices of the ULS stations we are analyzing + DoubleTriplet _beamConeLatLons; // Stores beam cone coordinates together to be + // loaded into geometries + + RlanRegionClass *_rlanRegion; // RLAN Uncertainty Region + + ITUDataClass *_ituData; + NFAClass *_nfa; + PRTABLEClass *_prTable; + std::string _prTableFile; // File containing passive repeater tabular data described + // in WINNF-TS-1014-V1.2.0-App02 + /**************************************************************************************/ + + /**************************************************************************************/ + /* Output Parameters */ + /**************************************************************************************/ + std::vector + FSLatLon; // Three vertices for complete coverage triangle in lat/lon + std::vector calculatedIoverN; + std::vector EIRPMask; // Maximum EIRP for a given channel frequency + + std::vector + _channelList; // List of channels, each channel identified by startFreq, + // stopFreq. Computed results are availability and + // eirp_limit_dbm + bool _aciFlag; // If set, consider ACI in the overal interference calculation + + std::vector + _exclusionZone; // List of vertices of exclusion zone contour (Lon, Lat) + double _exclusionZoneFSTerrainHeight; // Terrain height at location of FS used in + // exclusion zone analysis + double _exclusionZoneHeightAboveTerrain; // Height above terrain for FS used in + // exclusion zone analysis + + double ** + _heatmapIToNDB; // Matrix of I/N values for heatmap + // _heatmapIToNDB[lonIdx][latIdx], lonIdx in + // [0,_heatmapNumPtsLon-1] latIdx in [0,_heatmapNumPtsLat-1] + bool **_heatmapIsIndoor; // Matrix of bool values: true for indoor, false for + // outdoor for grid point (lonIdx, latIdx) + int _heatmapNumPtsLon; // Num LON values in heatmap matrix + int _heatmapNumPtsLat; // Num LAT values in heatmap matrix + double _heatmapMinIToNDB; // Min I/N in _heatmapIToNDB + double _heatmapMaxIToNDB; // Max I/N in _heatmapIToNDB + double _heatmapIToNThresholdDB; // I/N threshold value used to determine colors in + // heatmap graphical desplay + double _heatmapMaxRLANHeightAGL; // Max AGL RLAN height over gridpoints in heatmap + // analysis + Vector3 _heatmapRLANCenterPosn; // Center gridpoint in heatmap analysis + double _heatmapRLANCenterLon; // Longitude of center gridpoint in heatmap analysis + double _heatmapRLANCenterLat; // Latitude of center gridpoint in heatmap analysis + + std::vector + statusMessageList; // List of messages regarding run status to send to GUI + CConst::ResponseCodeEnum _responseCode; + QStringList _missingParams; + QStringList _invalidParams; + QStringList _unexpectedParams; + /**************************************************************************************/ + + // bool _configChange = false; + +#if DEBUG_AFC + void runTestITM(std::string inputFile); + void runTestWinner2(std::string inputFile, std::string outputFile); + void runAnalyzeNLCD(); +#endif +}; + +#endif // INCLUDE_AFCMANAGER_H diff --git a/src/afc-engine/CMakeLists.txt b/src/afc-engine/CMakeLists.txt new file mode 100644 index 0000000..d21b7b2 --- /dev/null +++ b/src/afc-engine/CMakeLists.txt @@ -0,0 +1,19 @@ +# All source files to same target +set(TGT_NAME "afc-engine") + +file(GLOB ALL_CPP "*.cpp") +file(GLOB ALL_HEADER "*.h") +add_dist_executable(TARGET ${TGT_NAME} SOURCES ${ALL_CPP} ${ALL_HEADER}) + +target_link_libraries(${TGT_NAME} PUBLIC Qt5::Core) +target_link_libraries(${TGT_NAME} PUBLIC Qt5::Concurrent) +target_link_libraries(${TGT_NAME} PUBLIC Qt5::Gui) +target_link_libraries(${TGT_NAME} PUBLIC Qt5::Widgets) +target_link_libraries(${TGT_NAME} PUBLIC Qt5::Sql) +target_link_libraries(${TGT_NAME} PUBLIC GDAL::GDAL) +target_link_libraries(${TGT_NAME} PUBLIC ${ARMADILLO_LIBRARIES}) +target_link_libraries(${TGT_NAME} PUBLIC ${ZLIB_LIBRARIES}) +target_link_libraries(${TGT_NAME} PUBLIC Boost::program_options) +target_link_libraries(${TGT_NAME} PUBLIC ratcommon) +target_link_libraries(${TGT_NAME} PUBLIC afclogging) +target_link_libraries(${TGT_NAME} PUBLIC afcsql) diff --git a/src/afc-engine/CachedGdal.cpp b/src/afc-engine/CachedGdal.cpp new file mode 100644 index 0000000..7bee306 --- /dev/null +++ b/src/afc-engine/CachedGdal.cpp @@ -0,0 +1,620 @@ +/* + * Copyright (C) 2022 Broadcom. All rights reserved. + * The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate + * that owns the software below. + * This work is licensed under the OpenAFC Project License, a copy of which is + * included with this software program. + */ +#include "CachedGdal.h" +#include +#include +#include +#include +#include +#include + +namespace +{ +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "CachedGdal") + +} // end namespace + +/////////////////////////////////////////////////////////////////////////////// +// CachedGdalBase::GdalInfo +/////////////////////////////////////////////////////////////////////////////// + +CachedGdalBase::GdalInfo::GdalInfo( + const GdalDatasetHolder *gdalDataset, + int minBands, + const boost::optional> &transformationModifier) : + baseName(boost::filesystem::path(gdalDataset->fullFileName).filename().string()), + transformation(gdalDataset->gdalDataset, baseName), + numBands(minBands) +{ + if (transformationModifier) { + transformationModifier.get()(&transformation); + } + boundRect = transformation.makeBoundRect(); + std::ostringstream errStr; + boost::system::error_code systemErr; + if (gdalDataset->gdalDataset->GetRasterCount() < minBands) { + errStr << "ERROR: CachedGdalBase::GdalData::GdalData(): GDAL data file '" + << baseName << "' has only " << gdalDataset->gdalDataset->GetRasterCount() + << " bands, whereas at least " << minBands << "is expected"; + throw std::runtime_error(errStr.str()); + } + for (int i = 0; i < minBands; ++i) { + noDataValues.push_back( + gdalDataset->gdalDataset->GetRasterBand(i + 1)->GetNoDataValue()); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// CachedGdalBase::TileKey +/////////////////////////////////////////////////////////////////////////////// + +CachedGdalBase::TileKey::TileKey(int band_, + int latOffset_, + int lonOffset_, + const std::string &baseName_) : + band(band_), latOffset(latOffset_), lonOffset(lonOffset_), baseName(baseName_) +{ +} + +CachedGdalBase::TileKey::TileKey() : band(0), latOffset(0), lonOffset(0), baseName() +{ +} + +bool CachedGdalBase::TileKey::operator<(const TileKey &other) const +{ + if (band != other.band) + return band < other.band; + if (latOffset != other.latOffset) + return latOffset < other.latOffset; + if (lonOffset != other.lonOffset) + return lonOffset < other.lonOffset; + return baseName < other.baseName; +} + +/////////////////////////////////////////////////////////////////////////////// +// CachedGdalBase::TileInfo +/////////////////////////////////////////////////////////////////////////////// + +CachedGdalBase::TileInfo::TileInfo(CachedGdalBase *cachedGdal_, + const GdalTransform &transformation_, + const GdalInfo *gdalInfo_) : + cachedGdal(cachedGdal_), + transformation(transformation_), + boundRect(transformation_.makeBoundRect()), + gdalInfo(gdalInfo_), + tileVector(cachedGdal_->createTileVector(transformation_.latSize, transformation_.lonSize), + [cachedGdal_](void *p) { + cachedGdal_->deleteTileVector(p); + }) +{ +} + +CachedGdalBase::TileInfo::TileInfo() : cachedGdal(nullptr), gdalInfo(nullptr) +{ +} + +/////////////////////////////////////////////////////////////////////////////// +// CachedGdalBase::GdalDatasetHolder +/////////////////////////////////////////////////////////////////////////////// + +CachedGdalBase::GdalDatasetHolder::GdalDatasetHolder(const std::string &fullFileName_) : + fullFileName(fullFileName_) +{ + std::ostringstream errStr; + boost::system::error_code systemErr; + if (!boost::filesystem::is_regular_file(fullFileName, systemErr)) { + errStr << "ERROR: CachedGdalBase::GdalDatasetHolder::GdalDatasetHolder(): " + "GDAL data file '" + << fullFileName << "' not found"; + throw std::runtime_error(errStr.str()); + } + gdalDataset = static_cast(GDALOpen(fullFileName.c_str(), GA_ReadOnly)); + if (!gdalDataset) { + errStr << "ERROR: CachedGdalBase::GdalDatasetHolder::GdalDatasetHolder(): " + "Error opening GDAL data file '" + << fullFileName << "': " << CPLGetLastErrorMsg(); + throw std::runtime_error(errStr.str()); + } + LOGGER_DEBUG(logger) << "Opened GDAL file '" << fullFileName << "'"; +} + +CachedGdalBase::GdalDatasetHolder::~GdalDatasetHolder() +{ + GDALClose(gdalDataset); +} + +/////////////////////////////////////////////////////////////////////////////// +// CachedGdalBase::PixelInfo +/////////////////////////////////////////////////////////////////////////////// + +CachedGdalBase::PixelInfo::PixelInfo(const std::string &baseName_, int row_, int column_) : + baseName(baseName_), row(row_), column(column_) +{ +} + +CachedGdalBase::PixelInfo::PixelInfo() : baseName(""), row(-1), column(-1) +{ +} + +/////////////////////////////////////////////////////////////////////////////// +// CachedGdalBase +/////////////////////////////////////////////////////////////////////////////// + +CachedGdalBase::CachedGdalBase(std::string fileOrDir, + const std::string &dsName, + std::unique_ptr nameMapper, + int numBands, + int maxTileSize, + int cacheSize, + GDALDataType pixelType) : + _fileOrDir(fileOrDir), + _dsName(dsName), + _nameMapper(std::move(nameMapper)), + _numBands(numBands), + _pixelType(pixelType), + _maxTileSize(maxTileSize), + _tileCache(cacheSize), + _gdalDsCache(GDAL_CACHE_SIZE), + _recentGdalInfo(nullptr), + _allSeen(false) +{ + GDALAllRegister(); +} + +void CachedGdalBase::initialize() +{ + LOGGER_INFO(logger) << "Initializing access to '" << _fileOrDir + << (isMonolithic() ? "' GDAL file " : "' GDAL file directory ") + << "containing " << (dsName().empty() ? "?some?" : dsName()) + << " data. Assumed pixel data type is " + << GDALGetDataTypeName(_pixelType) << ", number of bands is " + << _numBands; + boost::system::error_code systemErr; + std::ostringstream errStr; + if (isMonolithic()) { + if (!boost::filesystem::is_regular_file(_fileOrDir, systemErr)) { + errStr << "ERROR: CachedGdalBase::initialize(): " + "GDAL file '" + << _fileOrDir << "' not found"; + throw std::runtime_error(errStr.str()); + } + std::string baseName = boost::filesystem::path(_fileOrDir).filename().string(); + addGdalInfo(baseName, getGdalDatasetHolder(_fileOrDir)); + _allSeen = true; + } else { + if (!boost::filesystem::is_directory(_fileOrDir, systemErr)) { + errStr << "ERROR: CachedGdalBase::initialize(): " + "GDAL data directory '" + << _fileOrDir << "' not found or is not a directory"; + throw std::runtime_error(errStr.str()); + } + if (!forEachGdalInfo([](const GdalInfo &gdalInfo) { + return true; + })) { + errStr << "ERROR: CachedGdalBase::initialize(): " + "GDAL data directory '" + << _fileOrDir + << "' does not contain files matching fnmatch pattern '" + << _nameMapper->fnmatchPattern() << "'"; + throw std::runtime_error(errStr.str()); + } + } +} + +void CachedGdalBase::cleanup() +{ + _tileCache.clear(); +} + +const std::string &CachedGdalBase::dsName() const +{ + return _dsName; +} + +void CachedGdalBase::setTransformationModifier(std::function modifier) +{ + _transformationModifier = modifier; + rereadGdal(); +} + +bool CachedGdalBase::isMonolithic() const +{ + return !_nameMapper.get(); +} + +void CachedGdalBase::rereadGdal() +{ + _tileCache.clear(); + std::string anyBaseName = _gdalInfos.begin()->first; + _gdalInfos.clear(); + addGdalInfo(anyBaseName, getGdalDatasetHolder(anyBaseName)); + if (!isMonolithic()) { + _allSeen = false; + } +} + +bool CachedGdalBase::forEachGdalInfo( + const std::function &op) +{ + // First iterating over previously seen (multifile and monolithic case) + for (const auto &it : _gdalInfos) { + const GdalInfo *gdalInfo = it.second.get(); + if (gdalInfo && op(*gdalInfo)) { + return true; + } + } + // Are there any not yet seen GDAL files? + if (_allSeen) { + return false; + } + // Iterating over not yet seen GDAL files + std::string fnmatch_pattern = _nameMapper->fnmatchPattern(); + for (boost::filesystem::directory_iterator di(_fileOrDir); + di != boost::filesystem::directory_iterator(); + ++di) { + std::string baseName = di->path().filename().native(); + // Skipping seen, nonmatching and nonfiles + if ((_gdalInfos.find(baseName) != _gdalInfos.end()) || + (fnmatch(fnmatch_pattern.c_str(), baseName.c_str(), 0) == FNM_NOMATCH) || + (!boost::filesystem::is_regular_file(di->path()))) { + continue; + } + const GdalInfo *gdalInfo = addGdalInfo(baseName, getGdalDatasetHolder(baseName)); + if (op(*gdalInfo)) { + return true; + } + } + _allSeen = true; + return false; +} + +const void *CachedGdalBase::getTileVector(int band, double latDeg, double lonDeg, int *pixelIndex) +{ + checkBandIndex(band); + if (!findTile(band, latDeg, lonDeg)) { + return nullptr; + } + const TileInfo &tileInfo(*_tileCache.recentValue()); + int tileLatIdx, tileLonIdx; + tileInfo.transformation.computePixel(latDeg, lonDeg, &tileLatIdx, &tileLonIdx); + *pixelIndex = tileInfo.transformation.lonSize * tileLatIdx + tileLonIdx; + return tileInfo.tileVector.get(); +} + +bool CachedGdalBase::getPixelDirect(int band, double latDeg, double lonDeg, void *pixelBuf) +{ + checkBandIndex(band); + // First need to find pixel whereabouts in file + const GdalInfo *gdalInfo; + int fileLatIdx, fileLonIdx; + if (!getGdalPixel(latDeg, lonDeg, &gdalInfo, &fileLatIdx, &fileLonIdx)) { + return false; // GDAL file not found + } + // Bringing GDAL data set and reading from it + const GdalDatasetHolder *datasetHolder = getGdalDatasetHolder(gdalInfo->baseName); + CPLErr readError = datasetHolder->gdalDataset->GetRasterBand(band)->RasterIO(GF_Read, + fileLonIdx, + fileLatIdx, + 1, + 1, + pixelBuf, + 1, + 1, + _pixelType, + 0, + 0); + if (readError != CPLErr::CE_None) { + std::ostringstream errStr; + errStr << "ERROR: CachedGdalBase::getPixelDirect(): Reading GDAL pixel from '" + << gdalInfo->baseName << "' (band: " << band << ", xOffset: " << fileLonIdx + << ", yOffset: " << fileLatIdx << ") failed: " << CPLGetLastErrorMsg(); + throw std::runtime_error(errStr.str()); + } + return true; +} + +bool CachedGdalBase::findTile(int band, double latDeg, double lonDeg) +{ + // Maybe recent tile would suffice? + // Double boundary check is necessary to cover the case of noninteger margin + // (not reflected in tile boundary, but reflected in GDAL boundary) + if (_tileCache.recentValue() && + (_tileCache.recentValue()->boundRect.contains(latDeg, lonDeg)) && + (_tileCache.recentValue()->gdalInfo->boundRect.contains(latDeg, lonDeg)) && + (_tileCache.recentKey()->band == band)) { + return true; + } + // Will look up in cache. First need to find pixel whereabouts in file + const GdalInfo *gdalInfo; + int fileLatIdx, fileLonIdx; + if (!getGdalPixel(latDeg, lonDeg, &gdalInfo, &fileLatIdx, &fileLonIdx)) { + return false; + } + // Key for tile cache + int intMargin = int(std::floor(gdalInfo->transformation.margin)); + TileKey tileKey(band, + std::max(fileLatIdx - (fileLatIdx % _maxTileSize), intMargin), + std::max(fileLonIdx - (fileLonIdx % _maxTileSize), intMargin), + gdalInfo->baseName); + // Trying to bring tile from cache + if (_tileCache.get(tileKey)) { + return true; + } + // Tile not in cache - will add it. First building TileInfo object + int latTileSize = std::min(_maxTileSize, + gdalInfo->transformation.latSize - tileKey.latOffset - + intMargin); + int lonTileSize = std::min(_maxTileSize, + gdalInfo->transformation.lonSize - tileKey.lonOffset - + intMargin); + TileInfo tileInfo(this, + GdalTransform(gdalInfo->transformation, + tileKey.latOffset, + tileKey.lonOffset, + latTileSize, + lonTileSize), + gdalInfo); + + // Now reading pixel data into buffer of tile object + const GdalDatasetHolder *datasetHolder = getGdalDatasetHolder(gdalInfo->baseName); + CPLErr readError = datasetHolder->gdalDataset->GetRasterBand(tileKey.band) + ->RasterIO(GF_Read, + tileKey.lonOffset, + tileKey.latOffset, + lonTileSize, + latTileSize, + getTileBuffer(tileInfo.tileVector.get()), + lonTileSize, + latTileSize, + _pixelType, + 0, + 0); + if (readError != CPLErr::CE_None) { + std::ostringstream errStr; + errStr << "ERROR: CachedGdalBase::findTile(): Reading GDAL data from '" + << tileKey.baseName << "' (band: " << tileKey.band + << ", xOffset: " << tileKey.lonOffset << ", yOffset: " << tileKey.latOffset + << ", xSize: " << lonTileSize << ", ySize: " << latTileSize + << ") failed: " << CPLGetLastErrorMsg(); + throw std::runtime_error(errStr.str()); + } + LOGGER_DEBUG(logger) << "[" << latTileSize << " X " << lonTileSize + << "] tile retrieved from (" << tileKey.latOffset << ", " + << tileKey.lonOffset << ") of band " << tileKey.band << " of '" + << gdalInfo->baseName << "'"; + // Finally adding tile to cache + _tileCache.add(tileKey, tileInfo); + return true; +} + +bool CachedGdalBase::getGdalPixel(double latDeg, + double lonDeg, + const GdalInfo **gdalInfo, + int *fileLatIdx, + int *fileLonIdx) +{ + // First look for GdalInfo containing given point. + // For monolithic data - it is recent (and the only) GdalInfo object + *gdalInfo = _recentGdalInfo; + if (!isMonolithic()) { + // If not recent - will look further + if (!_recentGdalInfo->boundRect.contains(latDeg, lonDeg)) { + // Name of file + std::string baseName = _nameMapper->nameFor(latDeg, lonDeg); + // No such file? + if (baseName.empty()) { + return false; + } + bool known = getGdalInfo(baseName, gdalInfo); + // Is file known to not exist? + if (known && (!*gdalInfo)) { + return false; + } + // If file is unknown - let's try to bring it + if (!known) { + boost::filesystem::path filePath(_fileOrDir); + filePath /= baseName; + boost::system::error_code systemErr; + // Does this file exist? + if (!boost::filesystem::is_regular_file(filePath, systemErr)) { + addGdalInfo(baseName, nullptr); + return false; + } + *gdalInfo = addGdalInfo(baseName, getGdalDatasetHolder(baseName)); + } + } + } + // GdalInfo found. Does it contain given point? + if (!(*gdalInfo)->boundRect.contains(latDeg, lonDeg)) { + return false; + } + // GDAL file found. Computing indices in it + (*gdalInfo)->transformation.computePixel(latDeg, lonDeg, fileLatIdx, fileLonIdx); + return true; +} + +const CachedGdalBase::GdalDatasetHolder *CachedGdalBase::getGdalDatasetHolder( + const std::string &baseName) +{ + if ((_gdalDsCache.recentKey() && (baseName == *_gdalDsCache.recentKey())) || + _gdalDsCache.get(baseName)) { + return _gdalDsCache.recentValue()->get(); + } + boost::filesystem::path filePath(_fileOrDir); + if (!isMonolithic()) { + filePath /= baseName; + } + auto ret = _gdalDsCache + .add(baseName, + std::shared_ptr( + new GdalDatasetHolder(filePath.native()))) + ->get(); + return ret; +} + +const CachedGdalBase::GdalInfo *CachedGdalBase::addGdalInfo( + const std::string &baseName, + const GdalDatasetHolder *gdalDatasetHolder) +{ + if (gdalDatasetHolder) { + auto p = _gdalInfos.emplace(baseName, + std::move(std::unique_ptr( + new GdalInfo(gdalDatasetHolder, + _numBands, + _transformationModifier)))); + _recentGdalInfo = p.first->second.get(); + LOGGER_DEBUG(logger) + << "GDAL file '" << gdalDatasetHolder->fullFileName + << "' covers area from [" + << formatPosition(_recentGdalInfo->boundRect.latDegMin, + _recentGdalInfo->boundRect.lonDegMin) + << "] (Lower Left) to [" + << formatPosition(_recentGdalInfo->boundRect.latDegMax, + _recentGdalInfo->boundRect.lonDegMax) + << "] (Upper Right). " + "Image resolution " + << formatDms(_recentGdalInfo->transformation.latPixPerDeg) << " by " + << formatDms(_recentGdalInfo->transformation.lonPixPerDeg) + << " pixels per degree. Image size is " + << _recentGdalInfo->transformation.latSize << " by " + << _recentGdalInfo->transformation.lonSize << " pixels"; + return _recentGdalInfo; + } + _gdalInfos.emplace(baseName, nullptr); + return nullptr; +} + +bool CachedGdalBase::getGdalInfo(const std::string &filename, + const CachedGdalBase::GdalInfo **gdalInfo) +{ + auto iter = _gdalInfos.find(filename); + if (iter == _gdalInfos.end()) { + *gdalInfo = nullptr; + return false; + } + if (!iter->second.get()) { + *gdalInfo = nullptr; + return true; + } + _recentGdalInfo = iter->second.get(); + *gdalInfo = _recentGdalInfo; + return true; +} + +double CachedGdalBase::gdalNoData(int band) const +{ + return _recentGdalInfo->noDataValues[band - 1]; +} + +void CachedGdalBase::checkBandIndex(int band) const +{ + if ((unsigned)(band - 1) < (unsigned)_numBands) { + return; + } + std::ostringstream errStr; + errStr << "ERROR: CachedGdalBase::checkBandIndex(): Invalid band index " << band + << ". Should be in [1.." << _numBands << "] range"; + throw std::runtime_error(errStr.str()); +} + +bool CachedGdalBase::covers(double latDeg, double lonDeg) +{ + return forEachGdalInfo([latDeg, lonDeg](const GdalInfo &gdalInfo) { + return gdalInfo.boundRect.contains(latDeg, lonDeg); + }); +} + +GdalTransform::BoundRect CachedGdalBase::boundRect() +{ + GdalTransform::BoundRect ret(_recentGdalInfo->boundRect); + if (!isMonolithic()) { + forEachGdalInfo([&ret](const GdalInfo &gdalInfo) { + ret.combine(gdalInfo.boundRect); + return false; + }); + } + return ret; +} + +boost::optional CachedGdalBase::getPixelInfo(double latDeg, + double lonDeg) +{ + const GdalInfo *gdalInfo; + int fileLatIdx, fileLonIdx; + if (!getGdalPixel(latDeg, lonDeg, &gdalInfo, &fileLatIdx, &fileLonIdx)) { + return boost::none; + } + return PixelInfo(gdalInfo->baseName, fileLatIdx, fileLonIdx); +} + +std::string CachedGdalBase::formatDms(double deg, bool forceDegrees) +{ + std::string ret; + if (deg < 0) { + ret += "-"; + deg = -deg; + } + bool started = false; + int d = (int)floor(deg); + if (d || forceDegrees) { + ret += std::to_string(d); + ret += "d"; + started = true; + } + char buf[50]; + deg = (deg - d) * 60.; + int m = (int)floor(deg); + if (m || started) { + snprintf(buf, sizeof(buf), started ? "%02d'" : "%d'", m); + ret += buf; + started = true; + } + snprintf(buf, sizeof(buf), started ? "%05.2f\"" : "%0.2f\"", (deg - m) * 60.); + ret += buf; + return ret; +} + +std::string CachedGdalBase::formatPosition(double latDeg, double lonDeg) +{ + std::string ret; + ret += formatDms(fabs(latDeg), true); + ret += (latDeg >= 0) ? "N, " : "S, "; + ret += formatDms(fabs(lonDeg), true); + ret += (lonDeg >= 0) ? "E" : "W"; + return ret; +} + +GDALDataType CachedGdalBase::gdalDataType(uint8_t) +{ + return GDT_Byte; +} +GDALDataType CachedGdalBase::gdalDataType(uint16_t) +{ + return GDT_UInt16; +} +GDALDataType CachedGdalBase::gdalDataType(int16_t) +{ + return GDT_Int16; +} +GDALDataType CachedGdalBase::gdalDataType(uint32_t) +{ + return GDT_UInt32; +} +GDALDataType CachedGdalBase::gdalDataType(int32_t) +{ + return GDT_Int32; +} +GDALDataType CachedGdalBase::gdalDataType(float) +{ + return GDT_Float32; +} +GDALDataType CachedGdalBase::gdalDataType(double) +{ + return GDT_Float64; +} diff --git a/src/afc-engine/CachedGdal.h b/src/afc-engine/CachedGdal.h new file mode 100644 index 0000000..46f575c --- /dev/null +++ b/src/afc-engine/CachedGdal.h @@ -0,0 +1,790 @@ +/* + * Copyright (C) 2022 Broadcom. All rights reserved. + * The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate + * that owns the software below. + * This work is licensed under the OpenAFC Project License, a copy of which is + * included with this software program. + */ + +/** @file + * GDAL-based geospatial data accessor. + * + * Geospatial data (terrain heights, terrain type, etc.) come in image files + * (usually TIFF or TIFF-like) processed through GDAL library - hereinafter + * named GDAL files, containing GDAL data. Data values are pixel 'color' + * components in these files. Pixel data type may be 8/16/32 bit signed/unsigned + * integer or 32/64 bit floating point. Data may be contained in a monolithic + * file or be split to tile files each covering some rectangular piece of terrain + * (usually 1 degree by 1 degree). Tile files are contained in a single directory + * and have some unified name structure. Each file may contain one or more piece + * of data per pixel (e.g. terrain height and building height) - such data layers + * stored as color components, named bands (there may be up to 4 bands in a file). + * + * GDAL data may contain data tied to any coordinate system (grid or + * latitude/longitude), but this module assumes that coordinate system is + * geodetic north-up (longitude east-positive, latitude north-positive), + * coordinates expressed in degrees, datum is WGS84. Heights assumed to be + * ellipsoidal (not orthometric). It is user responsibility to convert data + * according to these assumptions. + * + * Besides pixel data, GDAL files contain transformation information that + * allows to find pixel coordinates for given latitude and longitude. This + * information might be slightly imprecise and API of this module provide a means + * for rectifying it. + * + * This module provides access to monolithic and tiled GDAL data sources. GDAL + * access performance is improved by caching tiles of recently accessed geodetic + * data in LRU cache. + * + * Usage notes: + * + * INITIALIZATION + * + * Creation of GDAL data source object (template parameter is pixel data type, + * second constructor parameter is data set name, not used internally but useful + * for logging, etc.) for various GDAL sources: + * + * - NLCD data (land usage information). May come in several forms: + * - Single file: + * nlcd = CachedGdal nlcd("nlcd/federated_nlcd.tif", "nlcd"); + * - Directory with several files (not many, otherwise startup will be slow): + * nlcd = CachedGdal("nlcd/nlcd_production", "nlcd", + * GdalNameMapperDirect::make_unique("*.tif", "nlcd/nlcd_production")); + * - Direcory with tiled NLCD files (pixels numbered from top left corner): + * nlcd = CachedGdal("tiled_nlcd/nlcd_production", "nlcd", + * GdalNameMapperPattern::make_unique( + * "nlcd_production_{latHem:ns}{latDegCeil:02}{lonHem:ew}{lonDegFloor:03}.tif", + * "tiled_nlcd/nlcd_production")); + * + * - Single LiDAR file data (monolithic 2-band file with 32-bit float data): + * lidar = CachedGdal("San_Francisco_20080708-11/san_francisco_ca_0_01.tif, + * "LiDAR", nullptr, 2); + * + * - 3DEP tiled data (tile files with 32-bit float data, pixels numbered from + * top left corner): + * dep = CachedGdal("3dep\1_arcsec", "3dep", + * GdalNameMapperPattern::make_unique( + * "USGS_1_{latHem:ns}{latDegCeil:}{lonHem:ew}{lonDegFloor:}*.tif"))); + * dep.setTransformationModifier( + * [](CachedGdalBase::Transformation *t) + * {t->roundPpdToMultipleOf(1.); t->setMarginsOutsideDeg(1.);}) + * + * - SRTM data (tile files with 16-bit integer data, margins are half-pixel + * wide, pixels numbered from bottom left corner). In previous implementation + * coordinate system was shifted by half a pixes down and right. This + * initialization doesn't do this shift: + * srtm = CachedGdal("srtm3arcsecondv003", "srtm", + * GdalNameMapperPattern::make_unique( + * "{latHem:NS}{latDegFloor:02}{lonHem:EW}{lonDegFloor:03}.hgt")); + * srtm.setTransformationModifier( + * [](CachedGdalBase::Transformation *t) + * {t->roundPpdToMultipleOf(0.5); t->setMarginsOutsideDeg(1.);}); + * + * - Globe data (tile files with 16-bit integer data): + * globe = CachedGdal("globe", "globe", + * GdalNameMapperDirect::make_unique("globe", "*.bil")); + * + * DATA RETRIEVAL + * + * - Indirect retrieval of data for 38N, 122E from 2-nd band of LiDAR data: + * float h; + * if (lidar.getValue(38, -122, &h, 2)) { + * // Found, value in 'h' + * } else { + * // Not found + * } + * + * - Direct retrieval of SRTM value: + * constexpr int16_t SRTM_NO_DATA = -1000; + * srtm.setNoData(SRTM_NO_DATA); // Once, somewhere after initialization + * .... + * int16_t h = srtm.valueAt(38, -122); + * if (h == SRTM_NO_DATA) { + * // Value not found + * } else { + * // Value found + * } + * + * General implementation notes: + * Core logic is implemented in abstract base class CachedGdalBase. + * Instantiable are derived template classes CachedGdal, + * parameterized by C type of pixel data stored in GDAL files. + * + * CachedGdalBase contains LRU cache (limited capacity map) of tiles (square/ + * rectangular extracts of pixel data from single band). Cache capacity and + * maximum tile size are optional parameters of CachedGdal + * constructor. + * + * Note here the ambiguity of the word 'tile' in GDAL context. Tile in the + * context of this module is a unit of caching (in-memory rectangular piece of + * geospatial data), yet in the context of GDAL tile might be a rectangular piece + * of geospatial data, stored in one file of multifile (tiled) data source (such + * as 3DEP). + * + * The essential part (looking for tile, containing data for given + * latitude/longitude) is in CachedGdalBase::findTile(). + * + * Brief overview of classes: + * - CachedGdal. GDAL data manager (derived from CachedGdalBase). + * Objects of this class are used by the rest of application to access GDAL + * data. This class is parameterized by pixel data type (char, short, int, + * float, double). + * - CachedGdalBase. Abstract base class for GDAL data manager. Implements the + * core logic. + * - CachedGdalBase::GdalDatasetHolder. RAII wrapper around GDALDataset objects. + * - CachedGdalBase::PixelInfo. Information about pixel whereabouts in GDAL file + * (file name, row, column). + * - CachedGdalBase::TileKey. Key in the cache of retrieved tiles. Describes + * tile whereabouts + * - CachedGdalBase::TileInfo. Information about tile in the tile cache, also + * holds pixel data of this tile + * - CachedGdalBase::GdalInfo. Information about single GDAL file + * - GdalTransform. Data pertinent to latitude/longitude to pixel row/column + * conversion + * - GdalTransform::BoundRect. Rectangle around data stored in tile or file. + * For checking if latitude/longitude is covered + * - GdalNameMapperBase. Abstract base class for name for name mappers that + * provides information about file names in tiled GDAL data + * - GdalNameMapperPattern. Concrete name mapper class built around file name + * pattern + * - GdalNameMapperDirect. Concrete name mapper that probes all tile files and + * retrieves mapping information from them. For use in those unfortunate + * cases, when name-based mapping is not obvious and number of tile files + * is relatively small (e.g. Globe data) + * - LruValueCache. LRU cache - copyless improvement of boost::lru_cache + */ + +#ifndef CACHED_GDAL_H +#define CACHED_GDAL_H + +#include +#include "GdalNameMapper.h" +#include "GdalTransform.h" +#include +#include "LruValueCache.h" +#include +#include +#include +#include +#include +#include + +/** @file + * Unified GDAL geospatial data access module */ + +/** Abstract base class that handles everything but pixel data */ +class CachedGdalBase : private boost::noncopyable +{ + public: + ////////////////////////////////////////////////// + // CachedGdalBase. Public class constants + ////////////////////////////////////////////////// + + /** Default maximum tile side (number of pixels in one dimension) size */ + static const int DEFAULT_MAX_TILE_SIZE = 1000; + + /** Default maximum size of LRU cache of tiles */ + static const int DEFAULT_CACHE_SIZE = 50; + + /** Maximum number of simultaneously opened GDAL files */ + static const int GDAL_CACHE_SIZE = 9; + + ////////////////////////////////////////////////// + // CachedGdalBase. Public class types + ////////////////////////////////////////////////// + + /** Holds GDALDataset pointer and full filename. + * RAII wrapper around GDALDataset* + */ + struct GdalDatasetHolder { + /** Constructor - opens dataset. + * @param fullFileName Full file name of GDAL file + */ + GdalDatasetHolder(const std::string &fullFileName); + + /** Destructor - closes dataset */ + ~GdalDatasetHolder(); + + /** Dataset pointer */ + GDALDataset *gdalDataset; + + /** Full file name of GDAL file */ + std::string fullFileName; + }; + + /** Information of whereabouts of data for certain pixel */ + struct PixelInfo { + /** Constructor + * @param baseName Base name of GDAL file containing pixel + * @param row 0-based row number in GDAL file + * @param column 0-based column number in GDAL file + */ + PixelInfo(const std::string &baseName, int row, int column); + + /** Default constructor */ + PixelInfo(); + + /** Base name of GDAL file containing pixel */ + std::string baseName; + /** 0-based row number in GDAL file */ + int row; + /** 0-based column number in GDAL file */ + int column; + }; + + ////////////////////////////////////////////////// + // CachedGdalBase. Public member functions + ////////////////////////////////////////////////// + + /** Virtual destructor */ + virtual ~CachedGdalBase() = default; + + /** Data set name */ + const std::string &dsName() const; + + /** Sets callback that modifies (rectifies) transformation data retrieved + * from GDAL file. */ + void setTransformationModifier(std::function modifier); + + /** True for monolithic data source, false for tiled directory */ + bool isMonolithic() const; + + /** Check if given point is covered by GDAL data. + * Current version only works for monolithic data + */ + bool covers(double latDeg, double lonDeg); + + /** Retrieves geospatial data boundaries. + * Current version only works for monolithic data + * @param[out] lonDegMax Optional maximum longitude in east-positive degrees + */ + GdalTransform::BoundRect boundRect(); + + /** Returns whereabouts of data for given latitude/longitude in GDAL file. + * This function is for comparison with other implementations + * @param latDeg Latitude in north-positive degrees + * @param lonDeg Longitude in east-positive degrees + * @return Optional information about pixel whereabouts + */ + boost::optional getPixelInfo(double latDeg, double lonDeg); + + ////////////////////////////////////////////////// + // CachedGdalBase. Public static methods + ////////////////////////////////////////////////// + + /** Format degree value into degree/minute/second. + * @param deg Degree value + * @param forceDegrees True to present leading degrees/minutes even if they + * are zero + * @return String representation + */ + static std::string formatDms(double deg, bool forceDegrees = false); + + /** String representation of given position. + * @param latDeg North-positive latitude in degrees + * @param lonDeg East-positive longitude in degrees + * @return String representation of given position + */ + static std::string formatPosition(double latDeg, double lonDeg); + + protected: + /** Constructor. + * @param fileOrDir Name of file (for monolithic file data GDAL source) or + * name of directory (for multifile directory data source) + * @param dsName Data set name (not used internally - for logging purposes) + * @param nameMapper Null for monolithic (single-file) data, address of + * GdalNameMapper object for multifile (tiled) data + * @param numBands Number of bands that will be used (i.e. maximum 1-based + * band index) + * @param maxTileSize Maximum size for tile in one dimension + * @param cacheSize Maximum number of tiles in tile cache + * @param pixelType Value describing pixel data type in RasterIO operation + */ + CachedGdalBase(std::string fileOrDir, + const std::string &dsName, + std::unique_ptr nameMapper, + int numBands, + int maxTileSize, + int cacheSize, + GDALDataType pixelType); + + /** Value for unavailable data for given band, as obtained from GDAL */ + double gdalNoData(int band) const; + + /** Post-construction initialization. + * Initialization functionality that requires virtual functions of derived + * classes, unavailable at this class' construction time + */ + void initialize(); + + /** Pre-destruction cleanup (stuff that requires virtual functions, + * unavailable in destructor) + */ + void cleanup(); + + /** Looks up tile, containing pixel for given coordinates. + * @param[in] band 1-based index of band in GDAL file + * @param[in] latDeg North-positive latitude in degrees + * @param[in] lonDeg East-positive longitude in degrees + * @param[out] pixelIndex Index of pixel data inside tile vector + * @return std::vector, containing tile pixel data, nullptr if lookup failed + */ + const void *getTileVector(int band, double latDeg, double lonDeg, int *pixelIndex); + + /** Read pixel data directly, bypassing caching mechanism + * @param[in] band 1-based index of band in GDAL file + * @param[in] latDeg North-positive latitude in degrees + * @param[in] lonDeg East-positive longitude in degrees + * @param[out] pixelBuf Buffer to read pixel into + * @return True on success, false on fail + */ + bool getPixelDirect(int band, double latDeg, double lonDeg, void *pixelBuf); + + /** Throws if given band index is invalid */ + void checkBandIndex(int band) const; + + ////////////////////////////////////////////////// + // CachedGdalBase. Pixel-type specific tile manipulation pure virtual functions + ////////////////////////////////////////////////// + + /** Creates on heap a std::vector buffer for tile pixel data. + * @param latSize Pixel count in latitude direction + * @param lonSize Pixel count in longitude direction + * @return Address of created std::vector + */ + virtual void *createTileVector(int latSize, int lonSize) const = 0; + + /** Deletes tile vector. + * @param tileVector std::vector to delete + */ + virtual void deleteTileVector(void *tileVector) const = 0; + + /** Returns address of tile's data buffer + * @param tileVector std::vector, containing tile pixel data + * @return Vector's buffer, containing pixel data + */ + virtual void *getTileBuffer(void *tileVector) const = 0; + + ////////////////////////////////////////////////// + // CachedGdalBase. Protected static methods + ////////////////////////////////////////////////// + + // Functions that map pixel types to respective GDAL data type codes + static GDALDataType gdalDataType(uint8_t); + static GDALDataType gdalDataType(uint16_t); + static GDALDataType gdalDataType(int16_t); + static GDALDataType gdalDataType(uint32_t); + static GDALDataType gdalDataType(int32_t); + static GDALDataType gdalDataType(float); + static GDALDataType gdalDataType(double); + + private: + ////////////////////////////////////////////////// + // CachedGdalBase. Private class types + ////////////////////////////////////////////////// + + ////////////////////////////////////////////////// + // CachedGdalBase::GdalInfo + ////////////////////////////////////////////////// + + /** Information pertinent to a single GDAL file */ + struct GdalInfo { + ////////////////////////////////////////////////// + // CachedGdalBase::GdalInfo. Public instance methods + ////////////////////////////////////////////////// + + /** Constructor. + * @param gdalDataset GdalDatasetHolder for file being added + * @param minBands Minimum required number of bands + * @param transformationModifier Optional transformation modifier + */ + GdalInfo(const GdalDatasetHolder *gdalDataset, + int minBands, + const boost::optional> + &transformationModifier); + + ////////////////////////////////////////////////// + // CachedGdalBase::GdalInfo. Public instance data + ////////////////////////////////////////////////// + + /** File name without directory */ + std::string baseName; + + /** Transformation of coordinates to pixel indices */ + GdalTransform transformation; + + /** Boundary rectangle with margins (if any) applied */ + GdalTransform::BoundRect boundRect; + + /** Number of bands */ + int numBands; + + /** Per-band no-data values [0] contains value for band 1, etc. */ + std::vector noDataValues; + }; + + ////////////////////////////////////////////////// + // CachedGdalBase::TileKey + ////////////////////////////////////////////////// + + /** Tile identifier in cache */ + struct TileKey { + ////////////////////////////////////////////////// + // CachedGdalBase::TileKey. Public instance methods + ////////////////////////////////////////////////// + + /** Constructor. + * @param band 1-based band index + * @param latOffset Tile offset in latitude direction + * @param lonOffset Tile offset in longitude direction + * @param baseName Tile file base name + */ + TileKey(int band, + int latOffset, + int lonOffset, + const std::string &baseName); + + /** Default constructor */ + TileKey(); + + /** Ordering comparison */ + bool operator<(const TileKey &other) const; + + ////////////////////////////////////////////////// + // CachedGdalBase::TileKey. Public instance data + ////////////////////////////////////////////////// + + /** 1-based band index */ + int band; + + /** Tile offset in latitude direction */ + int latOffset; + + /** Tile offset in longitude direction */ + int lonOffset; + + /** Tile file base name */ + std::string baseName; + }; + + ////////////////////////////////////////////////// + // CachedGdalBase::TileInfo + ////////////////////////////////////////////////// + + /** Tile data in cache */ + struct TileInfo { + ////////////////////////////////////////////////// + // CachedGdalBase::TileInfo. Public instance methods + ////////////////////////////////////////////////// + + /** Constructor. + * @param cachedGdal Parent container + * @param transformation Pixel indices computation transformation + * @param gdalInfo GdalInfo containing this tile + */ + TileInfo(CachedGdalBase *cachedGdal, + const GdalTransform &transformation, + const GdalInfo *gdalInfo); + + /** Default constructor to appease boost::lru_cache */ + TileInfo(); + + ////////////////////////////////////////////////// + // CachedGdalBase::TileInfo. Public instance data + ////////////////////////////////////////////////// + + /** Parent container */ + const CachedGdalBase *cachedGdal; + + /** Transformation of coordinates to pixel indices */ + GdalTransform transformation; + + /** Tile boundary rectangle. + * Always contain whole number of pixels, noninteger boundaries + * checked through gdalInfo->boundRect + */ + GdalTransform::BoundRect boundRect; + + /* GdalInfo containing this tile */ + const GdalInfo *gdalInfo; + + /** std::vector that contains tile pixel data */ + std::shared_ptr tileVector; + }; + + ////////////////////////////////////////////////// + // CachedGdalBase. Private instance methods + ////////////////////////////////////////////////// + + /** Tries to find tile for given coordinates and bands + * @param[in] band 1-based band index + * @param[in] latDeg Latitude of point to look tile of in north-positive + * degrees + * @param[in] lonDeg Longitude of point to look tile of in east-positive + * degrees + * @return On success makes desired tile the recent in tile cache and returns + * true, otherwise returns false + */ + bool findTile(int band, double latDeg, double lonDeg); + + /** Provides GDAL whereabouts of data for point with given coordinates. + * @param latDeg[in] North-positive latitude in degrees + * @param lonDeg[in] East-positive longitude in degrees + * @param gdalInfo[out] GdalInfo of file, containing point (if found) + * @param fileLatIdx[out] Latitude index (row) in GDAL file (if found) + * @param fileLonIdx[out] Longitude index (column) in file (if found) + * @return True if pixel for given point found, false otherwise + */ + bool getGdalPixel(double latDeg, + double lonDeg, + const GdalInfo **gdalInfo, + int *fileLatIdx, + int *fileLonIdx); + + /** Brings in GDAL dataset holder that corresponds to given file name (file + * must exist). + * @param baseName Base name of file to bring in + * @return Pointer to holder of GDALDataset of given file + */ + const GdalDatasetHolder *getGdalDatasetHolder(const std::string &filename); + + /** Adds GdalInfo information for given file to collection of known GDAL files + * @param baseName GDAL file base name + * @param gdalDataset Holder of GDALDataset for existing file, nullptr for + * nonexistent file + * @return Address of created GdalInfo object + */ + const GdalInfo *addGdalInfo(const std::string &baseName, + const GdalDatasetHolder *gdalDataset); + + /* Lookup of GdalInfo for given file name. + * @param[in] baseName File base name + * @param[out] gdalInfo Address of found (or not found) GdalInfo object + * @return True on lookup success (in case of lookup for nonexistent files, + * true is still returned, but *gdalInfo filled with nullptr) + */ + bool getGdalInfo(const std::string &baseName, const GdalInfo **gdalInfo); + + /** Calls given function for GdalInfo objects, corresponding to some or all + * GDAL files + * @param op Function to call for each GdalInfo object. Function return true + * to stop iteration (e.g. if something desirable was found), false to + * continue + * @return True if last call of op() returned true + */ + bool forEachGdalInfo(const std::function &op); + + /** Does proper cleanup and reinitialization after GDAL parameters modification + * For use after mapping parameter change + */ + void rereadGdal(); + + ////////////////////////////////////////////////// + // CachedGdalBase. Private instance data + ////////////////////////////////////////////////// + + /** Name of file or directory of tiled files */ + const std::string _fileOrDir; + + /** Data set name */ + std::string _dsName; + + /** For tiled (multifile) data source - provides base name of tile file + * for given latitude/longitude. Null for monolithic data source + */ + std::unique_ptr _nameMapper; + + /** Optional transformation modifier (rectifier) callback */ + boost::optional> _transformationModifier; + + /** Number of bands to be used (maximum 1-based band index) */ + const int _numBands; + + /** Pixel data type for RasterIO() call */ + const GDALDataType _pixelType; + + /** Maximum size for tile in one dimension */ + const int _maxTileSize; + + /** LRU tile cache */ + LruValueCache _tileCache; + + /** GDAL dataset holders indexed by base file names */ + LruValueCache> _gdalDsCache; + + /** Maps base filenames to GdalInfo objects (null pointers for nonexistent + * files) + */ + std::map> _gdalInfos; + + /** Recently used GdalInfo object. + * After initial initialization is always nonnull. May only be changed by + * addGdalInfo() and getGdalInfo() + */ + const GdalInfo *_recentGdalInfo; + + /** True if information about all GDAL files retrieved to _gdalInfos */ + bool _allSeen; +}; + +/** Concrete GDAL cache class, parameterized by pixel data type */ +template +class CachedGdal : public CachedGdalBase +{ + public: + /** Constructor. + * @param fileOrDir Name of file (for monolithic file data GDAL source) or + * of directory (for multifile data source) + * @param dsName Data set name (not used internally - for logging purposes) + * @param nameMapper Null for monolithic (single-file) data, address of + * GdalNameMapperBase-derived object for multifile (tiled) data + * @param numBands Number of bands that will be used (maximum value for + * 1-based band index) + * @param maxTileSize Maximum size for tile in one dimension + * @param cacheSize Maximum number of tiles in LRU cache + */ + CachedGdal(const std::string &file_or_dir, + const std::string &dsName, + std::unique_ptr nameMapper = nullptr, + int numBands = 1, + int maxTileSize = CachedGdalBase::DEFAULT_MAX_TILE_SIZE, + int cacheSize = CachedGdalBase::DEFAULT_CACHE_SIZE) : + CachedGdalBase(file_or_dir, + dsName, + std::move(nameMapper), + numBands, + maxTileSize, + cacheSize, + CachedGdalBase::gdalDataType((PixelData)0)) + { + initialize(); + } + + /** Virtual destructor */ + virtual ~CachedGdal() + { + cleanup(); + } + + /** Retrieves geospatial data value by output parameter + * @param[in] latDeg North-positive latitude in degrees + * @param[in] lonDeg East-positive longitude in degrees + * @param[out] value Geospatial value + * @param[in] band 1-based band index + * @param[in] direct True to read pixel directly, bypassing caching + * mechanism (may speed up accessing scattered data) + * @return True on success, false if coordinates are outside of file(s) + */ + bool getValueAt(double latDeg, + double lonDeg, + PixelData *value, + int band = 1, + bool direct = false) + { + PixelData v; + bool ret; // True if retrieval successful + if (direct) { + // Directly reading pixel + ret = getPixelDirect(band, latDeg, lonDeg, &v); + } else { + // First - finding tile + int pixelIndex; + auto tileVector = reinterpret_cast *>( + getTileVector(band, latDeg, lonDeg, &pixelIndex)); + ret = tileVector != nullptr; + if (ret) { + // if tile found - retrieving pixel from it + v = tileVector->at(pixelIndex); + } + } + if (ret && (v == static_cast(gdalNoData(band)))) { + // If 'no-data' pixel was retrieved - count as faiilure + ret = false; + } + if (value) { + // Caller needs pixel value + if (!ret) { + // Value for 'no-data' pixel - overridden or from GHDAL file + auto ndi = _noData.find(band); + v = (ndi != _noData.end()) ? + ndi->second : + static_cast(gdalNoData(band)); + } + *value = v; + } + return ret; + } + + /** Retrieves geospatial data value by return result + * @param[in] latDeg North-positive latitude in degrees + * @param[in] lonDeg East-positive longitude in degrees + * @param[in] band 1-based band index + * @param[in] direct True to read pixel directly, bypassing caching + * mechanism (may speed up accessing scattered data) + * @return Resulted geospatial value + */ + PixelData valueAt(double latDeg, double lonDeg, int band = 1, bool direct = false) + { + PixelData ret; + getValueAt(latDeg, lonDeg, &ret, band, direct); + return ret; + } + + /** Sets value used when no data is available for given band */ + void setNoData(PixelData value, int band = 1) + { + checkBandIndex(band); + _noData[band] = value; + } + + /** Returns value used when no data is available for given band */ + PixelData noData(int band = 1) const + { + checkBandIndex(band); + auto ndi = _noData.find(band); + return (ndi == _noData.end()) ? static_cast(gdalNoData(band)) : + ndi->second; + } + + protected: + ////////////////////////////////////////////////// + // CachedGdal. Protected instance methods + ////////////////////////////////////////////////// + + /** Creates on heap a std::vector buffer for tile pixel data. + * @param latSize Pixel count in latitude direction + * @param lonSize Pixel count in longitude direction + * @return Address of created std::vector + */ + virtual void *createTileVector(int latSize, int lonSize) const + { + return new std::vector(latSize * lonSize); + } + + /** Deletes tile vector. + * @param tileVector std::vector to delete + */ + virtual void deleteTileVector(void *tile) const + { + delete reinterpret_cast *>(tile); + } + + /** Returns address of tile's data buffer + * @param tileVector std::vector, containing tile pixel data + * @return Vector's buffer, containing pixel data + */ + virtual void *getTileBuffer(void *tile) const + { + return reinterpret_cast *>(tile)->data(); + } + + private: + ////////////////////////////////////////////////// + // CachedGdal. Private instance data + ////////////////////////////////////////////////// + + /** Overridden no-data values */ + std::map _noData; +}; + +#endif /* CACHED_GDAL_H */ diff --git a/src/afc-engine/EcefModel.cpp b/src/afc-engine/EcefModel.cpp new file mode 100644 index 0000000..a367d96 --- /dev/null +++ b/src/afc-engine/EcefModel.cpp @@ -0,0 +1,88 @@ +#include +#include +#include "MathConstants.h" +#include "EcefModel.h" + +// Note: Altitude here is a true altitude, i.e. a height. Given an altitude (in km), this returns a +// value in an ECEF coordinate +// frame in km. +Vector3 EcefModel::geodeticToEcef(double lat, double lon, double alt) +{ + const double a = + MathConstants::WGS84EarthSemiMajorAxis; // 6378.137; // Radius of the earth in km. + const double esq = + MathConstants::WGS84EarthFirstEccentricitySquared; // 6.694379901e-3; // First + // eccentricity squared. + + // Convert lat/lon to radians. + const double latr = lat * M_PI / 180.0; + const double lonr = lon * M_PI / 180.0; + + double cosLon, sinLon; + ::sincos(lonr, &sinLon, &cosLon); + double cosLat, sinLat; + ::sincos(latr, &sinLat, &cosLat); + + // Compute 'chi', which adjusts for vertical eccentricity. + const double chi = sqrt(1.0 - esq * sinLat * sinLat); + + return Vector3((a / chi + alt) * cosLat * cosLon, + (a / chi + alt) * cosLat * sinLon, + (a * (1 - esq) / chi + alt) * sinLat); +} + +// Converts from ecef to geodetic coordinates. This algorithm is from Wikipedia, and +// all constants are from WGS '84. +GeodeticCoord EcefModel::ecefToGeodetic(const Vector3 &ecef) +{ + const double a = MathConstants::WGS84EarthSemiMajorAxis; // 6378.137; + const double b = MathConstants::WGS84EarthSemiMinorAxis; // 6356.7523142; + // double e = sqrt(MathConstants::WGS84EarthFirstEccentricitySquared); + const double eprime = sqrt(MathConstants::WGS84EarthSecondEccentricitySquared); + const double esq = MathConstants::WGS84EarthFirstEccentricitySquared; + // double eprimesq = MathConstants::WGS84EarthSecondEccentricitySquared; + + const double X = ecef.x(); + const double Y = ecef.y(); + const double Z = ecef.z(); + + double r = sqrt(X * X + Y * Y); + double Esq = a * a - b * b; + double F = 54 * b * b * Z * Z; + double G = r * r + (1 - esq) * Z * Z - esq * Esq; + double C = esq * esq * F * r * r / (G * G * G); + double S = pow(1 + C + sqrt(C * C + 2 * C), 1.0 / 3.0); + double P = F / (3 * (S + 1 / S + 1) * (S + 1 / S + 1) * G * G); + double Q = sqrt(1 + 2 * esq * esq * P); + double r0 = -(P * esq * r) / (1 + Q) + + sqrt(a * a / 2 * (1 + 1 / Q) - (P * (1 - esq) * Z * Z) / (Q * (1 + Q)) - + P * r * r / 2.0); + double U = sqrt((r - esq * r0) * (r - esq * r0) + Z * Z); + double V = sqrt((r - esq * r0) * (r - esq * r0) + (1 - esq) * Z * Z); + double Z0 = (b * b * Z) / (a * V); + + double h = U * (1 - (b * b) / (a * V)); + double lat = atan((Z + eprime * eprime * Z0) / r) * 180 / M_PI; + double lon = atan2(Y, X) * 180 / M_PI; + return GeodeticCoord(lon, lat, h); +} + +Vector3 EcefModel::fromGeodetic(const GeodeticCoord &in) +{ + return geodeticToEcef(in.latitudeDeg, in.longitudeDeg, in.heightKm); +} + +GeodeticCoord EcefModel::toGeodetic(const Vector3 &in) +{ + return ecefToGeodetic(in); +} + +Vector3 EcefModel::localVertical(const GeodeticCoord &in) +{ + double cosLon, sinLon; + ::sincos(M_PI / 180.0 * in.longitudeDeg, &sinLon, &cosLon); + double cosLat, sinLat; + ::sincos(M_PI / 180.0 * in.latitudeDeg, &sinLat, &cosLat); + + return Vector3(cosLat * cosLon, cosLat * sinLon, sinLat); +} diff --git a/src/afc-engine/EcefModel.h b/src/afc-engine/EcefModel.h new file mode 100644 index 0000000..556b1ab --- /dev/null +++ b/src/afc-engine/EcefModel.h @@ -0,0 +1,36 @@ +#ifndef ECEF_MODEL_H +#define ECEF_MODEL_H + +#include +#include "GeodeticCoord.h" +#include "Vector3.h" + +/** Convert between geodetic coordinates and WGS84 Earth-centered Earth-fixed + * (ECEF) coordinates. + */ +class EcefModel +{ + public: + static Vector3 geodeticToEcef(double lat, double lon, double alt); + static GeodeticCoord ecefToGeodetic(const Vector3 &ecef); + + /** Convert from geodetic coordinates to ECEF point. + * @param in The geodetic coordinates to convert. + * @return The ECEF coordinates for the same location (in units kilometers). + */ + static Vector3 fromGeodetic(const GeodeticCoord &in); + + /** Convert from ECEF point to geodetic coordinates. + * @param in The ECEF coordiantes to convert (in units kilometers). + * @return The geodetic coordinates for the same location. + */ + static GeodeticCoord toGeodetic(const Vector3 &in); + + /** Determine the local ellipsoid normal "up" direction at a given location. + * @param in The geodetic coordinates of the location. + * @return A unit vector in ECEF coordinates in the direction "up". + */ + static Vector3 localVertical(const GeodeticCoord &in); +}; + +#endif diff --git a/src/afc-engine/ErrorTypes.h b/src/afc-engine/ErrorTypes.h new file mode 100644 index 0000000..564d13c --- /dev/null +++ b/src/afc-engine/ErrorTypes.h @@ -0,0 +1,67 @@ +#ifndef ERROR_TYPES_H +#define ERROR_TYPES_H + +#include +#include + +/** A convenience class to construct std::runtime_error from QStrings. + */ +class RuntimeError : public std::runtime_error +{ + public: + /** Create a new error with title and message. + * @param title Is used as the title of the error dialog. + * @param msg I used as the body of the message dialog. + * @param parent Optionally define the parent for the dialog. + */ + RuntimeError(const QString &msg) : runtime_error(msg.toStdString()) + { + } + + /// Required for std::exception child + virtual ~RuntimeError() throw() + { + } +}; + +/** Represent an error which should be displayed as a dialog. + * This is not derived from std::exception so that it is guaranteed not to + * be trapped in a normal catch block. + */ +class FatalError +{ + public: + /** Create a new error with title and message. + */ + FatalError(const QString &titleVal, const QString &msg) : + _title(titleVal), _msg(msg) + { + } + + /// Same behavior as std::exception + virtual ~FatalError() throw() + { + } + + /** Get the title string for the error. + */ + const QString &title() const throw() + { + return _title; + } + + /** Duck-type replacement for std::exception::what(). + */ + const QString &what() const throw() + { + return _msg; + } + + protected: + /// Intended title of the dialog + QString _title; + /// Intended message in the dialog + QString _msg; +}; + +#endif /* ERROR_TYPES_H */ diff --git a/src/afc-engine/GdalHelpers.cpp b/src/afc-engine/GdalHelpers.cpp new file mode 100644 index 0000000..e233a94 --- /dev/null +++ b/src/afc-engine/GdalHelpers.cpp @@ -0,0 +1,175 @@ +#include "GdalHelpers.h" +#include "MultiGeometryIterable.h" +#include "afclogging/ErrStream.h" +#include +#include +#include + +template<> +OGRPoint *GdalHelpers::createGeometry() +{ + return static_cast(OGRGeometryFactory::createGeometry(wkbPoint)); +} + +template<> +OGRMultiPoint *GdalHelpers::createGeometry() +{ + return static_cast(OGRGeometryFactory::createGeometry(wkbMultiPoint)); +} + +template<> +OGRLineString *GdalHelpers::createGeometry() +{ + return static_cast(OGRGeometryFactory::createGeometry(wkbLineString)); +} + +template<> +OGRMultiLineString *GdalHelpers::createGeometry() +{ + return static_cast( + OGRGeometryFactory::createGeometry(wkbMultiLineString)); +} + +template<> +OGRLinearRing *GdalHelpers::createGeometry() +{ + return static_cast(OGRGeometryFactory::createGeometry(wkbLinearRing)); +} + +template<> +OGRPolygon *GdalHelpers::createGeometry() +{ + return static_cast(OGRGeometryFactory::createGeometry(wkbPolygon)); +} + +template<> +OGRMultiPolygon *GdalHelpers::createGeometry() +{ + return static_cast(OGRGeometryFactory::createGeometry(wkbMultiPolygon)); +} + +template<> +OGRGeometryCollection *GdalHelpers::createGeometry() +{ + return static_cast( + OGRGeometryFactory::createGeometry(wkbGeometryCollection)); +} + +void GdalHelpers::OgrFreer::operator()(void *ptr) const +{ + if (ptr) { + OGRFree(ptr); + } +} + +void GdalHelpers::GeometryDeleter::operator()(OGRGeometry *ptr) const +{ + OGRGeometryFactory::destroyGeometry(ptr); +} + +void GdalHelpers::FeatureDeleter::operator()(OGRFeature *obj) +{ + OGRFeature::DestroyFeature(obj); +} + +void GdalHelpers::SrsDeleter::operator()(OGRSpatialReference *obj) +{ + OGRSpatialReference::DestroySpatialReference(obj); +} + +void GdalHelpers::coalesce(OGRGeometryCollection *target, OGRGeometryCollection *source) +{ + for (OGRGeometry *item : MultiGeometryIterableMutable(*source)) { + target->addGeometryDirectly(item); + } + while (!source->IsEmpty()) { + source->removeGeometry(0, FALSE); + } +} + +std::string GdalHelpers::exportWkb(const OGRGeometry &geom) +{ + const size_t wkbSize = geom.WkbSize(); + std::unique_ptr wkbData(new unsigned char[wkbSize]); + const int status = geom.exportToWkb(wkbXDR, wkbData.get()); + if (status != OGRERR_NONE) { + throw std::runtime_error(ErrStream() << "Failed to export WKB: code " << status); + } + return std::string(reinterpret_cast(wkbData.get()), wkbSize); +} + +std::string GdalHelpers::exportWkt(const OGRGeometry *geom) +{ + if (!geom) { + return std::string(); + } + + char *wktData; + const int status = geom->exportToWkt(&wktData); + if (status != OGRERR_NONE) { + throw std::runtime_error(ErrStream() << "Failed to export WKT: code " << status); + } + std::unique_ptr wrap(wktData); + + return std::string(wktData); +} + +OGRGeometry *GdalHelpers::importWkt(const std::string &data) +{ + if (data.empty()) { + return nullptr; + } + + OGRGeometry *geom = nullptr; + std::string edit(data); + char *front = const_cast(edit.data()); + const int status = OGRGeometryFactory::createFromWkt(&front, nullptr, &geom); + if (status != OGRERR_NONE) { + delete geom; + throw std::runtime_error(ErrStream() << "Failed to import WKT: code " << status); + } + return geom; +} + +std::string GdalHelpers::exportWkt(const OGRSpatialReference *srs) +{ + if (!srs) { + return std::string(); + } + + char *wktData; + const int status = srs->exportToWkt(&wktData); + if (status != OGRERR_NONE) { + throw std::runtime_error(ErrStream() << "Failed to export WKT: code " << status); + } + std::unique_ptr wrap(wktData); + + return std::string(wktData); +} + +std::string GdalHelpers::exportProj4(const OGRSpatialReference *srs) +{ + if (!srs) { + return std::string(); + } + + char *projData; + const int status = srs->exportToProj4(&projData); + std::unique_ptr wrap(projData); + if (status != OGRERR_NONE) { + throw std::runtime_error(ErrStream() << "Failed to export Proj.4: code " << status); + } + + return std::string(projData); +} + +OGRSpatialReference *GdalHelpers::importWellKnownGcs(const std::string &name) +{ + std::unique_ptr srs(new OGRSpatialReference()); + const auto status = srs->SetWellKnownGeogCS(name.data()); + if (status != OGRERR_NONE) { + throw std::runtime_error(ErrStream() + << "Failed to import well-known GCS: code " << status); + } + return srs.release(); +} diff --git a/src/afc-engine/GdalHelpers.h b/src/afc-engine/GdalHelpers.h new file mode 100644 index 0000000..d6c2bf7 --- /dev/null +++ b/src/afc-engine/GdalHelpers.h @@ -0,0 +1,142 @@ +#ifndef SRC_KSCEGEOMETRY_GDALHELPERS_H_ +#define SRC_KSCEGEOMETRY_GDALHELPERS_H_ + +#include +#include + +class OGRGeometry; +class OGRPoint; +class OGRMultiPoint; +class OGRLineString; +class OGRMultiLineString; +class OGRLinearRing; +class OGRPolygon; +class OGRMultiPolygon; +class OGRGeometryCollection; +class OGRFeature; +class OGRSpatialReference; + +namespace GdalHelpers +{ + +/** A helper function to construct new OGRGeometry-derived objects in the + * proper dynamic library memory space (specifically for windows DLLs). + * + * @tparam Typ A class derived from OGRGeometry. + * @return A new default instance of the desired class. + */ +template +Typ *createGeometry() = delete; + +template<> +OGRPoint *createGeometry(); + +template<> +OGRMultiPoint *createGeometry(); + +template<> +OGRLineString *createGeometry(); + +template<> +OGRMultiLineString *createGeometry(); + +template<> +OGRLinearRing *createGeometry(); + +template<> +OGRPolygon *createGeometry(); + +template<> +OGRMultiPolygon *createGeometry(); + +template<> +OGRGeometryCollection *createGeometry(); + +/** Delete OGR data with OGRFree() in a DLL-safe way. + */ +class OgrFreer +{ + public: + /// Interface for std::unique_ptr deleter + void operator()(void *ptr) const; +}; + +/** Delete OGRGeometry objects in a DLL-safe way. + */ +class GeometryDeleter +{ + public: + /// Interface for std::unique_ptr deleter + void operator()(OGRGeometry *ptr) const; +}; + +/// Convenience name for unique-pointer class +template +using GeomUniquePtr = std::unique_ptr; + +/** Delete OGRFeature objects in a DLL-safe way. + */ +class FeatureDeleter +{ + public: + /// Interface for std::unique_ptr deleter + void operator()(OGRFeature *obj); +}; + +/** Delete OGRSpatialReference objects in a DLL-safe way. + */ +class SrsDeleter +{ + public: + /// Interface for std::unique_ptr deleter + void operator()(OGRSpatialReference *obj); +}; + +/** Move contained geometries from one collection to another. + * @param target The target into which all geometries are moved. + * @param source The source from which geometries are taken. + * @post The @c source is empty. + */ +void coalesce(OGRGeometryCollection *target, OGRGeometryCollection *source); + +/** Export geometry to a Well Known Binary representation. + * + * @param geom The geometry object to export. + * @return The WKB byte string for the geometry. + * The data is in network byte order (big-endian). + */ +std::string exportWkb(const OGRGeometry &geom); + +/** Export geometry to Well Known Text representation. + * A null geometry has an empty array. + * @param geom The object to export, which may be null. + * Ownership is kept by the caller. + * @return The WKT string for the geometry. + */ +std::string exportWkt(const OGRGeometry *geom); + +/** Import geometry from Well Known Text representation. + * An empty array will result in a null geometry. + * @param data The WKT string for the geometry. + * @return The new created geometry. + * Ownership is taken by the caller. + */ +OGRGeometry *importWkt(const std::string &data); + +/** Export SRS to Well Known Text representation. + * @param srs Pointer to the object to export, which may be null. + * @return The WKT string for the SRS. + */ +std::string exportWkt(const OGRSpatialReference *srs); + +/** Export SRS to Proj.4 representation. + * @param srs Pointer to the object to export, which may be null. + * @return The proj.4 string for the SRS. + */ +std::string exportProj4(const OGRSpatialReference *srs); + +OGRSpatialReference *importWellKnownGcs(const std::string &name); + +} + +#endif /* SRC_KSCEGEOMETRY_GDALHELPERS_H_ */ diff --git a/src/afc-engine/GdalNameMapper.cpp b/src/afc-engine/GdalNameMapper.cpp new file mode 100644 index 0000000..d87c94e --- /dev/null +++ b/src/afc-engine/GdalNameMapper.cpp @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2022 Broadcom. All rights reserved. + * The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate + * that owns the software below. + * This work is licensed under the OpenAFC Project License, a copy of which is + * included with this software program. + */ + +#include "GdalNameMapper.h" +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +// GdalNameMapperPattern::NamePart +/////////////////////////////////////////////////////////////////////////////// + +GdalNameMapperPattern::NamePart::NamePart(GdalNameMapperPattern::Src src_, + GdalNameMapperPattern::Op op_, + std::string str_) : + src(src_), op(op_), str(str_) +{ +} + +/////////////////////////////////////////////////////////////////////////////// +// GdalNameMapperPattern +/////////////////////////////////////////////////////////////////////////////// + +GdalNameMapperPattern::GdalNameMapperPattern(const std::string &pattern, + const std::string &directory) +{ + if (pattern.find_first_of("*?[]") != std::string::npos) { + std::ostringstream errStr; + if (directory.empty()) { + errStr << "ERROR: GdalNameMapperPattern::GdalNameMapperPattern(): " + "GDAL filename pattern contains wildcard, but directory " + "is not specified"; + throw std::runtime_error(errStr.str()); + } + if (!boost::filesystem::is_directory(directory)) { + errStr << "ERROR: GdalNameMapperPattern::GdalNameMapperPattern(): " + "Specified directory '" + << directory << "' does not exist"; + throw std::runtime_error(errStr.str()); + } + _directory = boost::filesystem::absolute(directory).string(); + } else { + _directory = ""; + } + boost::smatch m; + boost::regex elemRegex("\\{(\\w+):(.*?)\\}"); + std::string lit; + std::string::const_iterator start = pattern.begin(), end = pattern.end(); + while (boost::regex_search(start, end, m, elemRegex)) { + appendLiteral(pattern, start - pattern.begin(), m.position()); + start = m[0].second; + std::string elemType = m[1].str(), elemFormat = m[2].str(); + std::ostringstream errPrefix; + errPrefix << "ERROR: GdalNameMapperPattern::GdalNameMapperPattern(): " + "Invalid format for element '" + << m.str() << "' in filename pattern '" << pattern << "'"; + if ((elemType == "latHem") || (elemType == "lonHem")) { + if (elemFormat.length() != 2) { + throw std::runtime_error(errPrefix.str() + + ": hemisphere specifier must be two " + "character long"); + } + _nameParts.emplace_back((elemType.substr(0, 3) == "lat") ? Src::Lat : + Src::Lon, + Op::Hemi, + elemFormat); + _fnmatchPattern += "[" + elemFormat + "]"; + } else if (elemType == "latDegFloor") { + appendLatLon(Src::Lat, Op::DegFloor1, elemFormat, errPrefix.str()); + } else if (elemType == "latDegCeil") { + appendLatLon(Src::Lat, Op::DegCeil, elemFormat, errPrefix.str()); + } else if (elemType == "lonDegFloor") { + appendLatLon(Src::Lon, Op::DegFloor, elemFormat, errPrefix.str()); + } else if (elemType == "lonDegCeil") { + appendLatLon(Src::Lon, Op::DegCeil1, elemFormat, errPrefix.str()); + } else { + throw std::runtime_error(errPrefix.str()); + } + } + appendLiteral(pattern, start - pattern.begin(), end - start); +} + +void GdalNameMapperPattern::appendLiteral(std::string pattern, size_t pos, size_t len) +{ + if (len == 0) { + return; + } + std::string lit = pattern.substr(pos, len); + if ((lit.find('{') != std::string::npos) || (lit.find('{') != std::string::npos)) { + std::ostringstream errStr; + errStr << "ERROR: GdalNameMapperPattern::appendLiteral(): " + "Filename pattern '" + << pattern << "' contains unrecognized element at offset " << pos; + throw std::runtime_error(errStr.str()); + } + _nameParts.emplace_back(Src::Str, Op::Literal, lit); + _fnmatchPattern += lit; +} + +void GdalNameMapperPattern::appendLatLon(Src src, + Op op, + const std::string &elemFormat, + const std::string errPrefix) +{ + if (elemFormat.find('%') != std::string::npos) { + throw std::runtime_error(errPrefix + ": format should not contain '%' character"); + } + char buf[50]; + std::string printfFormat = "%" + elemFormat + "d"; + int n = snprintf(buf, sizeof(buf), printfFormat.c_str(), 0); + if ((n <= 0) || (n >= (int)sizeof(buf))) { + throw std::runtime_error(errPrefix); + } + _nameParts.emplace_back(src, op, printfFormat); + if ((elemFormat.length() >= 2) && (elemFormat[0] == '0')) { + for (int i = atoi(elemFormat.substr(1, 1).c_str()); i--; _fnmatchPattern += "[0-9]") + ; + } else { + _fnmatchPattern += "*"; + } +} + +std::string GdalNameMapperPattern::fnmatchPattern() const +{ + return _fnmatchPattern; +} + +std::string GdalNameMapperPattern::nameFor(double latDeg, double lonDeg) +{ + for (const NamePart &np : _nameParts) { + double *src = (np.src == Src::Lat) ? &latDeg : &lonDeg; + switch (np.op) { + case Op::DegCeil1: + if (*src == std::round(*src)) { + *src += 1; + if ((np.src == Src::Lon) && (*src == 181)) { + *src = -179; + } + } + break; + case Op::DegFloor1: + if (*src == std::round(*src)) { + *src -= 1; + if ((np.src == Src::Lon) && (*src == -181)) { + *src = 179; + } + } + break; + default: + break; + } + } + std::string ret; + for (const NamePart &np : _nameParts) { + char buf[50]; + switch (np.op) { + case Op::Literal: + ret += np.str; + break; + case Op::Hemi: + ret += np.str[((np.src == Src::Lat) ? latDeg : lonDeg) < 0]; + break; + case Op::DegCeil1: + case Op::DegCeil: + snprintf(buf, + sizeof(buf), + np.str.c_str(), + (int)fabs(ceil((np.src == Src::Lat) ? latDeg : lonDeg))); + ret += buf; + break; + case Op::DegFloor: + case Op::DegFloor1: + snprintf(buf, + sizeof(buf), + np.str.c_str(), + (int)fabs(floor((np.src == Src::Lat) ? latDeg : lonDeg))); + ret += buf; + break; + } + } + if (!ret.empty() && !_directory.empty()) { + // Source pattern - and hence generated pattern - contains wildcard. + // So real file name should be found + + // Maybe it was found previously? + auto i = _wildcardMap.find(ret); + if (i != _wildcardMap.end()) { + return i->second; + } + // So it should be looked up in directory - will lookup for lexicographically + // largest candidate (to exclude ambiguity and to cater for 3DEP) + std::string candidate = ""; + for (boost::filesystem::directory_iterator di(_directory); + di != boost::filesystem::directory_iterator(); + ++di) { + std::string filename = di->path().filename().string(); + if (boost::filesystem::is_regular_file(di->path()) && + (fnmatch(ret.c_str(), filename.c_str(), 0) != FNM_NOMATCH) && + (candidate.empty() || (candidate < filename))) { + candidate = filename; + } + } + _wildcardMap[ret] = candidate; + return candidate; + } + return ret; +} + +std::unique_ptr GdalNameMapperPattern::make_unique(const std::string &pattern, + const std::string &directory) +{ + return std::unique_ptr(new GdalNameMapperPattern(pattern, directory)); +} + +/////////////////////////////////////////////////////////////////////////////// +// GdalNameMapperDirect +/////////////////////////////////////////////////////////////////////////////// + +GdalNameMapperDirect::GdalNameMapperDirect(const std::string &fnmatchPattern, + const std::string &directory) : + _fnmatchPattern(fnmatchPattern) +{ + GDALAllRegister(); + std::ostringstream errStr; + for (boost::filesystem::directory_iterator di(directory); + di != boost::filesystem::directory_iterator(); + ++di) { + std::string filename = di->path().filename().native(); + if ((!boost::filesystem::is_regular_file(di->path())) || + (fnmatch(fnmatchPattern.c_str(), filename.c_str(), 0) == FNM_NOMATCH)) { + continue; + } + GDALDataset *gdalDataSet = nullptr; + try { + gdalDataSet = static_cast( + GDALOpen(di->path().native().c_str(), GA_ReadOnly)); + _files.push_back(std::make_tuple( + GdalTransform(gdalDataSet, filename).makeBoundRect(), + filename)); + GDALClose(gdalDataSet); + } catch (...) { + if (gdalDataSet) { + GDALClose(gdalDataSet); + } + throw; + } + } +} + +std::string GdalNameMapperDirect::fnmatchPattern() const +{ + return _fnmatchPattern; +} + +std::string GdalNameMapperDirect::nameFor(double latDeg, double lonDeg) +{ + for (auto &fi : _files) { + if (std::get<0>(fi).contains(latDeg, lonDeg)) { + return std::get<1>(fi); + } + } + return ""; +} +std::unique_ptr GdalNameMapperDirect::make_unique( + const std::string &fnmatchPattern, + const std::string &directory) +{ + return std::unique_ptr( + new GdalNameMapperDirect(fnmatchPattern, directory)); +} diff --git a/src/afc-engine/GdalNameMapper.h b/src/afc-engine/GdalNameMapper.h new file mode 100644 index 0000000..021f743 --- /dev/null +++ b/src/afc-engine/GdalNameMapper.h @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2022 Broadcom. All rights reserved. + * The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate + * that owns the software below. + * This work is licensed under the OpenAFC Project License, a copy of which is + * included with this software program. + */ + +#ifndef GDAL_NAME_MAPPER_H +#define GDAL_NAME_MAPPER_H + +#include "GdalTransform.h" +#include +#include +#include +#include +#include + +/** @file + * Discovery of GDAL file name that corresponds to given latitude/longitude + */ + +/** Abstract base class for multifile (tiled) directory file naming handlers. + * Such handler class provides file name for given latitude/longitude, provides + * fnmatch()-compatible filename pattern that matches all files + */ +class GdalNameMapperBase +{ + public: + ////////////////////////////////////////////////// + // GdalNameMapperBase. Public instance methods + ////////////////////////////////////////////////// + + /** Virtual destructor */ + virtual ~GdalNameMapperBase() = default; + + /** Returns fnmatch-compatible filename pattern that matches all relevant + * GDAL files in the directory + */ + virtual std::string fnmatchPattern() const = 0; + + /** Provides file name for given latitude/longitude + * @param latDeg North-positive latitude in degrees + * @param lonDeg East-positive latitude in degrees + * @return File name for given coordinates, empty string if there is none + */ + virtual std::string nameFor(double latDeg, double lonDeg) = 0; +}; + +/** GDAL mapper, based on filename pattern. + * Pattern is a string with {type:format} inserts, containing its variable parts. + * Type is always a literal, format is variable and type-dependent. Currently + * supported type:format pairs + * - latDegCeil:d-spec (int)fabs(ceil(latDeg)). Format is sprintf()-compatible + * variable part of d-format specification (without leading '%' and + * trailing 'd') + * - latDegFloor:d-spec (int)fabs(floor1(latDeg)). Format is sprintf()-compatible + * variable part of d-format specification (without leading '%' and + * trailing 'd') + * - latHem:NS Latitude hemisphere. Format is character to use for north and + * character to use for south + * - lonDegCeil:d-spec (int)fabs(ceil1(lonDeg)). Format is sprintf()-compatible + * variable part of d-format specification (without leading '%' and + * trailing 'd') + * - lonDegFloor:d-spec (int)fabs(floor(lonDeg)). Format is sprintf()-compatible + * variable part of d-format specification (without leading '%' and + * trailing 'd') + * - lonHem:EW Longitude hemisphere. Format is character to use for east and + * character to use for west + * Since to avoid ambiguity this module when testing belonging point to rectangle + * includes top and left boundaries, but excludes right and bottom ones, floor1() + * and ceil1() differ from normal floor() and ceil on integer arguments: + * floor1(N) == N-1, ceil1(N) == N+1 for integer N + * Examples: + * - "USGS_1_{latHem:ns}{latDegCeil:}{lonHem:ew}{lonDegFloor:}*.tif" - 3DEP files + * - "{latHem:NS}{latDegFloor:02}{lonHem:EW}{lonDegFloor:03}.hgt" - SRTM files + * Note that since SRTM uses latDegFloor, e.g. heights for points on 25N are + * taken from N24....hgt + * As of time of this writing globe file name mapping is too obscure - use + * DirectGdalNameMapper for them + */ +class GdalNameMapperPattern : public GdalNameMapperBase +{ + public: + ////////////////////////////////////////////////// + // GdalNameMapperPattern. Public instance methods + ////////////////////////////////////////////////// + + /** Construct with filename pattern. + * @param pattern Filename pattern (see comments to class) + * @param directory Directory containing files. Must be specified if pattern + * contains wildcard symbols (*?[]), otherwise ignored + */ + GdalNameMapperPattern(const std::string &pattern, + const std::string &directory = ""); + + /** Virtual destructor */ + virtual ~GdalNameMapperPattern() = default; + + /** Returns fnmatch-compatible filename pattern that matches all relevant + * GDAL files in the directory + */ + virtual std::string fnmatchPattern() const; + + /** Provides file name for given latitude/longitude + * @param latDeg North-positive latitude in degrees + * @param lonDeg East-positive latitude in degrees + * @return File name for given coordinates, empty string if there is none + */ + virtual std::string nameFor(double latDeg, double lonDeg); + + ////////////////////////////////////////////////// + // GdalNameMapperPattern. Public instance methods + ////////////////////////////////////////////////// + + /** Creates pointer, passable to CachedGdal constructor. + * This function encapsulates C++11 hassle around unique + * pointer creation. Parameters are the same as constructor + * has. + * @param pattern Filename pattern (see comments to class) + * @param directory Directory containing files. Must be specified if pattern + * contains wildcard symbols (*?[]), otherwise ignored + */ + static std::unique_ptr make_unique( + const std::string &pattern, + const std::string &directory = ""); + + private: + ////////////////////////////////////////////////// + // GdalNameMapperPattern. Private constants + ////////////////////////////////////////////////// + + /** Source data for operation */ + enum class Src { + Str, /*!< String part */ + Lat, /*!< Latitude */ + Lon, /* _nameParts; + + /** fnmatch() pattern */ + std::string _fnmatchPattern; + + /** Directory for the case of pattern with wildcards, empty otherwise */ + std::string _directory; + + /** Maps generated wildcarded filenames to real filenames */ + std::map _wildcardMap; +}; + +/** GDAL mapper that obtains information from GDAL files in directory. + * Employment of this mapper is not recommended (as it reads metadata from every + * file during initialization) - especially on large directories, but can be + * tolerated on data that was not properly tiled + */ +class GdalNameMapperDirect : public GdalNameMapperBase +{ + public: + ////////////////////////////////////////////////// + // DirectGdalNameMapper. Public instance methods + ////////////////////////////////////////////////// + + /** Constructor + * @param fnmatchPattern Fnmatch-compatible pattern for relevant files + * @param directory Directory containing GDAL files + */ + GdalNameMapperDirect(const std::string &fnmatchPattern, + const std::string &directory); + + /** Virtual destructor */ + virtual ~GdalNameMapperDirect() = default; + + /** Returns fnmatch-compatible filename pattern that matches all relevant + * GDAL files in the directory + */ + virtual std::string fnmatchPattern() const; + + /** Provides file name for given latitude/longitude + * @param latDeg North-positive latitude in degrees + * @param lonDeg East-positive latitude in degrees + * @return File name for given coordinates, empty string if there is none + */ + virtual std::string nameFor(double latDeg, double lonDeg); + + ////////////////////////////////////////////////// + // DirectGdalNameMapper. Public instance methods + ////////////////////////////////////////////////// + + /** Creates pointer, passable to CachedGdal constructor. + * This function encapsulates C++11 hassle around unique + * pointer creation. Parameters are the same as constructor + * has. + * @param fnmatchPattern Fnmatch-compatible pattern for relevant files + * @param directory Directory containing GDAL files + */ + static std::unique_ptr make_unique( + const std::string &fnmatchPattern, + const std::string &directory); + + private: + ////////////////////////////////////////////////// + // GdalNameMapperDirect. Private instance data + ////////////////////////////////////////////////// + + /** Pattern for relevant files */ + std::string _fnmatchPattern; + + /** Coordinate rectangles mapped to file names */ + std::vector> _files; +}; + +#endif /* GDAL_NAME_MAPPER_H */ diff --git a/src/afc-engine/GdalTransform.cpp b/src/afc-engine/GdalTransform.cpp new file mode 100644 index 0000000..f09d574 --- /dev/null +++ b/src/afc-engine/GdalTransform.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2022 Broadcom. All rights reserved. + * The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate + * that owns the software below. + * This work is licensed under the OpenAFC Project License, a copy of which is + * included with this software program. + */ +#include "GdalTransform.h" + +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// + +GdalTransform::BoundRect::BoundRect(double latDegMin_, + double lonDegMin_, + double latDegMax_, + double lonDegMax_) : + latDegMin(latDegMin_), lonDegMin(lonDegMin_), latDegMax(latDegMax_), lonDegMax(lonDegMax_) +{ +} + +GdalTransform::BoundRect::BoundRect() : latDegMin(0), lonDegMin(0), latDegMax(0), lonDegMax(0) +{ +} + +bool GdalTransform::BoundRect::contains(double latDeg, double lonDeg) const +{ + lonDeg = rebaseLon(lonDeg, lonDegMin); + return (latDegMin < latDeg) && (lonDegMin <= lonDeg) && (latDeg <= latDegMax) && + (lonDeg < lonDegMax); +} + +void GdalTransform::BoundRect::combine(const GdalTransform::BoundRect &other) +{ + latDegMin = std::min(latDegMin, other.latDegMin); + lonDegMin = std::min(lonDegMin, other.lonDegMin); + latDegMax = std::max(latDegMax, other.latDegMax); + lonDegMax = std::max(lonDegMax, other.lonDegMax); +} + +double GdalTransform::BoundRect::rebaseLon(double lon, double base) +{ + while ((lon - 360) >= base) { + lon -= 360; + } + while (lon < base) { + lon += 360; + } + return lon; +} + +/////////////////////////////////////////////////////////////////////////////// + +GdalTransform::GdalTransform(GDALDataset *gdalDataSet_, const std::string &filename_) : margin(0) +{ + std::ostringstream errStr; + + // For GDAL files with latitude/longitude grid data meaning of + // transformation coefficients is a s follows: + // Longitude = gdalTransform(0) + PixelColumn*gdalTransform(1) + PixelRow*gdalTransform(2) + // Latitude = gdalTransform(3) + PixelColumn*gdalTransform(4) + PixelRow*gdalTransform(5) + double gdalTransform[6]; + CPLErr gdalErr = gdalDataSet_->GetGeoTransform(gdalTransform); + if (gdalErr != CPLErr::CE_None) { + errStr << "ERROR: GdalTransform::GdalTransform(): " + "Failed to read transformation from GDAL data file '" + << filename_ << "': " << CPLGetLastErrorMsg(); + throw std::runtime_error(errStr.str()); + } + if (!((gdalTransform[2] == 0) && (gdalTransform[4] == 0) && (gdalTransform[1] > 0) && + (gdalTransform[5] < 0))) { + errStr << "ERROR: GdalTransform::GdalTransform(): GDAL data file '" << filename_ + << "': does not contain 'north up' data"; + throw std::runtime_error(errStr.str()); + } + latPixPerDeg = -1. / gdalTransform[5]; + lonPixPerDeg = 1. / gdalTransform[1]; + latPixMax = -gdalTransform[3] / gdalTransform[5]; + lonPixMin = gdalTransform[0] / gdalTransform[1]; + latSize = gdalDataSet_->GetRasterYSize(); + lonSize = gdalDataSet_->GetRasterXSize(); +} + +GdalTransform::GdalTransform(const GdalTransform &gdalXform_, + int latPixOffset_, + int lonPixOffset_, + int latSize_, + int lonSize_) : + latPixPerDeg(gdalXform_.latPixPerDeg), + lonPixPerDeg(gdalXform_.lonPixPerDeg), + latPixMax(gdalXform_.latPixMax - latPixOffset_), + lonPixMin(gdalXform_.lonPixMin + lonPixOffset_), + latSize(latSize_), + lonSize(lonSize_), + margin(0) +{ +} + +GdalTransform::GdalTransform() : + latPixPerDeg(0), + lonPixPerDeg(0), + latPixMax(0), + lonPixMin(0), + latSize(0), + lonSize(0), + margin(0) +{ +} + +void GdalTransform::computePixel(double latDeg, double lonDeg, int *latIdx, int *lonIdx) const +{ + // Rebasing longitude relative to left side of bounding rectangle + lonDeg = BoundRect::rebaseLon(lonDeg, (lonPixMin + margin) / lonPixPerDeg); + + *latIdx = (int)std::floor(latPixMax - latDeg * latPixPerDeg); + *lonIdx = (int)std::floor(lonDeg * lonPixPerDeg - lonPixMin); + + // Off by more than a pixel - definitely a bug, not a rounding error + if ((*latIdx < -1) || (*latIdx > latSize) || (*lonIdx < -1) || (*lonIdx > lonSize)) { + auto br(makeBoundRect()); + std::ostringstream errStr; + errStr << "ERROR: GdalTransform::computePixel() internal error: point (" << latDeg + << "N, " << lonDeg << "E is out of tile/GDAL bounds of [" << br.latDegMin + << " - " << br.latDegMax << "]N X [" << br.lonDegMin << " - " << br.lonDegMax + << "]E"; + throw std::runtime_error(errStr.str()); + } + // Preventing rounding errors by less than a pixel + *latIdx = std::max(0, std::min(latSize - 1, *latIdx)); + *lonIdx = std::max(0, std::min(lonSize - 1, *lonIdx)); +} + +GdalTransform::BoundRect GdalTransform::makeBoundRect() const +{ + return BoundRect((latPixMax - latSize + margin) / latPixPerDeg, + (lonPixMin + margin) / lonPixPerDeg, + (latPixMax - margin) / latPixPerDeg, + (lonPixMin + lonSize - margin) / lonPixPerDeg); +} + +void GdalTransform::roundPpdToMultipleOf(double pixelsPerDegree) +{ + latPixPerDeg = std::round(latPixPerDeg / pixelsPerDegree) * pixelsPerDegree; + lonPixPerDeg = std::round(lonPixPerDeg / pixelsPerDegree) * pixelsPerDegree; + latPixMax = std::round(latPixMax / pixelsPerDegree) * pixelsPerDegree; + lonPixMin = std::round(lonPixMin / pixelsPerDegree) * pixelsPerDegree; +} + +void GdalTransform::setMarginsOutsideDeg(double deg) +{ + // ShiftUp ensures that fmod dividend is positive (latPixMax / latPixPerDeg + // is latitude, minimum latitude is -90) - to ensure rounding down, and yet + // does not affect fmod() result + double shiftUp = std::round(100 / deg) * deg; + margin = std::fmod(latPixMax / latPixPerDeg + shiftUp, deg) * latPixPerDeg; + // Since there is no more than one extra pixel, margin should be a multiple + // of 0.5 + margin = std::round(margin * 2) / 2; +} diff --git a/src/afc-engine/GdalTransform.h b/src/afc-engine/GdalTransform.h new file mode 100644 index 0000000..df091fb --- /dev/null +++ b/src/afc-engine/GdalTransform.h @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2022 Broadcom. All rights reserved. + * The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate + * that owns the software below. + * This work is licensed under the OpenAFC Project License, a copy of which is + * included with this software program. + */ + +#ifndef GDAL_TRANSFORM_H +#define GDAL_TRANSFORM_H + +/** @file + * Utility classes handling latitude/longitude to pixel row/column computations + */ + +/** Handles 6-parameter data transformation. + * Made struct (not class) as there will be API for fine-tuning them by an + * external function + */ + +#include + +class GDALDataset; // Forward declaration + +/** Parameters for coordinates to pixel index transformation. + * This structure contains parameters for inverse GDAL 6-element transformation, + * narrowed to the case of north-up geodetic coordinates. + * Chosen format is based on original transformation, used for DEP terrain + * (as of time of development the main source of terrain information) + */ +struct GdalTransform { + /** Bounding rectangle */ + struct BoundRect { + /** Constructor. + * @param latDegMin Minimum latitude in north-positive degrees + * @param lonDegMin Minimum longitude in east-positive degrees + * @param latDegMax Maximum latitude in north-positive degrees + * @param lonDegMax Maximum longitude in east-positive degrees + */ + BoundRect(double latDegMin, + double lonDegMin, + double latDegMax, + double lonDegMax); + + /** Default constructor */ + BoundRect(); + + /** True if rectangle contains given point. + * To facilitate unambiguous tiled inclusion detection, top and left + * boundaries included, bottom and right - not included) */ + bool contains(double latDeg, double lonDeg) const; + + /** Combine self with other */ + void combine(const BoundRect &other); + + /* Rectangle boundaries */ + double latDegMin; /*!< Minimum latitude in north-positive degrees */ + double lonDegMin; /*!< Minimum longitude in east-positive degrees */ + double latDegMax; /*!< Maximum latitude in north-positive degrees */ + double lonDegMax; /*!< Maximum longitude in east-positive degrees */ + + /** Rebase longitude value to [base, base+360[ range */ + static double rebaseLon(double lon, double base); + }; + + /** Constructor from GDAL transformation. + * @param gdalTransform 6-element GDAL transformation matrix + * @param margin Number of pixels along boundary rectangle to ignore (only + * affects makeBoundRect() result, does not affect transformation itself) + */ + GdalTransform(GDALDataset *gdalDataSet, const std::string &filename); + + /** Construct tile GDAL transformation from file GDAL transformation. + * @param gdalXform Transformation for the whole GDAL file + * @param latPixOffset Tile offset in number of pixels from first row + * @param lonPixOffset Tile offset in number of pixels from first column + * @param latSize Number of pixels in latitudinal direction + * @param lonSize Number of pixels in longitudinal direction + */ + GdalTransform(const GdalTransform &gdalXform, + int latPixOffset, + int lonPixOffset, + int latSize, + int lonSize); + + /** Default constructor */ + GdalTransform(); + + /** Compute pixel indices for given point. + * @param[in] latDeg Latitude in north-positive degrees to apply + * transformation to + * @param[in] lonDeg Longitude in east-positive degrees to apply + * transformation to + * @param[out] latIdx Resulted latitude index + * @param[out] lonIdx Resulted longitude index + */ + void computePixel(double latDeg, double lonDeg, int *latIdx, int *lonIdx) const; + + /** Returns GDAL data bounding rectangle. + * It is guaranteed that latitudes are in [-90, 90] range, + * latDegMin <= latDegMax, lonDegMin, <= lonDegMax. But it is not guaranteed + * that longitudes lie in [-180, 180[ range (out of range case - NLCD for + * Alaska) + */ + BoundRect makeBoundRect() const; + + /** Round pixels per degree and pixel boundaries to multiple of given value. + * @param pixelsPerDegree Value to make parameter component multiple of. + * E.g. 1 means that all parameters become integer + */ + void roundPpdToMultipleOf(double pixelsPerDegree); + + /** Treat everything outside given number of degrees as margin. + * E.g. setMarginsOutsideDeg(1) treat as margin everything outside whole + * number of degrees. + * This function uses latitudinal parameters for margin computation + * (assuming longitudinal margin has the same size). + * This function ensures that resulting margin is a multiple of 0.5 (as + * it is barely imaginably how can it be otherwise). + * @param deg Number of degrees to round grid to + */ + void setMarginsOutsideDeg(double deg); + + /** Number of pixels per degree in latitudinal direction */ + double latPixPerDeg; + /** Number of pixels per degree in longitudinal direction */ + double lonPixPerDeg; + /** Number of pixels from equator to top boundary (signed, north-positive) */ + double latPixMax; + /** Number of pixels from Greenwich to left boundary (signed, east-positive) */ + double lonPixMin; + /** Number of pixels in latitude direction */ + int latSize; + /** Number of pixels in longitude direction */ + int lonSize; + /** Number of (overlap) pixels along boundary to exclude from bounding rectangle */ + double margin; +}; +#endif /* GDAL_TRANSFORM_H */ diff --git a/src/afc-engine/GeodeticConstellation.h b/src/afc-engine/GeodeticConstellation.h new file mode 100644 index 0000000..f42a326 --- /dev/null +++ b/src/afc-engine/GeodeticConstellation.h @@ -0,0 +1,15 @@ + +#ifndef GEODETIC_CONSTELLATION_H +#define GEODETIC_CONSTELLATION_H + +#include +#include "GeodeticCoord.h" + +/** A constellation is a set of discrete earth-fixed points. + * There is no implied meaning to the order of the points. + */ +struct GeodeticConstellation : public QVector {}; + +Q_DECLARE_METATYPE(GeodeticConstellation); + +#endif /* GEODETIC_CONSTELLATION_H */ diff --git a/src/afc-engine/GeodeticCoord.cpp b/src/afc-engine/GeodeticCoord.cpp new file mode 100644 index 0000000..90782ae --- /dev/null +++ b/src/afc-engine/GeodeticCoord.cpp @@ -0,0 +1,41 @@ + +#include +#include +#include +#include "GeodeticCoord.h" + +namespace +{ +const int metaTypeId = qRegisterMetaType("GeodeticCoord"); +} + +const qreal GeodeticCoord::nan = std::numeric_limits::quiet_NaN(); + +bool GeodeticCoord::isNull() const +{ + return (std::isnan(longitudeDeg) || std::isnan(latitudeDeg) || std::isnan(heightKm)); +} + +void GeodeticCoord::normalize() +{ + // This is the number of (positive or negative) wraps occurring + const int over = std::floor((longitudeDeg + 180.0) / 360.0); + // Remove the number of wraps from longitude + longitudeDeg -= 360.0 * over; + // Clamp latitude + latitudeDeg = std::max(-90.0, std::min(+90.0, latitudeDeg)); +} + +bool GeodeticCoord::isIdenticalTo(const GeodeticCoord &other, qreal accuracy) const +{ + const qreal diffLon = longitudeDeg - other.longitudeDeg; + const qreal diffLat = latitudeDeg - other.latitudeDeg; + return ((std::abs(diffLon) <= accuracy) && (std::abs(diffLat) <= accuracy)); +} + +QDebug operator<<(QDebug stream, const GeodeticCoord &pt) +{ + stream.nospace() << "(lon: " << pt.longitudeDeg << ", lat: " << pt.latitudeDeg + << ", height: " << pt.heightKm << ")"; + return stream.space(); +} diff --git a/src/afc-engine/GeodeticCoord.h b/src/afc-engine/GeodeticCoord.h new file mode 100644 index 0000000..9d74a78 --- /dev/null +++ b/src/afc-engine/GeodeticCoord.h @@ -0,0 +1,97 @@ + + +#ifndef GEODETIC_COORD_H +#define GEODETIC_COORD_H + +#include +#include +//#include + +/** The base structure contains a 3D Earth-fixed geodetic coordinate. + * This is in the WGS84 ellipsoid, so any conversion functions must follow + * the WGS84 conventions. The height is an optional constructor parameter + * because it is unused in many cases, but it is still more consistent to + * have a single geodetic coordinate type than to have a 2D type and a 3D type + * separate from each other. + */ +struct GeodeticCoord { + /// Convenience definition for NaN value + static const qreal nan; + + /// Static helper function for lat/lon coordinate order. + static inline GeodeticCoord fromLatLon(qreal latDeg, qreal lonDeg, qreal htKm = 0) + { + return GeodeticCoord(lonDeg, latDeg, htKm); + } + + /// Static helper function for lon/lat coordinate order. + static inline GeodeticCoord fromLonLat(qreal lonDeg, qreal latDeg, qreal htKm = 0) + { + return GeodeticCoord(lonDeg, latDeg, htKm); + } + + /** Default constructor has NaN horizontal values to distinguish an + * invalid coordinate, but zero height value. + */ + GeodeticCoord() : longitudeDeg(nan), latitudeDeg(nan), heightKm(0) + { + } + + /** Construct a new geodetic coordinate, the height is optional. + */ + GeodeticCoord(qreal inLongitudeDeg, qreal inLatitudeDeg, qreal inHeightKm = 0) : + longitudeDeg(inLongitudeDeg), + latitudeDeg(inLatitudeDeg), + heightKm(inHeightKm) + { + } + + /** Implicit conversion to QVariant. + * @return The variant containing this GeodeticCoord value. + */ + operator QVariant() const + { + return QVariant::fromValue(*this); + } + + /** Determine if this location is the default NaN-valued. + * @return True if any coordinate is NaN. + */ + bool isNull() const; + + /** Normalize the coordinates in-place. + * Longitude is limited to the range [-180, +180) by wrapping. + * Latitude is limited to the range [-90, +90] by clamping. + */ + void normalize(); + + /** Get a normalized copy of the coordinates. + * @return A copy of these coordinates after normalize() is called on it. + */ + GeodeticCoord normalized() const + { + GeodeticCoord oth(*this); + oth.normalize(); + return oth; + } + + /** Compare two points to some required accuracy of sameness. + * @param other The point to compare against. + * @param accuracy This applies to difference between each of the + * longitudes and latitude independently. + */ + bool isIdenticalTo(const GeodeticCoord &other, qreal accuracy) const; + + /// Longitude referenced to WGS84 zero meridian; units of degrees. + qreal longitudeDeg; + /// Latitude referenced to WGS84 equator; units of degrees. + qreal latitudeDeg; + /// Height referenced to WGS84 ellipsoid; units of kilometers. + qreal heightKm; +}; +Q_DECLARE_METATYPE(GeodeticCoord); + +/// Allow debugging prints +QDebug operator<<(QDebug stream, const GeodeticCoord &pt); + +#endif /* GEODETIC_COORD_H */ diff --git a/src/afc-engine/ITMDLL.cpp b/src/afc-engine/ITMDLL.cpp new file mode 100644 index 0000000..0ee8c27 --- /dev/null +++ b/src/afc-engine/ITMDLL.cpp @@ -0,0 +1,1478 @@ +// ************************************* +// C++ routines for this program are taken from +// a translation of the FORTRAN code written by +// U.S. Department of Commerce NTIA/ITS +// Institute for Telecommunication Sciences +// ***************** +// Irregular Terrain Model (ITM) (Longley-Rice) +// ************************************* + +#include +#include +#include +#include +#include +#include +#include + +#define THIRD (1.0 / 3.0) +#define DllExport +#define DEG2RAD 1.74532925199e-02 +#define EARTHRADIUS 20902230.97 + +using namespace std; + +struct tcomplex { + double tcreal; + double tcimag; +}; + +struct prop_type { + double aref; + double dist; + double hg[2]; + double wn; + double dh; + double ens; + double gme; + double zgndreal; + double zgndimag; + double he[2]; + double dl[2]; + double the[2]; + int kwx; + int mdp; +}; + +struct propv_type { + double sgc; + int lvar; + int mdvar; + int klim; +}; + +struct propa_type { + double dlsa; + double dx; + double ael; + double ak1; + double ak2; + double aed; + double emd; + double aes; + double ems; + double dls[2]; + double dla; + double tha; +}; + +struct site { + double lat; + double lon; + double alt; + char name[50]; + char filename[255]; +}; + +int mymin(const int &i, const int &j) +{ + if (i < j) + return i; + else + return j; +} + +int mymax(const int &i, const int &j) +{ + if (i > j) + return i; + else + return j; +} + +double mymin(const double &a, const double &b) +{ + if (a < b) + return a; + else + return b; +} + +double mymax(const double &a, const double &b) +{ + if (a > b) + return a; + else + return b; +} + +double FORTRAN_DIM(const double &x, const double &y) +{ + // This performs the FORTRAN DIM function. + // result is x-y if x is greater than y; otherwise result is 0.0 + if (x > y) + return x - y; + else + return 0.0; +} + +double aknfe(const double &v2) +{ + double a; + if (v2 < 5.76) + a = 6.02 + 9.11 * sqrt(v2) - 1.27 * v2; + else + a = 12.953 + 4.343 * log(v2); + return a; +} + +double fht(const double &x, const double &pk) +{ + double w, fhtv; + if (x < 200.0) { + w = -log(pk); + if (pk < 1e-5 || x * pow(w, 3.0) > 5495.0) { + fhtv = -117.0; + if (x > 1.0) + fhtv = 17.372 * log(x) + fhtv; + } else + fhtv = 2.5e-5 * x * x / pk - 8.686 * w - 15.0; + } else { + fhtv = 0.05751 * x - 4.343 * log(x); + if (x < 2000.0) { + w = 0.0134 * x * exp(-0.005 * x); + fhtv = (1.0 - w) * fhtv + w * (17.372 * log(x) - 117.0); + } + } + return fhtv; +} + +double h0f(double r, double et) +{ + double a[5] = {25.0, 80.0, 177.0, 395.0, 705.0}; + double b[5] = {24.0, 45.0, 68.0, 80.0, 105.0}; + double q, x; + int it; + double h0fv; + it = (int)et; + if (it <= 0) { + it = 1; + q = 0.0; + } else if (it >= 5) { + it = 5; + q = 0.0; + } else + q = et - it; + x = pow(1.0 / r, 2.0); + h0fv = 4.343 * log((a[it - 1] * x + b[it - 1]) * x + 1.0); + if (q != 0.0) + h0fv = (1.0 - q) * h0fv + q * 4.343 * log((a[it] * x + b[it]) * x + 1.0); + return h0fv; +} + +double ahd(double td) +{ + int i; + double a[3] = {133.4, 104.6, 71.8}; + double b[3] = {0.332e-3, 0.212e-3, 0.157e-3}; + double c[3] = {-4.343, -1.086, 2.171}; + if (td <= 10e3) + i = 0; + else if (td <= 70e3) + i = 1; + else + i = 2; + return a[i] + b[i] * td + c[i] * log(td); +} + +struct adiff_statc { + double wd1, xd1, afo, qk, aht, xht; +}; + +double adiff(double d, prop_type &prop, propa_type &propa, struct adiff_statc &sc) +{ + complex prop_zgnd(prop.zgndreal, prop.zgndimag); + // static double wd1, xd1, afo, qk, aht, xht; + double a, q, pk, ds, th, wa, ar, wd, adiffv; + if (d == 0) { + q = prop.hg[0] * prop.hg[1]; + sc.qk = prop.he[0] * prop.he[1] - q; + if (prop.mdp < 0.0) + q += 10.0; + sc.wd1 = sqrt(1.0 + sc.qk / q); + sc.xd1 = propa.dla + propa.tha / prop.gme; + q = (1.0 - 0.8 * exp(-propa.dlsa / 50e3)) * prop.dh; + q *= 0.78 * exp(-pow(q / 16.0, 0.25)); // Initial diffraction constants, 11 + sc.afo = mymin(15.0, + 2.171 * log(1.0 + 4.77e-4 * prop.hg[0] * prop.hg[1] * prop.wn * q)); + sc.qk = 1.0 / abs(prop_zgnd); + sc.aht = 20.0; + sc.xht = 0.0; + for (int j = 0; j < 2; ++j) { + a = 0.5 * pow(prop.dl[j], 2.0) / prop.he[j]; + wa = pow(a * prop.wn, THIRD); + pk = sc.qk / wa; + q = (1.607 - pk) * 151.0 * wa * prop.dl[j] / a; + sc.xht += q; + sc.aht += fht(q, pk); + } + adiffv = 0.0; + } else { + th = propa.tha + d * prop.gme; + ds = d - propa.dla; + q = 0.0795775 * prop.wn * ds * pow(th, 2.0); + adiffv = aknfe(q * prop.dl[0] / (ds + prop.dl[0])) + + aknfe(q * prop.dl[1] / (ds + prop.dl[1])); + a = ds / th; + wa = pow(a * prop.wn, THIRD); + pk = sc.qk / wa; + q = (1.607 - pk) * 151.0 * wa * th + sc.xht; + ar = 0.05751 * q - 4.343 * log(q) - sc.aht; + q = (sc.wd1 + sc.xd1 / d) * + mymin(((1.0 - 0.8 * exp(-d / 50e3)) * prop.dh * prop.wn), 6283.2); + wd = 25.1 / (25.1 + sqrt(q)); + adiffv = ar * wd + (1.0 - wd) * adiffv + sc.afo; + } + return adiffv; +} + +struct ascat_statc { + double ad, rr, etq, h0s; +}; + +double ascat(double d, prop_type &prop, propa_type &propa, struct ascat_statc &sc) +{ + complex prop_zgnd(prop.zgndreal, prop.zgndimag); + // static double ad, rr, etq, h0s; + double h0, r1, r2, z0, ss, et, ett, th, q; + double ascatv; + if (d == 0.0) { + sc.ad = prop.dl[0] - prop.dl[1]; + sc.rr = prop.he[1] / prop.he[0]; + if (sc.ad < 0.0) { + sc.ad = -sc.ad; + sc.rr = 1.0 / sc.rr; + } + sc.etq = (5.67e-6 * prop.ens - 2.32e-3) * prop.ens + 0.031; + sc.h0s = -15.0; + ascatv = 0.0; + } else { + if (sc.h0s > 15.0) + h0 = sc.h0s; + else { + th = prop.the[0] + prop.the[1] + d * prop.gme; + r2 = 2.0 * prop.wn * th; + r1 = r2 * prop.he[0]; + r2 *= prop.he[1]; + if (r1 < 0.2 && r2 < 0.2) + return 1001.0; // <==== early return + ss = (d - sc.ad) / (d + sc.ad); + q = sc.rr / ss; + ss = mymax(0.1, ss); + q = mymin(mymax(0.1, q), 10.0); + z0 = (d - sc.ad) * (d + sc.ad) * th * 0.25 / d; + et = (sc.etq * exp(-pow(mymin(1.7, z0 / 8.0e3), 6.0)) + 1.0) * z0 / + 1.7556e3; + ett = mymax(et, 1.0); + h0 = (h0f(r1, ett) + h0f(r2, ett)) * 0.5; + h0 += mymin(h0, (1.38 - log(ett)) * log(ss) * log(q) * 0.49); + h0 = FORTRAN_DIM(h0, 0.0); + if (et < 1.0) + h0 = et * h0 + + (1.0 - et) * 4.343 * + log(pow((1.0 + 1.4142 / r1) * (1.0 + 1.4142 / r2), + 2.0) * + (r1 + r2) / (r1 + r2 + 2.8284)); + if (h0 > 15.0 && sc.h0s >= 0.0) + h0 = sc.h0s; + } + sc.h0s = h0; + th = propa.tha + d * prop.gme; + ascatv = ahd(th * d) + 4.343 * log(47.7 * prop.wn * pow(th, 4.0)) - + 0.1 * (prop.ens - 301.0) * exp(-th * d / 40e3) + h0; + } + return ascatv; +} + +double qerfi(double q) +{ + double x, t, v; + double c0 = 2.515516698; + double c1 = 0.802853; + double c2 = 0.010328; + double d1 = 1.432788; + double d2 = 0.189269; + double d3 = 0.001308; + + x = 0.5 - q; + t = mymax(0.5 - fabs(x), 0.000001); + t = sqrt(-2.0 * log(t)); + v = t - ((c2 * t + c1) * t + c0) / (((d3 * t + d2) * t + d1) * t + 1.0); + if (x < 0.0) + v = -v; + return v; +} + +void qlrps(double fmhz, double zsys, double en0, int ipol, double eps, double sgm, prop_type &prop) +{ + double gma = 157e-9; + prop.wn = fmhz / 47.7; + prop.ens = en0; + if (zsys != 0.0) + prop.ens *= exp(-zsys / 9460.0); + prop.gme = gma * (1.0 - 0.04665 * exp(prop.ens / 179.3)); + complex zq, prop_zgnd(prop.zgndreal, prop.zgndimag); + zq = complex(eps, 376.62 * sgm / prop.wn); + prop_zgnd = sqrt(zq - 1.0); + if (ipol != 0.0) + prop_zgnd = prop_zgnd / zq; + + prop.zgndreal = prop_zgnd.real(); + prop.zgndimag = prop_zgnd.imag(); +} + +double abq_alos(complex r) +{ + return r.real() * r.real() + r.imag() * r.imag(); +} + +struct alos_statc { + double wls; +}; + +double alos(double d, prop_type &prop, propa_type &propa, struct alos_statc &sc) +{ + complex prop_zgnd(prop.zgndreal, prop.zgndimag); + // static double wls; + complex r; + double s, sps, q; + double alosv; + if (d == 0.0) { + sc.wls = 0.021 / (0.021 + prop.wn * prop.dh / mymax(10e3, propa.dlsa)); + alosv = 0.0; + } else { + q = (1.0 - 0.8 * exp(-d / 50e3)) * prop.dh; + s = 0.78 * q * exp(-pow(q / 16.0, 0.25)); + q = prop.he[0] + prop.he[1]; + sps = q / sqrt(d * d + q * q); + r = (sps - prop_zgnd) / (sps + prop_zgnd) * exp(-mymin(10.0, prop.wn * s * sps)); + q = abq_alos(r); + if (q < 0.25 || q < sps) + r = r * sqrt(sps / q); + alosv = propa.emd * d + propa.aed; + q = prop.wn * prop.he[0] * prop.he[1] * 2.0 / d; + if (q > 1.57) + q = 3.14 - 2.4649 / q; + alosv = (-4.343 * log(abq_alos(complex(cos(q), -sin(q)) + r)) - alosv) * + sc.wls + + alosv; + } + return alosv; +} + +void qlra(int kst[], int klimx, int mdvarx, prop_type &prop, propv_type &propv) +{ + complex prop_zgnd(prop.zgndreal, prop.zgndimag); + double q; + for (int j = 0; j < 2; ++j) { + if (kst[j] <= 0) + prop.he[j] = prop.hg[j]; + else { + q = 4.0; + if (kst[j] != 1) + q = 9.0; + if (prop.hg[j] < 5.0) + q *= sin(0.3141593 * prop.hg[j]); + prop.he[j] = prop.hg[j] + + (1.0 + q) * + exp(-mymin(20.0, + 2.0 * prop.hg[j] / mymax(1e-3, prop.dh))); + } + q = sqrt(2.0 * prop.he[j] / prop.gme); + prop.dl[j] = q * exp(-0.07 * sqrt(prop.dh / mymax(prop.he[j], 5.0))); + prop.the[j] = (0.65 * prop.dh * (q / prop.dl[j] - 1.0) - 2.0 * prop.he[j]) / q; + } + prop.mdp = 1; + propv.lvar = mymax(propv.lvar, 3); + if (mdvarx >= 0) { + propv.mdvar = mdvarx; + propv.lvar = mymax(propv.lvar, 4); + } + if (klimx > 0) { + propv.klim = klimx; + propv.lvar = 5; + } +} + +struct lrprop_statc { + bool wlos, wscat; + double dmin, xae; +}; + +void lrprop(double d, prop_type &prop, propa_type &propa, struct lrprop_statc &sc) // PaulM_lrprop +{ + // static bool wlos, wscat; + // static double dmin, xae; + complex prop_zgnd(prop.zgndreal, prop.zgndimag); + double a0, a1, a2, a3, a4, a5, a6; + double d0, d1, d2, d3, d4, d5, d6; + bool wq; + double q; + int j; + + struct adiff_statc ad_st; + + if (prop.mdp != 0) { + for (j = 0; j < 2; j++) + propa.dls[j] = sqrt(2.0 * prop.he[j] / prop.gme); + propa.dlsa = propa.dls[0] + propa.dls[1]; + propa.dla = prop.dl[0] + prop.dl[1]; + propa.tha = mymax(prop.the[0] + prop.the[1], -propa.dla * prop.gme); + sc.wlos = false; + sc.wscat = false; + if (prop.wn < 0.838 || prop.wn > 210.0) { + prop.kwx = mymax(prop.kwx, 1); + } + for (j = 0; j < 2; j++) + if (prop.hg[j] < 1.0 || prop.hg[j] > 1000.0) { + prop.kwx = mymax(prop.kwx, 1); + } + for (j = 0; j < 2; j++) + if (abs(prop.the[j]) > 200e-3 || prop.dl[j] < 0.1 * propa.dls[j] || + prop.dl[j] > 3.0 * propa.dls[j]) { + prop.kwx = mymax(prop.kwx, 3); + // printf("kwx = 3 [1]\n"); + // printf("abs(prop.the[%d]) = %.8g (should be < %.8g)\n", j, + // abs(prop.the[j]), 200e-3); printf("prop.dl[%d] = %.8g, + // prop.dls[%d] = %.8g\n", j, prop.dl[j], j, propa.dls[j]); + // printf("prop.dl should be between 0.1 and 3 times dls, + // below:\n"); printf("prop.dls * 0.1 = %.8g\n", 0.1 * + // propa.dls[j]); printf("prop.dls * 3 = %.8g\n", 3 * propa.dls[j]); + // printf("failures = %d %d %d\n", abs(prop.the[j]) >200e-3, + // prop.dl[j]<0.1*propa.dls[j], + // prop.dl[j]>3.0*propa.dls[j]); + } + if (prop.ens < 250.0 || prop.ens > 400.0 || prop.gme < 75e-9 || prop.gme > 250e-9 || + prop_zgnd.real() <= abs(prop_zgnd.imag()) || prop.wn < 0.419 || + prop.wn > 420.0) { + prop.kwx = 4; + } + for (j = 0; j < 2; j++) + if (prop.hg[j] < 0.5 || prop.hg[j] > 3000.0) { + prop.kwx = 4; + } + sc.dmin = abs(prop.he[0] - prop.he[1]) / 200e-3; + q = adiff(0.0, prop, propa, ad_st); + sc.xae = pow(prop.wn * pow(prop.gme, 2), -THIRD); + d3 = mymax(propa.dlsa, 1.3787 * sc.xae + propa.dla); + d4 = d3 + 2.7574 * sc.xae; + a3 = adiff(d3, prop, propa, ad_st); + a4 = adiff(d4, prop, propa, ad_st); + propa.emd = (a4 - a3) / (d4 - d3); + propa.aed = a3 - propa.emd * d3; + } + if (prop.mdp >= 0) { + prop.mdp = 0; + prop.dist = d; + } + if (prop.dist > 0.0) { + if (prop.dist > 1000e3) { + prop.kwx = mymax(prop.kwx, 1); + } + if (prop.dist < sc.dmin) { + prop.kwx = mymax(prop.kwx, 3); + // printf("kwx = 3 [2]\n"); + } + if (prop.dist < 1e3 || prop.dist > 2000e3) { + prop.kwx = 4; + } + } + if (prop.dist < propa.dlsa) { + if (!sc.wlos) { + struct alos_statc asc; + q = alos(0.0, prop, propa, asc); + d2 = propa.dlsa; + a2 = propa.aed + d2 * propa.emd; + d0 = 1.908 * prop.wn * prop.he[0] * prop.he[1]; + if (propa.aed >= 0.0) { + d0 = mymin(d0, 0.5 * propa.dla); + d1 = d0 + 0.25 * (propa.dla - d0); + } else + d1 = mymax(-propa.aed / propa.emd, 0.25 * propa.dla); + a1 = alos(d1, prop, propa, asc); + wq = false; + if (d0 < d1) { + a0 = alos(d0, prop, propa, asc); + q = log(d2 / d0); + propa.ak2 = mymax(0.0, + ((d2 - d0) * (a1 - a0) - (d1 - d0) * (a2 - a0)) / + ((d2 - d0) * log(d1 / d0) - + (d1 - d0) * q)); + wq = propa.aed >= 0.0 || propa.ak2 > 0.0; + if (wq) { + propa.ak1 = (a2 - a0 - propa.ak2 * q) / (d2 - d0); + if (propa.ak1 < 0.0) { + propa.ak1 = 0.0; + propa.ak2 = FORTRAN_DIM(a2, a0) / q; + if (propa.ak2 == 0.0) + propa.ak1 = propa.emd; + } + } + } + if (!wq) { + propa.ak1 = FORTRAN_DIM(a2, a1) / (d2 - d1); + propa.ak2 = 0.0; + if (propa.ak1 == 0.0) + propa.ak1 = propa.emd; + } + propa.ael = a2 - propa.ak1 * d2 - propa.ak2 * log(d2); + sc.wlos = true; + } + if (prop.dist > 0.0) + prop.aref = propa.ael + propa.ak1 * prop.dist + propa.ak2 * log(prop.dist); + } + if (prop.dist <= 0.0 || prop.dist >= propa.dlsa) { + if (!sc.wscat) { + struct ascat_statc asc; + + q = ascat(0.0, prop, propa, asc); + d5 = propa.dla + 200e3; + d6 = d5 + 200e3; + a6 = ascat(d6, prop, propa, asc); + a5 = ascat(d5, prop, propa, asc); + if (a5 < 1000.0) { + propa.ems = (a6 - a5) / 200e3; + propa.dx = mymax(propa.dlsa, + mymax(propa.dla + + 0.3 * sc.xae * log(47.7 * prop.wn), + (a5 - propa.aed - propa.ems * d5) / + (propa.emd - propa.ems))); + propa.aes = (propa.emd - propa.ems) * propa.dx + propa.aed; + } else { + propa.ems = propa.emd; + propa.aes = propa.aed; + propa.dx = 10.e6; + } + sc.wscat = true; + } + if (prop.dist > propa.dx) + prop.aref = propa.aes + propa.ems * prop.dist; + else + prop.aref = propa.aed + propa.emd * prop.dist; + } + prop.aref = mymax(prop.aref, 0.0); +} + +double curve(double const &c1, + double const &c2, + double const &x1, + double const &x2, + double const &x3, + double const &de) +{ + return (c1 + c2 / (1.0 + pow((de - x2) / x3, 2.0))) * pow(de / x1, 2.0) / + (1.0 + pow(de / x1, 2.0)); +} + +struct avar_statc { + int kdv; + double dexa, de, vmd, vs0, sgl, sgtm, sgtp, sgtd, tgtd, gm, gp, cv1, cv2, yv1, yv2, + yv3, csm1, csm2, ysm1, ysm2, ysm3, csp1, csp2, ysp1, ysp2, ysp3, csd1, zd, + cfm1, cfm2, cfm3, cfp1, cfp2, cfp3; + bool ws, w1; +}; + +double avar(double zzt, + double zzl, + double zzc, + prop_type &prop, + propv_type &propv, + struct avar_statc &sc) +{ // static int kdv; + // static double dexa, de, vmd, vs0, sgl, sgtm, sgtp, sgtd, tgtd, + // gm, gp, cv1, cv2, yv1, yv2, yv3, csm1, csm2, ysm1, ysm2, + // ysm3, csp1, csp2, ysp1, ysp2, ysp3, csd1, zd, cfm1, cfm2, + // cfm3, cfp1, cfp2, cfp3; + double bv1[7] = {-9.67, -0.62, 1.26, -9.21, -0.62, -0.39, 3.15}; + double bv2[7] = {12.7, 9.19, 15.5, 9.05, 9.19, 2.86, 857.9}; + double xv1[7] = {144.9e3, 228.9e3, 262.6e3, 84.1e3, 228.9e3, 141.7e3, 2222.e3}; + double xv2[7] = {190.3e3, 205.2e3, 185.2e3, 101.1e3, 205.2e3, 315.9e3, 164.8e3}; + double xv3[7] = {133.8e3, 143.6e3, 99.8e3, 98.6e3, 143.6e3, 167.4e3, 116.3e3}; + double bsm1[7] = {2.13, 2.66, 6.11, 1.98, 2.68, 6.86, 8.51}; + double bsm2[7] = {159.5, 7.67, 6.65, 13.11, 7.16, 10.38, 169.8}; + double xsm1[7] = {762.2e3, 100.4e3, 138.2e3, 139.1e3, 93.7e3, 187.8e3, 609.8e3}; + double xsm2[7] = {123.6e3, 172.5e3, 242.2e3, 132.7e3, 186.8e3, 169.6e3, 119.9e3}; + double xsm3[7] = {94.5e3, 136.4e3, 178.6e3, 193.5e3, 133.5e3, 108.9e3, 106.6e3}; + double bsp1[7] = {2.11, 6.87, 10.08, 3.68, 4.75, 8.58, 8.43}; + double bsp2[7] = {102.3, 15.53, 9.60, 159.3, 8.12, 13.97, 8.19}; + double xsp1[7] = {636.9e3, 138.7e3, 165.3e3, 464.4e3, 93.2e3, 216.0e3, 136.2e3}; + double xsp2[7] = {134.8e3, 143.7e3, 225.7e3, 93.1e3, 135.9e3, 152.0e3, 188.5e3}; + double xsp3[7] = {95.6e3, 98.6e3, 129.7e3, 94.2e3, 113.4e3, 122.7e3, 122.9e3}; + double bsd1[7] = {1.224, 0.801, 1.380, 1.000, 1.224, 1.518, 1.518}; + double bzd1[7] = {1.282, 2.161, 1.282, 20., 1.282, 1.282, 1.282}; + double bfm1[7] = {1.0, 1.0, 1.0, 1.0, 0.92, 1.0, 1.0}; + double bfm2[7] = {0.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.0}; + double bfm3[7] = {0.0, 0.0, 0.0, 0.0, 1.77, 0.0, 0.0}; + double bfp1[7] = {1.0, 0.93, 1.0, 0.93, 0.93, 1.0, 1.0}; + double bfp2[7] = {0.0, 0.31, 0.0, 0.19, 0.31, 0.0, 0.0}; + double bfp3[7] = {0.0, 2.00, 0.0, 1.79, 2.00, 0.0, 0.0}; + // static bool ws, w1; + double rt = 7.8, rl = 24.0, avarv, q, vs, zt, zl, zc; + double sgt, yr; + int temp_klim = propv.klim - 1; + + if (propv.lvar > 0) { + switch (propv.lvar) { + default: + if (propv.klim <= 0 || propv.klim > 7) { + propv.klim = 5; + temp_klim = 4; + { + prop.kwx = mymax(prop.kwx, 2); + } + } + sc.cv1 = bv1[temp_klim]; + sc.cv2 = bv2[temp_klim]; + sc.yv1 = xv1[temp_klim]; + sc.yv2 = xv2[temp_klim]; + sc.yv3 = xv3[temp_klim]; + sc.csm1 = bsm1[temp_klim]; + sc.csm2 = bsm2[temp_klim]; + sc.ysm1 = xsm1[temp_klim]; + sc.ysm2 = xsm2[temp_klim]; + sc.ysm3 = xsm3[temp_klim]; + sc.csp1 = bsp1[temp_klim]; + sc.csp2 = bsp2[temp_klim]; + sc.ysp1 = xsp1[temp_klim]; + sc.ysp2 = xsp2[temp_klim]; + sc.ysp3 = xsp3[temp_klim]; + sc.csd1 = bsd1[temp_klim]; + sc.zd = bzd1[temp_klim]; + sc.cfm1 = bfm1[temp_klim]; + sc.cfm2 = bfm2[temp_klim]; + sc.cfm3 = bfm3[temp_klim]; + sc.cfp1 = bfp1[temp_klim]; + sc.cfp2 = bfp2[temp_klim]; + sc.cfp3 = bfp3[temp_klim]; + case 4: + sc.kdv = propv.mdvar; + sc.ws = sc.kdv >= 20; + if (sc.ws) + sc.kdv -= 20; + sc.w1 = sc.kdv >= 10; + if (sc.w1) + sc.kdv -= 10; + if (sc.kdv < 0 || sc.kdv > 3) { + sc.kdv = 0; + prop.kwx = mymax(prop.kwx, 2); + } + case 3: + q = log(0.133 * prop.wn); + sc.gm = sc.cfm1 + sc.cfm2 / (pow(sc.cfm3 * q, 2.0) + 1.0); + sc.gp = sc.cfp1 + sc.cfp2 / (pow(sc.cfp3 * q, 2.0) + 1.0); + case 2: + sc.dexa = sqrt(18e6 * prop.he[0]) + sqrt(18e6 * prop.he[1]) + + pow((575.7e12 / prop.wn), THIRD); + case 1: + if (prop.dist < sc.dexa) + sc.de = 130e3 * prop.dist / sc.dexa; + else + sc.de = 130e3 + prop.dist - sc.dexa; + } + sc.vmd = curve(sc.cv1, sc.cv2, sc.yv1, sc.yv2, sc.yv3, sc.de); + sc.sgtm = curve(sc.csm1, sc.csm2, sc.ysm1, sc.ysm2, sc.ysm3, sc.de) * sc.gm; + sc.sgtp = curve(sc.csp1, sc.csp2, sc.ysp1, sc.ysp2, sc.ysp3, sc.de) * sc.gp; + sc.sgtd = sc.sgtp * sc.csd1; + sc.tgtd = (sc.sgtp - sc.sgtd) * sc.zd; + if (sc.w1) + sc.sgl = 0.0; + else { + q = (1.0 - 0.8 * exp(-prop.dist / 50e3)) * prop.dh * prop.wn; + sc.sgl = 10.0 * q / (q + 13.0); + } + if (sc.ws) + sc.vs0 = 0.0; + else + sc.vs0 = pow(5.0 + 3.0 * exp(-sc.de / 100e3), 2.0); + propv.lvar = 0; + } + zt = zzt; + zl = zzl; + zc = zzc; + switch (sc.kdv) { + case 0: + zt = zc; + zl = zc; + break; + case 1: + zl = zc; + break; + case 2: + zl = zt; + } + if (fabs(zt) > 3.1 || fabs(zl) > 3.1 || fabs(zc) > 3.1) { + prop.kwx = mymax(prop.kwx, 1); + } + if (zt < 0.0) + sgt = sc.sgtm; + else if (zt <= sc.zd) + sgt = sc.sgtp; + else + sgt = sc.sgtd + sc.tgtd / zt; + vs = sc.vs0 + pow(sgt * zt, 2.0) / (rt + zc * zc) + pow(sc.sgl * zl, 2.0) / (rl + zc * zc); + if (sc.kdv == 0) { + yr = 0.0; + propv.sgc = sqrt(sgt * sgt + sc.sgl * sc.sgl + vs); + } else if (sc.kdv == 1) { + yr = sgt * zt; + propv.sgc = sqrt(sc.sgl * sc.sgl + vs); + } else if (sc.kdv == 2) { + yr = sqrt(sgt * sgt + sc.sgl * sc.sgl) * zt; + propv.sgc = sqrt(vs); + } else { + yr = sgt * zt + sc.sgl * zl; + propv.sgc = sqrt(vs); + } + avarv = prop.aref - sc.vmd - yr - propv.sgc * zc; + if (avarv < 0.0) + avarv = avarv * (29.0 - avarv) / (29.0 - 10.0 * avarv); + return avarv; +} + +void hzns(double pfl[], prop_type &prop) +{ + bool wq; + int np; + double xi, za, zb, qc, q, sb, sa; + + np = (int)pfl[0]; + xi = pfl[1]; + za = pfl[2] + prop.hg[0]; + zb = pfl[np + 2] + prop.hg[1]; + qc = 0.5 * prop.gme; + q = qc * prop.dist; + prop.the[1] = (zb - za) / prop.dist; + prop.the[0] = prop.the[1] - q; + prop.the[1] = -prop.the[1] - q; + prop.dl[0] = prop.dist; + prop.dl[1] = prop.dist; + // printf("prop.dl = %.8g, %.8g\n", prop.dl[0], prop.dl[1]); + + for (int i = 0; i < np; i++) { + // printf("pfl[%d] = %.8g\n", i, pfl[i + 2]); + } + if (np >= 2) { + sa = 0.0; + sb = prop.dist; + wq = true; + for (int i = 1; i < np; i++) { + sa += xi; + sb -= xi; + q = pfl[i + 2] - (qc * sa + prop.the[0]) * sa - za; + // printf("For point %d, q = %.8g - (%.8g * %.8g + %.8g) * %.8g - %.8g = + // %.8g\n", i, pfl[i+2], qc, sa, prop.the[0], sa, za, q); + if (q > 0.0) { + prop.the[0] += q / sa; + prop.dl[0] = sa; + wq = false; + } + if (!wq) { + q = pfl[i + 2] - (qc * sb + prop.the[1]) * sb - zb; + if (q > 0.0) { + prop.the[1] += q / sb; + prop.dl[1] = sb; + // printf("prop.dl[1] = %.8g because q = %.8g\n", + // prop.dl[1], q); + } + } + } + } +} + +void z1sq1(double z[], const double &x1, const double &x2, double &z0, double &zn) +{ + double xn, xa, xb, x, a, b; + int n, ja, jb; + xn = z[0]; + xa = int(FORTRAN_DIM(x1 / z[1], 0.0)); + xb = xn - int(FORTRAN_DIM(xn, x2 / z[1])); + if (xb <= xa) { + xa = FORTRAN_DIM(xa, 1.0); + xb = xn - FORTRAN_DIM(xn, xb + 1.0); + } + ja = (int)xa; + jb = (int)xb; + n = jb - ja; + xa = xb - xa; + x = -0.5 * xa; + xb += x; + a = 0.5 * (z[ja + 2] + z[jb + 2]); + b = 0.5 * (z[ja + 2] - z[jb + 2]) * x; + for (int i = 2; i <= n; ++i) { + ++ja; + x += 1.0; + a += z[ja + 2]; + b += z[ja + 2] * x; + } + a /= xa; + b = b * 12.0 / ((xa * xa + 2.0) * xa); + z0 = a - b * xb; + zn = a + b * (xn - xb); +} + +double qtile(const int &nn, double a[], const int &ir) +{ + double q, r; + int m, n, i, j, j1, i0, k; + bool done = false; + bool goto10 = true; + + m = 0; + n = nn; + k = mymin(mymax(0, ir), n); + while (!done) { + if (goto10) { + q = a[k]; + i0 = m; + j1 = n; + } + i = i0; + while (i <= n && a[i] >= q) + i++; + if (i > n) + i = n; + j = j1; + while (j >= m && a[j] <= q) + j--; + if (j < m) + j = m; + if (i < j) { + r = a[i]; + a[i] = a[j]; + a[j] = r; + i0 = i + 1; + j1 = j - 1; + goto10 = false; + } else if (i < k) { + a[k] = a[i]; + a[i] = q; + m = i + 1; + goto10 = true; + } else if (j > k) { + a[k] = a[j]; + a[j] = q; + n = j - 1; + goto10 = true; + } else + done = true; + } + return q; +} + +double qerf(const double &z) +{ + double b1 = 0.319381530, b2 = -0.356563782, b3 = 1.781477937; + double b4 = -1.821255987, b5 = 1.330274429; + double rp = 4.317008, rrt2pi = 0.398942280; + double t, x, qerfv; + x = z; + t = fabs(x); + if (t >= 10.0) + qerfv = 0.0; + else { + t = rp / (t + rp); + qerfv = exp(-0.5 * x * x) * rrt2pi * + ((((b5 * t + b4) * t + b3) * t + b2) * t + b1) * t; + } + if (x < 0.0) + qerfv = 1.0 - qerfv; + return qerfv; +} + +double d1thx(double pfl[], const double &x1, const double &x2) +{ + int np, ka, kb, n, k, j; + double d1thxv, sn, xa, xb; + double *s; + + np = (int)pfl[0]; + xa = x1 / pfl[1]; + xb = x2 / pfl[1]; + d1thxv = 0.0; + if (xb - xa < 2.0) // exit out + return d1thxv; + ka = (int)(0.1 * (xb - xa + 8.0)); + ka = mymin(mymax(4, ka), 25); + n = 10 * ka - 5; + kb = n - ka + 1; + sn = n - 1; + s = new double[n + 2]; + assert(s != (double *)NULL); + s[0] = sn; + s[1] = 1.0; + xb = (xb - xa) / sn; + k = (int)(xa + 1.0); + xa -= (double)k; + for (j = 0; j < n; j++) { + while (xa > 0.0 && k < np) { + xa -= 1.0; + ++k; + } + s[j + 2] = pfl[k + 2] + (pfl[k + 2] - pfl[k + 1]) * xa; + xa = xa + xb; + } + z1sq1(s, 0.0, sn, xa, xb); + xb = (xb - xa) / sn; + for (j = 0; j < n; j++) { + s[j + 2] -= xa; + xa = xa + xb; + } + d1thxv = qtile(n - 1, s + 2, ka - 1) - qtile(n - 1, s + 2, kb - 1); + d1thxv /= 1.0 - 0.8 * exp(-(x2 - x1) / 50.0e3); + delete[] s; + return d1thxv; +} + +void qlrpfl(double pfl[], + int klimx, + int mdvarx, + prop_type &prop, + propa_type &propa, + propv_type &propv) +{ + int np, j; + double xl[2], q, za, zb; + + prop.dist = pfl[0] * pfl[1]; + np = (int)pfl[0]; + // #error Check this + // printf("THIS POPULATES DL\n"); + hzns(pfl, prop); + for (j = 0; j < 2; j++) { + xl[j] = mymin(15.0 * prop.hg[j], 0.1 * prop.dl[j]); + } + xl[1] = prop.dist - xl[1]; + prop.dh = d1thx(pfl, xl[0], xl[1]); + if (prop.dl[0] + prop.dl[1] > 1.5 * prop.dist) { + z1sq1(pfl, xl[0], xl[1], za, zb); + prop.he[0] = prop.hg[0] + FORTRAN_DIM(pfl[2], za); + prop.he[1] = prop.hg[1] + FORTRAN_DIM(pfl[np + 2], zb); + for (j = 0; j < 2; j++) + prop.dl[j] = sqrt(2.0 * prop.he[j] / prop.gme) * + exp(-0.07 * sqrt(prop.dh / mymax(prop.he[j], 5.0))); + q = prop.dl[0] + prop.dl[1]; + + if (q <= prop.dist) { + q = pow(prop.dist / q, 2.0); + for (j = 0; j < 2; j++) { + prop.he[j] *= q; + prop.dl[j] = sqrt(2.0 * prop.he[j] / prop.gme) * + exp(-0.07 * sqrt(prop.dh / mymax(prop.he[j], 5.0))); + } + } + for (j = 0; j < 2; j++) { + q = sqrt(2.0 * prop.he[j] / prop.gme); + prop.the[j] = (0.65 * prop.dh * (q / prop.dl[j] - 1.0) - 2.0 * prop.he[j]) / + q; + } + } else { + z1sq1(pfl, xl[0], 0.9 * prop.dl[0], za, q); + z1sq1(pfl, prop.dist - 0.9 * prop.dl[1], xl[1], q, zb); + prop.he[0] = prop.hg[0] + FORTRAN_DIM(pfl[2], za); + prop.he[1] = prop.hg[1] + FORTRAN_DIM(pfl[np + 2], zb); + } + prop.mdp = -1; + propv.lvar = mymax(propv.lvar, 3); + if (mdvarx >= 0) { + propv.mdvar = mdvarx; + propv.lvar = mymax(propv.lvar, 4); + } + if (klimx > 0) { + propv.klim = klimx; + propv.lvar = 5; + } + struct lrprop_statc lrc; + lrprop(0.0, prop, propa, lrc); +} + +double deg2rad(double d) +{ + return d * 3.1415926535897 / 180.0; +} + +//******************************************************** +//* Point-To-Point Mode Calculations * +//******************************************************** + +void point_to_point(double elev[], + double tht_m, + double rht_m, + double eps_dielect, + double sgm_conductivity, + double eno_ns_surfref, + double frq_mhz, + int radio_climate, + int pol, + double conf, + double rel, + double &dbloss, + std::string &strmode, + int &errnum) +// pol: 0-Horizontal, 1-Vertical +// radio_climate: 1-Equatorial, 2-Continental Subtropical, 3-Maritime Tropical, +// 4-Desert, 5-Continental Temperate, 6-Maritime Temperate, Over Land, +// 7-Maritime Temperate, Over Sea +// conf, rel: .01 to .99 +// elev[]: [num points - 1], [delta dist(meters)], [height(meters) point 1], ..., [height(meters) +// point n] errnum: 0- No Error. +// 1- Warning: Some parameters are nearly out of range. +// Results should be used with caution. +// 2- Note: Default parameters have been substituted for impossible ones. +// 3- Warning: A combination of parameters is out of range. +// Results are probably invalid. +// Other- Warning: Some parameters are out of range. +// Results are probably invalid. +{ + prop_type prop; + propv_type propv; + propa_type propa; + double zsys = 0; + double zc, zr; + double eno, enso, q; + long ja, jb, i, np; + // double dkm, xkm; + double fs; + + prop.hg[0] = tht_m; + prop.hg[1] = rht_m; + propv.klim = radio_climate; + prop.kwx = 0; + propv.lvar = 5; + prop.mdp = -1; + zc = qerfi(conf); + zr = qerfi(rel); + np = (long)elev[0]; + // dkm = (elev[1] * elev[0]) / 1000.0; + // xkm = elev[1] / 1000.0; + eno = eno_ns_surfref; + enso = 0.0; + q = enso; + if (q <= 0.0) { + ja = 3.0 + 0.1 * elev[0]; + jb = np - ja + 6; + for (i = ja - 1; i < jb; ++i) + zsys += elev[i]; + zsys /= (jb - ja + 1); + q = eno; + } + propv.mdvar = 13; + qlrps(frq_mhz, zsys, q, pol, eps_dielect, sgm_conductivity, prop); + for (int j = 0; j < 2; j++) { + // printf("after qlrps prop.dl[%d] = %.8g\n", j, prop.dl[j]); + } + qlrpfl(elev, propv.klim, propv.mdvar, prop, propa, propv); // adjusts prop.dl calling lrprop + fs = 32.45 + 20.0 * log10(frq_mhz) + 20.0 * log10(prop.dist / 1000.0); + // printf("prop.dist = %.8g, fs = %.8g\n", prop.dist, fs); + q = prop.dist - propa.dla; + // printf("prop.dist = %.8g, propa.dla = %.8g\n", prop.dist, propa.dla); + if (int(q) < 0.0) + strmode = "Line-Of-Sight Mode"; + else { + if (int(q) == 0.0) + strmode = "Single Horizon"; + else if (int(q) > 0.0) + strmode = "Double Horizon"; + if (prop.dist <= propa.dlsa || prop.dist <= propa.dx) + strmode += ", Diffraction Dominant"; + else if (prop.dist > propa.dx) + strmode += ", Troposcatter Dominant"; + } + // printf("fs = %.8g\n", fs); + struct avar_statc asc; + dbloss = avar(zr, 0.0, zc, prop, propv, asc) + fs; + errnum = prop.kwx; +} + +void point_to_pointMDH(double elev[], + double tht_m, + double rht_m, + double eps_dielect, + double sgm_conductivity, + double eno_ns_surfref, + double frq_mhz, + int radio_climate, + int pol, + double timepct, + double locpct, + double confpct, + double &dbloss, + int &propmode, + double &deltaH, + int &errnum) +// pol: 0-Horizontal, 1-Vertical +// radio_climate: 1-Equatorial, 2-Continental Subtropical, 3-Maritime Tropical, +// 4-Desert, 5-Continental Temperate, 6-Maritime Temperate, Over Land, +// 7-Maritime Temperate, Over Sea +// timepct, locpct, confpct: .01 to .99 +// elev[]: [num points - 1], [delta dist(meters)], [height(meters) point 1], ..., [height(meters) +// point n] propmode: Value Mode +// -1 mode is undefined +// 0 Line of Sight +// 5 Single Horizon, Diffraction +// 6 Single Horizon, Troposcatter +// 9 Double Horizon, Diffraction +// 10 Double Horizon, Troposcatter +// errnum: 0- No Error. +// 1- Warning: Some parameters are nearly out of range. +// Results should be used with caution. +// 2- Note: Default parameters have been substituted for impossible ones. +// 3- Warning: A combination of parameters is out of range. +// Results are probably invalid. +// Other- Warning: Some parameters are out of range. +// Results are probably invalid. +{ + prop_type prop; + propv_type propv; + propa_type propa; + double zsys = 0; + double ztime, zloc, zconf; + double eno, enso, q; + long ja, jb, i, np; + // double dkm, xkm; + double fs; + + propmode = -1; // mode is undefined + prop.hg[0] = tht_m; + prop.hg[1] = rht_m; + propv.klim = radio_climate; + prop.kwx = 0; + propv.lvar = 5; + prop.mdp = -1; + ztime = qerfi(timepct); + zloc = qerfi(locpct); + zconf = qerfi(confpct); + + np = (long)elev[0]; + // dkm = (elev[1] * elev[0]) / 1000.0; + // xkm = elev[1] / 1000.0; + eno = eno_ns_surfref; + enso = 0.0; + q = enso; + if (q <= 0.0) { + ja = 3.0 + 0.1 * elev[0]; + jb = np - ja + 6; + for (i = ja - 1; i < jb; ++i) + zsys += elev[i]; + zsys /= (jb - ja + 1); + q = eno; + } + propv.mdvar = 13; + qlrps(frq_mhz, zsys, q, pol, eps_dielect, sgm_conductivity, prop); + qlrpfl(elev, propv.klim, propv.mdvar, prop, propa, propv); + fs = 32.45 + 20.0 * log10(frq_mhz) + 20.0 * log10(prop.dist / 1000.0); + deltaH = prop.dh; + q = prop.dist - propa.dla; + if (int(q) < 0.0) + propmode = 0; // Line-Of-Sight Mode + else { + if (int(q) == 0.0) + propmode = 4; // Single Horizon + else if (int(q) > 0.0) + propmode = 8; // Double Horizon + if (prop.dist <= propa.dlsa || prop.dist <= propa.dx) + propmode += 1; // Diffraction Dominant + else if (prop.dist > propa.dx) + propmode += 2; // Troposcatter Dominant + } + struct avar_statc asc; + dbloss = avar(ztime, zloc, zconf, prop, propv, asc) + fs; // avar(time,location,confidence) + errnum = prop.kwx; +} + +void point_to_pointDH(double elev[], + double tht_m, + double rht_m, + double eps_dielect, + double sgm_conductivity, + double eno_ns_surfref, + double frq_mhz, + int radio_climate, + int pol, + double conf, + double rel, + double &dbloss, + double &deltaH, + int &errnum) +// pol: 0-Horizontal, 1-Vertical +// radio_climate: 1-Equatorial, 2-Continental Subtropical, 3-Maritime Tropical, +// 4-Desert, 5-Continental Temperate, 6-Maritime Temperate, Over Land, +// 7-Maritime Temperate, Over Sea +// conf, rel: .01 to .99 +// elev[]: [num points - 1], [delta dist(meters)], [height(meters) point 1], ..., [height(meters) +// point n] errnum: 0- No Error. +// 1- Warning: Some parameters are nearly out of range. +// Results should be used with caution. +// 2- Note: Default parameters have been substituted for impossible ones. +// 3- Warning: A combination of parameters is out of range. +// Results are probably invalid. +// Other- Warning: Some parameters are out of range. +// Results are probably invalid. +{ + std::string strmode; + prop_type prop; + propv_type propv; + propa_type propa; + double zsys = 0; + double zc, zr; + double eno, enso, q; + long ja, jb, i, np; + // double dkm, xkm; + double fs; + + prop.hg[0] = tht_m; + prop.hg[1] = rht_m; + propv.klim = radio_climate; + prop.kwx = 0; + propv.lvar = 5; + prop.mdp = -1; + zc = qerfi(conf); + zr = qerfi(rel); + np = (long)elev[0]; + // dkm = (elev[1] * elev[0]) / 1000.0; + // xkm = elev[1] / 1000.0; + eno = eno_ns_surfref; + enso = 0.0; + q = enso; + if (q <= 0.0) { + ja = 3.0 + 0.1 * elev[0]; + jb = np - ja + 6; + for (i = ja - 1; i < jb; ++i) + zsys += elev[i]; + zsys /= (jb - ja + 1); + q = eno; + } + propv.mdvar = 13; + qlrps(frq_mhz, zsys, q, pol, eps_dielect, sgm_conductivity, prop); + qlrpfl(elev, propv.klim, propv.mdvar, prop, propa, propv); + fs = 32.45 + 20.0 * log10(frq_mhz) + 20.0 * log10(prop.dist / 1000.0); + deltaH = prop.dh; + q = prop.dist - propa.dla; + if (int(q) < 0.0) + strmode = "Line-Of-Sight Mode"; + else { + if (int(q) == 0.0) + strmode = "Single Horizon"; + else if (int(q) > 0.0) + strmode = "Double Horizon"; + if (prop.dist <= propa.dlsa || prop.dist <= propa.dx) + strmode += ", Diffraction Dominant"; + else if (prop.dist > propa.dx) + strmode += ", Troposcatter Dominant"; + } + struct avar_statc asc; + dbloss = avar(zr, 0.0, zc, prop, propv, asc) + fs; // avar(time,location,confidence) + errnum = prop.kwx; +} + +//******************************************************** +//* Area Mode Calculations * +//******************************************************** + +void area(long ModVar, + double deltaH, + double tht_m, + double rht_m, + double dist_km, + int TSiteCriteria, + int RSiteCriteria, + double eps_dielect, + double sgm_conductivity, + double eno_ns_surfref, + double frq_mhz, + int radio_climate, + int pol, + double pctTime, + double pctLoc, + double pctConf, + double &dbloss, + std::string &strmode, + int &errnum) +{ + // pol: 0-Horizontal, 1-Vertical + // TSiteCriteria, RSiteCriteria: + // 0 - random, 1 - careful, 2 - very careful + // radio_climate: 1-Equatorial, 2-Continental Subtropical, 3-Maritime Tropical, + // 4-Desert, 5-Continental Temperate, 6-Maritime Temperate, Over Land, + // 7-Maritime Temperate, Over Sea + // ModVar: 0 - Single: pctConf is "Time/Situation/Location", pctTime, pctLoc not used + // 1 - Individual: pctTime is "Situation/Location", pctConf is "Confidence", pctLoc + // not used 2 - Mobile: pctTime is "Time/Locations (Reliability)", pctConf is + // "Confidence", pctLoc not used 3 - Broadcast: pctTime is "Time", pctLoc is + // "Location", pctConf is "Confidence" + // pctTime, pctLoc, pctConf: .01 to .99 + // errnum: 0- No Error. + // 1- Warning: Some parameters are nearly out of range. + // Results should be used with caution. + // 2- Note: Default parameters have been substituted for impossible ones. + // 3- Warning: A combination of parameters is out of range. + // Results are probably invalid. + // Other- Warning: Some parameters are out of range. + // Results are probably invalid. + // NOTE: strmode is not used at this time. + prop_type prop; + propv_type propv; + propa_type propa; + double zt, zl, zc, xlb; + double fs; + long ivar; + double eps, eno, sgm; + long ipol; + int kst[2]; + + kst[0] = (int)TSiteCriteria; + kst[1] = (int)RSiteCriteria; + zt = qerfi(pctTime); + zl = qerfi(pctLoc); + zc = qerfi(pctConf); + eps = eps_dielect; + sgm = sgm_conductivity; + eno = eno_ns_surfref; + prop.dh = deltaH; + prop.hg[0] = tht_m; + prop.hg[1] = rht_m; + propv.klim = (int)radio_climate; + prop.ens = eno; + prop.kwx = 0; + ivar = (long)ModVar; + ipol = (long)pol; + qlrps(frq_mhz, 0.0, eno, ipol, eps, sgm, prop); + qlra(kst, propv.klim, ivar, prop, propv); + if (propv.lvar < 1) + propv.lvar = 1; + struct lrprop_statc lrc; + lrprop(dist_km * 1000.0, prop, propa, lrc); + fs = 32.45 + 20.0 * log10(frq_mhz) + 20.0 * log10(prop.dist / 1000.0); + struct avar_statc asc; + xlb = fs + avar(zt, zl, zc, prop, propv, asc); + dbloss = xlb; + if (prop.kwx == 0) + errnum = 0; + else + errnum = prop.kwx; +} + +double ITMAreadBLoss(long ModVar, + double deltaH, + double tht_m, + double rht_m, + double dist_km, + int TSiteCriteria, + int RSiteCriteria, + double eps_dielect, + double sgm_conductivity, + double eno_ns_surfref, + double frq_mhz, + int radio_climate, + int pol, + double pctTime, + double pctLoc, + double pctConf) +{ + std::string strmode; + int errnum; + double dbloss; + area(ModVar, + deltaH, + tht_m, + rht_m, + dist_km, + TSiteCriteria, + RSiteCriteria, + eps_dielect, + sgm_conductivity, + eno_ns_surfref, + frq_mhz, + radio_climate, + pol, + pctTime, + pctLoc, + pctConf, + dbloss, + strmode, + errnum); + return dbloss; +} + +double ITMDLLVersion() +{ + return 7.0; +} + +// double ElevationAngle2(struct site source, struct site destination, double er) +//{ +// /* This function returns the angle of elevation (in degrees) +// of the destination as seen from the source location, UNLESS +// the path between the sites is obstructed, in which case, the +// elevation angle to the first obstruction is returned instead. +// "er" represents the earth radius. */ + +// int x; +// char block=0; +// double source_alt, destination_alt, cos_xmtr_angle, +// cos_test_angle, test_alt, elevation, distance, +// source_alt2, first_obstruction_angle = 0.0; +// struct path temp; + +// temp=path; + +// ReadPath(source,destination); + +// distance=5280.0*Distance(source,destination); +// source_alt=er+source.alt+GetElevation(source); +// destination_alt=er+destination.alt+GetElevation(destination); +// source_alt2=source_alt*source_alt; + +// /* Calculate the cosine of the elevation angle of the +// destination (receiver) as seen by the source (transmitter). */ + +// cos_xmtr_angle=((source_alt2)+(distance*distance)-(destination_alt*destination_alt))/(2.0*source_alt*distance); + +// /* Test all points in between source and destination locations to +// see if the angle to a topographic feature generates a higher +// elevation angle than that produced by the destination. Begin +// at the source since we're interested in identifying the FIRST +// obstruction along the path between source and destination. */ + +// for (x=2, block=0; x=cos_test_angle) +// { +// block=1; +// first_obstruction_angle=((acos(cos_test_angle))/DEG2RAD)-90.0; +// } +// } + +// if (block) +// elevation=first_obstruction_angle; + +// else +// elevation=((acos(cos_xmtr_angle))/DEG2RAD)-90.0; + +// path=temp; + +// return elevation; +//} diff --git a/src/afc-engine/ITMDLL.h b/src/afc-engine/ITMDLL.h new file mode 100644 index 0000000..9cec9b4 --- /dev/null +++ b/src/afc-engine/ITMDLL.h @@ -0,0 +1,25 @@ +// ************************************* +// C++ routines for this program are taken from +// a translation of the FORTRAN code written by +// U.S. Department of Commerce NTIA/ITS +// Institute for Telecommunication Sciences +// ***************** +// Irregular Terrain Model (ITM) (Longley-Rice) +// ************************************* + +double ITMAreadBLoss(long ModVar, + double deltaH, + double tht_m, + double rht_m, + double dist_km, + int TSiteCriteria, + int RSiteCriteria, + double eps_dielect, + double sgm_conductivity, + double eno_ns_surfref, + double frq_mhz, + int radio_climate, + int pol, + double pctTime, + double pctLoc, + double pctConf); diff --git a/src/afc-engine/LruValueCache.h b/src/afc-engine/LruValueCache.h new file mode 100644 index 0000000..50a0912 --- /dev/null +++ b/src/afc-engine/LruValueCache.h @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2022 Broadcom. All rights reserved. + * The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate + * that owns the software below. + * This work is licensed under the OpenAFC Project License, a copy of which is + * included with this software program. + */ + +#ifndef LRU_VALUE_CACHE_H +#define LRU_VALUE_CACHE_H + +#include +#include + +/** LRU Cache with explicit tracking of recent key and value. + * Implementation is mostly derived from boost::lru_cache, but all + * post-insertion copying is eliminated. This implementation is not good for + * storing C-style pointers (as null-pointer is indistinguishable from + * cache miss) + */ +template +class LruValueCache +{ + private: + ////////////////////////////////////////////////// + // LruValueCache. Private types + ////////////////////////////////////////////////// + typedef std::list ListType; + typedef std::map> MapType; + + public: + ////////////////////////////////////////////////// + // LruValueCache. Public instance methods + ////////////////////////////////////////////////// + + /** Constructor. + * @param capacity Maximum number of elements in cache + */ + LruValueCache(size_t capacity) : + _capacity(capacity), + _recentKey(nullptr), + _recentValue(nullptr), + _hits(0), + _misses(0), + _evictions(0) + { + } + + /** Add key/value to cache. + * @param key Key being added. If already there - value is replaced + * @param value Value being added + * @return Address of value in cache + */ + V *add(const K &key, const V &value) + { + typename MapType::iterator i = _map.find(key); + if (i == _map.end()) { + // Key not in cache + if (_map.size() >= _capacity) { + // Evicting most recent + typename ListType::iterator oldestKeyIter = --_list.end(); + _map.erase(*oldestKeyIter); + _list.erase(oldestKeyIter); + ++_evictions; + } + _list.push_front(key); + i = _map.emplace(std::piecewise_construct, + std::forward_as_tuple(key), + std::forward_as_tuple(value, _list.begin())) + .first; + } else { + // Key in cache + _list.splice(_list.begin(), _list, i->second.second); + i->second.first = value; + } + _recentKey = &(_list.front()); + return _recentValue = &(i->second.first); + } + + /** Cache lookup + * @param key Key to look up for + * @return Address of found value, nullptr if not found + */ + V *get(const K &key) + { + typename MapType::iterator i = _map.find(key); + if (i == _map.end()) { + ++_misses; + return nullptr; + } + ++_hits; + _list.splice(_list.begin(), _list, i->second.second); + _recentKey = &(_list.front()); + return _recentValue = &(i->second.first); + } + + /** Clear cache */ + void clear() + { + _map.clear(); + _list.clear(); + _recentKey = nullptr; + _recentValue = nullptr; + } + + /** Get recently accessed key (nullptr if there were no accesses) */ + const K *recentKey() const + { + return _recentKey; + } + + /** Get recently accessed value (nullptr if there were no accesses). + * Const version + */ + const V *recentValue() const + { + return _recentValue; + } + + /** Get recently accessed value (nullptr if there were no accesses). + * Nonconst version + */ + V *recentValue() + { + return _recentValue; + } + + /** Number of evictions */ + int ecictions() const + { + return _evictions; + } + + /** Number of search hits */ + int hits() const + { + return _hits; + } + + /** Number of search misses */ + int misses() const + { + return _misses; + } + + private: + ////////////////////////////////////////////////// + // LruValueCache. Private instance data + ////////////////////////////////////////////////// + + size_t _capacity; /*!< Maximum number of elements in cache */ + ListType _list; /*!< List of keys - most recent first */ + MapType _map; /*!< Map of values and pointers to key list */ + K *_recentKey; /*!< Recent key value */ + V *_recentValue; /*!< Pointer to recent value or nullptr */ + int _hits; /*!< Number of cache hits */ + int _misses; /*!< Number of cache misses */ + int _evictions; /*!< Number of cache evictions */ +}; + +#endif /* LRU_VALUE_CACHE_H */ diff --git a/src/afc-engine/MathConstants.cpp b/src/afc-engine/MathConstants.cpp new file mode 100644 index 0000000..2b283a8 --- /dev/null +++ b/src/afc-engine/MathConstants.cpp @@ -0,0 +1,21 @@ +/* + * Useful constants for various purposes. All static and public so they can be seen from + * everywhere. + */ + +#include "MathConstants.h" + +const double MathConstants::GeostationaryOrbitHeight = 35786.094; // altitude above sea-level (km) +const double MathConstants::EarthMaxRadius = 6378.137; // km, from WGS '84 +const double MathConstants::EarthMinRadius = 6356.760; // km +const double MathConstants::GeostationaryOrbitRadius = + MathConstants::EarthMaxRadius + + MathConstants::GeostationaryOrbitHeight; // from earth center (km) + +const double MathConstants::WGS84EarthSemiMajorAxis = 6378.137; // km +const double MathConstants::WGS84EarthSemiMinorAxis = 6356.7523142; // km +const double MathConstants::WGS84EarthFirstEccentricitySquared = 6.69437999014e-3; // unitless +const double MathConstants::WGS84EarthSecondEccentricitySquared = 6.73949674228e-3; // unitless + +const double MathConstants::speedOfLight = 2997924580.0; +const double MathConstants::boltzmannConstant = 1.3806488e-23; diff --git a/src/afc-engine/MathConstants.h b/src/afc-engine/MathConstants.h new file mode 100644 index 0000000..ab2e1c9 --- /dev/null +++ b/src/afc-engine/MathConstants.h @@ -0,0 +1,25 @@ +#ifndef MATH_CONSTANTS_H +#define MATH_CONSTANTS_H + +// See MathConstants.cpp for implementation details, values and units. + +class MathConstants +{ + public: + static const double GeostationaryOrbitHeight; + static const double GeostationaryOrbitRadius; + static const double EarthMaxRadius, EarthMinRadius; + static const double EarthEccentricitySquared; + + // WGS'84 constants. + static const double WGS84EarthSemiMajorAxis; + static const double WGS84EarthSemiMinorAxis; + static const double WGS84EarthFirstEccentricitySquared; + static const double WGS84EarthSecondEccentricitySquared; + + // Physics constants + static const double speedOfLight; + static const double boltzmannConstant; +}; + +#endif diff --git a/src/afc-engine/MathHelpers.cpp b/src/afc-engine/MathHelpers.cpp new file mode 100644 index 0000000..bfcd7e9 --- /dev/null +++ b/src/afc-engine/MathHelpers.cpp @@ -0,0 +1,31 @@ + +#include +#include "MathHelpers.h" + +double MathHelpers::tile(double size, double value) +{ + // This is the number of (positive or negative) wraps occurring + const int over = std::floor(value / size); + // Remove the number of wraps + return value - size * over; +} + +double MathHelpers::clamp(double size, double value) +{ + return std::max(0.0, std::min(size, value)); +} + +double MathHelpers::mirror(double size, double value) +{ + // This is the number of (positive or negative) wraps occurring + const int over = std::floor(value / size); + // Even wraps simply tile + if (over % 2 == 0) { + // Remove the number of wraps + return value - size * over; + } + // Odd wraps tile with inversion + else { + return size * over - value; + } +} diff --git a/src/afc-engine/MathHelpers.h b/src/afc-engine/MathHelpers.h new file mode 100644 index 0000000..46fb204 --- /dev/null +++ b/src/afc-engine/MathHelpers.h @@ -0,0 +1,213 @@ + +#ifndef MATH_HELPERS_H +#define MATH_HELPERS_H + +#include +#include +#include + +namespace MathHelpers +{ + +/** Convert an angle from degrees to radians. + * @param deg The angle in degrees + * @return The angle in radians. + */ +template +inline T deg2rad(T deg) +{ + return M_PI / 180.0 * deg; +} +/** Convert an angle from radians to degrees. + * @param rad The angle in radians + * @return The angle in degrees. + */ +template +inline T rad2deg(T rad) +{ + return 180.0 / M_PI * rad; +} + +/** Shortcut for computing squares. + * @param val The value to square. + * @return The square of @c val. + */ +template +inline T sqr(T val) +{ + return val * val; +} + +/** Shortcut for computing cubes. + * @param val The value to cube. + * @return The cube of @c val. + */ +template +inline T cube(T val) +{ + return val * val * val; +} + +/** Shortcut for computing sinc. + * @param x The value to compute sinc(x) = sin(pi * x) / (pi * x). + * @return The sinc of @c x. + */ +template +inline T sinc(T x) +{ + static const double eps = 1e-6; + if (x < eps && x > -eps) + return 1.0 - sqr(M_PI * x) / 6.0; + else + return sin(M_PI * x) / (M_PI * x); +} + +/** One-dimensional linear interpolation. + * @param val1 The zero-scale value. + * @param val2 The unit-scale value. + * @param t The scale factor. + * @return The effective value (t * val1) + ((1-t) * val2) + */ +inline double interp1d(double val1, double val2, double t) +{ + return val1 + t * (val2 - val1); +} + +/** Wrap a value to a particular size by tiling the object space onto + * the image space. + * @param size The exclusive maximum limit. + * @param value The value to be limited. + * @return The value limited to the range [0, @a size) by tiling. + */ +double tile(double size, double value); + +/** Wrap a value to a particular size by clamping the object space to the + * edge of the image space. + * @param size The exclusive maximum limit. + * @param value The value to be limited. + * @return The value limited to the range [0, @a size] by clamping. + */ +double clamp(double size, double value); + +/** Wrap a value to a particular size by mirroring the object space onto + * the image space. + * @param size The exclusive maximum limit. + * @param value The value to be limited. + * @return The value limited to the range [0, @a size) by mirroring. + */ +double mirror(double size, double value); + +/** Prepare a sample point for interpolating. + * This stores a set of two (low/high) integer-points corresponding to + * a single sample point, and a scale factor between them. + */ +struct Align { + /** Compute the grid-aligned points and scale. + * + * @param value The value to compute for. + */ + Align(const double value) + { + p1 = std::floor(value); + p2 = std::ceil(value); + factor = value - p1; + } + + /// First integer-point lower than the value + double p1; + /// First integer-point higher than the value + double p2; + /** Inverse weight factor to use for #p1 point. + * Value has range [0, 1] where 0 means p1 == value, 1 means p2 == value. + */ + double factor; +}; + +// helper class to calculate statistics of a continuously sampled one dimensional process +// without storage of the invidual samples +template +class RunningStatistic +{ + public: + RunningStatistic() + { + _count = 0; + _sum = 0.0; + _sumOfSquares = 0.0; + _max = 0.0; + _min = std::numeric_limits::max(); + } + + inline RunningStatistic &operator<<(T sample) + { + if (_count == 0) + _max = _min = sample; + + _count++; + _sum += sample; + _sumOfSquares += sqr(sample); + _max = std::max(_max, sample); + _min = std::min(_min, sample); + return *this; + } + + inline RunningStatistic &operator<<(const RunningStatistic &statistic) + { + _count += statistic._count; + _sum += statistic._sum; + _sumOfSquares += statistic._sumOfSquares; + _max = std::max(_max, statistic._max); + _min = std::min(_min, statistic._min); + return *this; + } + + inline int count() const + { + return _count; + } + + inline T mean() const + { + if (_count == 0) + return 0.0; + + return _sum / T(_count); + } + + inline T min() const + { + if (_count == 0) + return 0.0; + + return _min; + } + + inline T max() const + { + return _max; + } + + inline T variance(bool unbiased = true) const + { + if (_count < 1) + return 0.0; + + T u = mean(); + if (unbiased) + return _sumOfSquares / T(_count - 1) - + T(_count) / T(_count - 1) * sqr(u); + else + return _sumOfSquares / T(_count) - sqr(u); + } + + private: + int _count; + T _sum; + T _sumOfSquares; + T _min; + T _max; +}; + +} // end namespace MathHelpers + +#endif /* MATH_HELPERS_H */ diff --git a/src/afc-engine/MultiGeometryIterable.h b/src/afc-engine/MultiGeometryIterable.h new file mode 100644 index 0000000..34b2472 --- /dev/null +++ b/src/afc-engine/MultiGeometryIterable.h @@ -0,0 +1,128 @@ +// +#ifndef MULTI_GEOMETRY_ITERABLE_H +#define MULTI_GEOMETRY_ITERABLE_H + +#include +#include +#include + +/** Wrap an OGRMultiPoint object and allow STL-style iteration. + */ +template +class MultiGeometryIterable +{ + public: + /** Create a new iterable wrapper. + * + * @param geom The multi-geometry to iterate over. + */ + MultiGeometryIterable(Container &geom) : _geom(&geom) + { + } + + /// Shared iterator implementation + template + class base_iterator + { + public: + /** Define a new iterator. + * + * @param geom The geometry to access. + * @param index The point index to access. + */ + base_iterator(Container &geom, int index) : _geom(&geom), _ix(index) + { + } + + /** Dereference to a single item pointer. + * + * @return The item at this iterator. + */ + Contained *operator*() const + { + auto *sub = _geom->getGeometryRef(_ix); + if (!sub) { + throw std::logic_error("MultiGeometryIterable null " + "dereference"); + } + return static_cast(sub); + } + + /** Determine if two iterators are identical. + * + * @param other The iterator to compare against. + * @return True if the other iterator is the same. + */ + bool operator==(const base_iterator &other) const + { + return ((_geom == other._geom) && (_ix == other._ix)); + } + + /** Determine if two iterators are identical. + * + * @param other The iterator to compare against. + * @return True if the other iterator is different. + */ + bool operator!=(const base_iterator &other) const + { + return !operator==(other); + } + + /** Pre-increment operator. + * + * @return A reference to this incremented object. + */ + base_iterator &operator++() + { + ++_ix; + return *this; + } + + private: + /// External geometry being accessed + Container *_geom; + /// Geometry index + int _ix; + }; + + /// Iterator for mutable containers + typedef base_iterator iterator; + /// Iterator for immutable containers + typedef base_iterator const_iterator; + + iterator begin() + { + return iterator(*_geom, 0); + } + iterator end() + { + return iterator(*_geom, _geom->getNumGeometries()); + } + + private: + /// Reference to the externally-owned object + Container *_geom; +}; + +template +class MultiGeometryIterableConst : + public MultiGeometryIterable +{ + public: + MultiGeometryIterableConst(const OGRGeometryCollection &geom) : + MultiGeometryIterable(geom) + { + } +}; + +template +class MultiGeometryIterableMutable : public MultiGeometryIterable +{ + public: + MultiGeometryIterableMutable(OGRGeometryCollection &geom) : + MultiGeometryIterable(geom) + { + } +}; + +#endif /* MULTI_GEOMETRY_ITERABLE_H */ diff --git a/src/afc-engine/PopulationDatabase.cpp b/src/afc-engine/PopulationDatabase.cpp new file mode 100644 index 0000000..e302dc2 --- /dev/null +++ b/src/afc-engine/PopulationDatabase.cpp @@ -0,0 +1,96 @@ +#include "PopulationDatabase.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "afclogging/ErrStream.h" + +namespace +{ +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "PopulationDatabase") + +} // end namespace + +// Loads population data from an sql database that is stored in a vector +void PopulationDatabase::loadPopulationData(const QString &dbName, + std::vector &target, + const double &minLat, + const double &maxLat, + const double &minLon, + const double &maxLon) +{ + LOGGER_DEBUG(logger) << "Bounds: " << minLat << ", " << maxLat << "; " << minLon << ", " + << maxLon; + + // create and open db connection + SqlConnectionDefinition config; + config.driverName = "QSQLITE"; + config.dbName = dbName; + + LOGGER_INFO(logger) << "Opening database: " << dbName; + SqlScopedConnection db(new SqlExceptionDb(config.newConnection())); + db->tryOpen(); + + LOGGER_DEBUG(logger) << "Querying population database"; + auto populationQueryRes = SqlSelect(*db, "population") + .cols(QStringList() << "latitude" + << "longitude" + << "density") + .whereBetween("latitude", + std::min(minLat, maxLat), + std::max(minLat, maxLat)) + .whereBetween("longitude", + std::min(minLon, maxLon), + std::max(minLon, maxLon)) + .run(); + + LOGGER_DEBUG(logger) << "Is Active: " << populationQueryRes.isActive(); + LOGGER_DEBUG(logger) << "Is Select: " << populationQueryRes.isSelect(); + if (!populationQueryRes.isActive()) { + // Query encountered error + QSqlError err = populationQueryRes.lastError(); + throw std::runtime_error(ErrStream() << "PopulationDatabase.cpp: Database query " + "failed with code " + << err.type() << " " << err.text()); + } + + // resize vector to fit result + if (populationQueryRes.driver()->hasFeature(QSqlDriver::QuerySize)) { + // if the driver supports .size() then use it because is is more efficient + LOGGER_DEBUG(logger) << target.size() << " to " << populationQueryRes.size(); + target.resize(populationQueryRes.size()); + populationQueryRes.setForwardOnly(true); + } else { + if (!populationQueryRes.last()) { + throw std::runtime_error(ErrStream() + << "PopulationDatabase.cpp: Failed to get last " + "item. Check that lat/lon are within CONUS : " + << populationQueryRes.at()); + } + LOGGER_DEBUG(logger) << target.size() << " to " << populationQueryRes.at(); + target.resize(populationQueryRes.at() + 1); + populationQueryRes.first(); + populationQueryRes.previous(); + } + + while (populationQueryRes.next()) { + int r = populationQueryRes.at(); + target.at(r).latitude = populationQueryRes.value(0).toDouble(); + target.at(r).longitude = populationQueryRes.value(1).toDouble(); + target.at(r).density = populationQueryRes.value(2).toDouble(); + } + + LOGGER_DEBUG(logger) << target.size() << " rows retreived"; +} diff --git a/src/afc-engine/PopulationDatabase.h b/src/afc-engine/PopulationDatabase.h new file mode 100644 index 0000000..94b4f1a --- /dev/null +++ b/src/afc-engine/PopulationDatabase.h @@ -0,0 +1,29 @@ +// PopulationDatabse.h: header file for population database reading on program startup +// author: Sam Smucny + +#ifndef AFCENGINE_POP_DATABASE_H_ +#define AFCENGINE_POP_DATABASE_H_ + +#include +#include + +struct PopulationRecord { + double latitude; + double longitude; + double density; +}; + +class PopulationDatabase +{ + public: + // Loads population database from file path dbName into the target vector. Uses + // bounds to restrict the sections loaded + static void loadPopulationData(const QString &dbName, + std::vector &target, + const double &minLat = -90, + const double &maxLat = 90, + const double &minLon = -180, + const double &maxLon = 180); +}; + +#endif /* AFCENGINE_POP_DATABASE_H_ */ diff --git a/src/afc-engine/RlanRegion.cpp b/src/afc-engine/RlanRegion.cpp new file mode 100644 index 0000000..958910f --- /dev/null +++ b/src/afc-engine/RlanRegion.cpp @@ -0,0 +1,1369 @@ +/******************************************************************************************/ +/**** FILE: RlanRegion.cpp ****/ +/******************************************************************************************/ + +#include "RlanRegion.h" +#include "terrain.h" +#include "EcefModel.h" +#include "global_defines.h" + +double RlanRegionClass::minRlanHeightAboveTerrain; + +/******************************************************************************************/ +/**** CONSTRUCTOR: RlanRegionClass::RlanRegionClass() ****/ +/******************************************************************************************/ +RlanRegionClass::RlanRegionClass() +{ + centerLongitude = quietNaN; + centerLatitude = quietNaN; + cosVal = quietNaN; + oneOverCosVal = quietNaN; + centerHeightInput = quietNaN; + centerHeightAMSL = quietNaN; + centerTerrainHeight = quietNaN; + minTerrainHeight = quietNaN; + maxTerrainHeight = quietNaN; + heightUncertainty = quietNaN; + + fixedHeightAMSL = false; + configuredFlag = false; + boundaryPolygon = (PolygonClass *)NULL; + + polygonResolution = 1.0e-6; // 0.11 meter +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** DESTRUCTOR: RlanRegionClass::~RlanRegionClass() ****/ +/******************************************************************************************/ +RlanRegionClass::~RlanRegionClass() +{ + if (boundaryPolygon) { + delete boundaryPolygon; + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: RlanRegionClass::getMinHeightAGL() ****/ +/******************************************************************************************/ +double RlanRegionClass::getMinHeightAGL() const +{ + if (!configuredFlag) { + throw std::runtime_error(ErrStream() << "ERROR: RlanRegionClass::getMinHeightAGL() " + "RlanRegion not configured"); + } + + double minHeightAGL; + if (fixedHeightAMSL) { + minHeightAGL = centerHeightAMSL - heightUncertainty - maxTerrainHeight; + } else { + minHeightAGL = centerHeightAMSL - heightUncertainty - centerTerrainHeight; + } + + if (minHeightAGL < minRlanHeightAboveTerrain) { + minHeightAGL = minRlanHeightAboveTerrain; + } + + return (minHeightAGL); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: RlanRegionClass::getMaxHeightAGL() ****/ +/******************************************************************************************/ +double RlanRegionClass::getMaxHeightAGL() const +{ + if (!configuredFlag) { + throw std::runtime_error(ErrStream() << "ERROR: RlanRegionClass::getMaxHeightAGL() " + "RlanRegion not configured"); + } + + double maxHeightAGL; + if (fixedHeightAMSL) { + maxHeightAGL = centerHeightAMSL + heightUncertainty - minTerrainHeight; + } else { + maxHeightAGL = centerHeightAMSL + heightUncertainty - centerTerrainHeight; + } + + if (maxHeightAGL < minRlanHeightAboveTerrain) { + maxHeightAGL = minRlanHeightAboveTerrain; + } + + return (maxHeightAGL); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: RlanRegionClass::getMinHeightAMSL() ****/ +/******************************************************************************************/ +double RlanRegionClass::getMinHeightAMSL() const +{ + if (!configuredFlag) { + throw std::runtime_error(ErrStream() + << "ERROR: RlanRegionClass::getMinHeightAMSL() RlanRegion " + "not configured"); + } + + double minHeightAMSL; + if (fixedHeightAMSL) { + minHeightAMSL = centerHeightAMSL - heightUncertainty; + } else { + minHeightAMSL = centerHeightAMSL - heightUncertainty - centerTerrainHeight + + minTerrainHeight; + } + + if (minHeightAMSL < minTerrainHeight + minRlanHeightAboveTerrain) { + minHeightAMSL = minTerrainHeight + minRlanHeightAboveTerrain; + } + + return (minHeightAMSL); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: RlanRegionClass::getMaxHeightAMSL() ****/ +/******************************************************************************************/ +double RlanRegionClass::getMaxHeightAMSL() const +{ + if (!configuredFlag) { + throw std::runtime_error(ErrStream() + << "ERROR: RlanRegionClass::getMaxHeightAMSL() RlanRegion " + "not configured"); + } + + double maxHeightAMSL; + if (fixedHeightAMSL) { + maxHeightAMSL = centerHeightAMSL + heightUncertainty; + } else { + maxHeightAMSL = centerHeightAMSL + heightUncertainty - centerTerrainHeight + + maxTerrainHeight; + } + + if (maxHeightAMSL < maxTerrainHeight + minRlanHeightAboveTerrain) { + maxHeightAMSL = maxTerrainHeight + minRlanHeightAboveTerrain; + } + + return (maxHeightAMSL); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: RlanRegionClass::computePointing() ****/ +/******************************************************************************************/ +Vector3 RlanRegionClass::computePointing(double azimuth, double elevation) const +{ + double azimuthRad = azimuth * M_PI / 180.0; + double elevationRad = elevation * M_PI / 180.0; + + Vector3 pointing = (northVec * cos(azimuthRad) + eastVec * sin(azimuthRad)) * + cos(elevationRad) + + upVec * sin(elevationRad); + + return (pointing); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: EllipseRlanRegionClass::EllipseRlanRegionClass() ****/ +/******************************************************************************************/ +EllipseRlanRegionClass::EllipseRlanRegionClass(DoubleTriplet rlanLLA, + DoubleTriplet rlanUncerts_m, + double rlanOrientationDeg, + bool fixedHeightAMSLVal) +{ + fixedHeightAMSL = fixedHeightAMSLVal; + + std::tie(centerLatitude, centerLongitude, centerHeightInput) = rlanLLA; + std::tie(semiMinorAxis, semiMajorAxis, heightUncertainty) = rlanUncerts_m; + orientationDeg = rlanOrientationDeg; + + cosVal = cos(centerLatitude * M_PI / 180.0); + oneOverCosVal = 1.0 / cosVal; + + double rlanOrientationRad = rlanOrientationDeg * M_PI / 180.0; + + upVec = EcefModel::geodeticToEcef(centerLatitude, centerLongitude, 0.0).normalized(); + eastVec = (Vector3(-upVec.y(), upVec.x(), 0.0)).normalized(); + northVec = upVec.cross(eastVec); + + // define orthogonal unit vectors in the directions of semiMajor and semiMinor axis of + // ellipse + majorVec = cos(rlanOrientationRad) * northVec + sin(rlanOrientationRad) * eastVec; + minorVec = sin(rlanOrientationRad) * northVec - cos(rlanOrientationRad) * eastVec; + + arma::mat mxD(2, 2); + mxD(0, 0) = semiMinorAxis; + mxD(1, 1) = semiMajorAxis; + mxD(0, 1) = 0.0; + mxD(1, 0) = 0.0; + + arma::mat mxE(2, 2); + mxE(0, 0) = 1.0 / (semiMinorAxis * semiMinorAxis); + mxE(1, 1) = 1.0 / (semiMajorAxis * semiMajorAxis); + mxE(0, 1) = 0.0; + mxE(1, 0) = 0.0; + + arma::mat mxR(2, 2); + mxR(0, 0) = cos(rlanOrientationRad); + mxR(0, 1) = sin(rlanOrientationRad); + mxR(1, 0) = -sin(rlanOrientationRad); + mxR(1, 1) = cos(rlanOrientationRad); + + arma::mat mxS1(2, 2); + mxS1(0, 0) = CConst::earthRadius * M_PI / 180.0; + mxS1(0, 1) = 0.0; + mxS1(1, 0) = 0.0; + mxS1(1, 1) = CConst::earthRadius * M_PI / 180.0; + + arma::mat mxS2(2, 2); + mxS2(0, 0) = cosVal; + mxS2(0, 1) = 0.0; + mxS2(1, 0) = 0.0; + mxS2(1, 1) = 1.0; + + arma::mat mxInvS(2, 2); + mxInvS(0, 0) = 1.0 / (CConst::earthRadius * cosVal * M_PI / 180.0); + mxInvS(0, 1) = 0.0; + mxInvS(1, 0) = 0.0; + mxInvS(1, 1) = 1.0 / (CConst::earthRadius * M_PI / 180.0); + + mxC = mxS1 * mxR * mxE * (mxR.t()) * mxS1; + mxA = mxS2 * mxC * mxS2; + mxB = mxInvS * mxR * mxD; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** DESTRUCTOR: EllipseRlanRegionClass::~EllipseRlanRegionClass() ****/ +/******************************************************************************************/ +EllipseRlanRegionClass::~EllipseRlanRegionClass() +{ +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: EllipseRlanRegionClass::configure() ****/ +/******************************************************************************************/ +void EllipseRlanRegionClass::configure(CConst::HeightTypeEnum rlanHeightType, TerrainClass *terrain) +{ + double bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum rlanHeightSource; + terrain->getTerrainHeight(centerLongitude, + centerLatitude, + centerTerrainHeight, + bldgHeight, + lidarHeightResult, + rlanHeightSource); + + // LOGGER_DEBUG(logger) << "rlanHeight: " << centerTerrainHeight << ", building height: " << + // bldgHeight << ", from: " << rlanHeightSource; + + if (rlanHeightType == CConst::AMSLHeightType) { + centerHeightAMSL = centerHeightInput; + } else if (rlanHeightType == CConst::AGLHeightType) { + centerHeightAMSL = centerHeightInput + centerTerrainHeight; + } else { + throw std::runtime_error(ErrStream() + << "ERROR: INVALID rlanHeightType = " << rlanHeightType); + } + + centerPosn = EcefModel::geodeticToEcef(centerLatitude, + centerLongitude, + centerHeightAMSL / 1000.0); + + minTerrainHeight = centerTerrainHeight; + maxTerrainHeight = centerTerrainHeight; + + int scanPtIdx; + std::vector scanPtList = getScan(CConst::xyAlignRegionNorthEastScanRegionMethod, + 1.0, + -1); + for (scanPtIdx = 0; scanPtIdx < (int)scanPtList.size(); ++scanPtIdx) { + LatLon scanPt = scanPtList[scanPtIdx]; + double terrainHeight; + terrain->getTerrainHeight(scanPt.second, + scanPt.first, + terrainHeight, + bldgHeight, + lidarHeightResult, + rlanHeightSource); + + if (terrainHeight > maxTerrainHeight) { + maxTerrainHeight = terrainHeight; + } else if (terrainHeight < minTerrainHeight) { + minTerrainHeight = terrainHeight; + } + } + + configuredFlag = true; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: EllipseRlanRegionClass::calcMinAOB() ****/ +/******************************************************************************************/ +double EllipseRlanRegionClass::calcMinAOB(LatLon ulsRxLatLon, + Vector3 ulsAntennaPointing, + double ulsRxHeightAMSL, + double &minAOBLon, + double &minAOBLat, + double &minAOBHeghtAMSL) +{ + if (!boundaryPolygon) { + int numPts = 32; + + arma::vec x0(2); + arma::vec x3(2); + + double r = 1.0 / cos(M_PI / numPts); + std::vector> *ii_list = + new std::vector>(); + + for (int ptIdx = 0; ptIdx < numPts; ++ptIdx) { + x0[0] = r * cos(2 * M_PI * ptIdx / numPts); + x0[1] = r * sin(2 * M_PI * ptIdx / numPts); + + x3 = mxB * x0; + + int xval = (int)floor((x3[0] * cosVal / polygonResolution) + 0.5); + int yval = (int)floor((x3[1] / polygonResolution) + 0.5); + ii_list->push_back(std::tuple(xval, yval)); + } + + boundaryPolygon = new PolygonClass(ii_list); + + delete ii_list; + } + + arma::vec ptg(3); + ptg(0) = ulsAntennaPointing.dot(eastVec); + ptg(1) = ulsAntennaPointing.dot(northVec); + ptg(2) = ulsAntennaPointing.dot(upVec); + + arma::vec F(3); + F(0) = (ulsRxLatLon.second - centerLongitude) * cosVal; + F(1) = ulsRxLatLon.first - centerLatitude; + if (ulsRxHeightAMSL > centerHeightAMSL) { + F(2) = ulsRxHeightAMSL - getMaxHeightAMSL(); + minAOBHeghtAMSL = getMaxHeightAMSL(); + } else { + F(2) = ulsRxHeightAMSL - getMinHeightAMSL(); + minAOBHeghtAMSL = getMinHeightAMSL(); + } + F(2) *= (180.0 / M_PI) / CConst::earthRadius; + + arma::vec minLoc(2); + double minAOB = + RlanRegionClass::calcMinAOB(boundaryPolygon, polygonResolution, F, ptg, minLoc); + minAOBLon = centerLongitude + minLoc(0) * oneOverCosVal; + minAOBLat = centerLatitude + minLoc(1); + + return minAOB; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: EllipseRlanRegionClass::getType() ****/ +/******************************************************************************************/ +RLANBoundary EllipseRlanRegionClass::getType() const +{ + return ELLIPSE; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: EllipseRlanRegionClass::closestPoint() ****/ +/******************************************************************************************/ +LatLon EllipseRlanRegionClass::closestPoint(LatLon latlon, bool &contains) const +{ + double latitude = 0.0; + double longitude = 0.0; + + arma::vec P(2); + P(0) = latlon.second - centerLongitude; // longitude + P(1) = latlon.first - centerLatitude; // latitude + + double d = dot(P, mxA * P); + + if (d <= 1) { + contains = true; + } else { + contains = false; + longitude = centerLongitude + P(0) / sqrt(d); + latitude = centerLatitude + P(1) / sqrt(d); + } + + return (std::pair(latitude, longitude)); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: EllipseRlanRegionClass::getBoundary() ****/ +/******************************************************************************************/ +std::vector EllipseRlanRegionClass::getBoundary(TerrainClass *terrain) const +{ + std::vector ptList; + arma::vec P(2); + + if (!configuredFlag) { + throw std::runtime_error(ErrStream() + << "ERROR: EllipseRlanRegionClass::getBoundary() " + "RlanRegion not configured"); + } + + int ptIdx; + int numRLANPoints = 32; + + for (ptIdx = 0; ptIdx < numRLANPoints; ptIdx++) { + double phi = 2 * M_PI * ptIdx / numRLANPoints; + P(0) = cos(phi); + P(1) = sin(phi); + double d = dot(P, mxA * P); + double longitude = centerLongitude + P(0) / sqrt(d); + double latitude = centerLatitude + P(1) / sqrt(d); + + double heightAMSL; + if (fixedHeightAMSL) { + heightAMSL = centerHeightAMSL; + } else { + double terrainHeight, bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum rlanHeightSource; + terrain->getTerrainHeight(longitude, + latitude, + terrainHeight, + bldgHeight, + lidarHeightResult, + rlanHeightSource); + heightAMSL = terrainHeight + centerHeightAMSL - centerTerrainHeight; + } + GeodeticCoord rlanEllipsePtGeo = GeodeticCoord::fromLatLon(latitude, + longitude, + heightAMSL / 1000.0); + ptList.push_back(rlanEllipsePtGeo); + } + + return (ptList); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: EllipseRlanRegionClass::getScan() ****/ +/******************************************************************************************/ +std::vector EllipseRlanRegionClass::getScan(CConst::ScanRegionMethodEnum method, + double scanResolutionM, + int pointsPerDegree) +{ + std::vector ptList; + + if (method == CConst::xyAlignRegionNorthEastScanRegionMethod) { + // Scan points aligned with north/east + int N = floor(semiMajorAxis / scanResolutionM); + + double deltaLat = (scanResolutionM / CConst::earthRadius) * (180.0 / M_PI); + double deltaLon = deltaLat / cosVal; + + int ix, iy; + for (iy = -N; iy <= N; ++iy) { + double latitude = centerLatitude + iy * deltaLat; + for (ix = -N; ix <= N; ++ix) { + double longitude = centerLongitude + ix * deltaLon; + bool contains; + closestPoint(std::pair(latitude, longitude), + contains); + if (contains) { + ptList.push_back( + std::pair(latitude, longitude)); + } + } + } + } else if (method == CConst::xyAlignRegionMajorMinorScanRegionMethod) { + // Scan points aligned with major/minor axis + + double dx = scanResolutionM / semiMinorAxis; + double dy = scanResolutionM / semiMajorAxis; + + arma::vec X0(2), X3(2); + + int Ny = floor(1.0 / dy); + + int ix, iy, xs, ys; + for (iy = 0; iy <= Ny; ++iy) { + double yval = iy * dy; + int Nx = (int)floor(sqrt(1.0 - yval * yval) / dx); + for (ix = 0; ix <= Nx; ++ix) { + double xval = ix * dx; + X0(0) = xval; + X0(1) = yval; + X3 = mxB * X0; + + for (ys = 0; ys < (iy == 0 ? 1 : 2); ++ys) { + for (xs = 0; xs < (ix == 0 ? 1 : 2); ++xs) { + double longitude = centerLongitude + + (xs == 0 ? 1 : -1) * X3(0); + double latitude = centerLatitude + + (ys == 0 ? 1 : -1) * X3(1); + ptList.push_back( + std::pair(latitude, + longitude)); + } + } + } + } + } else if (method == CConst::latLonAlignGridScanRegionMethod) { + // Scan points aligned with lat / lon grid + int ix, iy; + + int N = floor((semiMajorAxis / CConst::earthRadius) * (180.0 / M_PI) * + pointsPerDegree / cosVal) + + 1; + int **S = (int **)malloc((2 * N + 1) * sizeof(int *)); + for (ix = 0; ix <= 2 * N; ++ix) { + S[ix] = (int *)malloc((2 * N + 1) * sizeof(int)); + for (iy = 0; iy <= 2 * N; ++iy) { + S[ix][iy] = 0; + } + } + S[N][N] = 1; + + int latN0 = (int)floor(centerLatitude * pointsPerDegree); + int lonN0 = (int)floor(centerLongitude * pointsPerDegree); + + for (iy = -N + 1; iy <= N; ++iy) { + double latVal = ((double)(latN0 + iy)) / pointsPerDegree; + bool flag; + double lonA, lonB; + calcHorizExtents(latVal, lonA, lonB, flag); + if (flag) { + int iA = ((int)floor(lonA * pointsPerDegree)) - lonN0; + int iB = ((int)floor(lonB * pointsPerDegree)) - lonN0; + for (ix = iA; ix <= iB; ++ix) { + S[N + ix][N + iy] = 1; + S[N + ix][N + iy - 1] = 1; + } + } + } + + for (ix = -N + 1; ix <= N; ++ix) { + double lonVal = ((double)(lonN0 + ix)) / pointsPerDegree; + bool flag; + double latA, latB; + calcVertExtents(lonVal, latA, latB, flag); + if (flag) { + int iA = ((int)floor(latA * pointsPerDegree)) - latN0; + int iB = ((int)floor(latB * pointsPerDegree)) - latN0; + for (iy = iA; iy <= iB; ++iy) { + S[N + ix][N + iy] = 1; + S[N + ix - 1][N + iy] = 1; + } + } + } + + for (iy = 2 * N; iy >= 0; --iy) { + for (ix = 0; ix <= 2 * N; ++ix) { + if (S[ix][iy]) { + double lonVal = (lonN0 + ix - N + 0.5) / pointsPerDegree; + double latVal = (latN0 + iy - N + 0.5) / pointsPerDegree; + ptList.push_back(std::pair(latVal, lonVal)); + } + } + } + + std::vector> *vListS = calcScanPointVirtices(S, + 2 * N + 1, + 2 * N + 1); + +#if 0 + printf("BOUNDARY POLYGON\n"); + for(iy=2*N; iy>=0; --iy) { + for(ix=0; ix<=2*N; ++ix) { + printf("%d ", S[ix][iy]); + } + printf("\n"); + } + for(int i=0; i<(int) vListS->size(); ++i) { + std::tie(ix, iy) = (*vListS)[i]; + printf("%d %d\n", ix, iy); + } +#endif + + std::vector> *ii_list = + new std::vector>(); + for (int i = 0; i < (int)vListS->size(); ++i) { + std::tie(ix, iy) = (*vListS)[i]; + double lonVal = ((double)lonN0 + ix - N) / pointsPerDegree; + double latVal = ((double)latN0 + iy - N) / pointsPerDegree; + + int xval = (int)floor( + ((lonVal - centerLongitude) * cosVal / polygonResolution) + 0.5); + int yval = (int)floor(((latVal - centerLatitude) / polygonResolution) + + 0.5); + ii_list->push_back(std::tuple(xval, yval)); + } + boundaryPolygon = new PolygonClass(ii_list); + + delete ii_list; + delete vListS; + for (ix = 0; ix <= 2 * N; ++ix) { + free(S[ix]); + } + free(S); + } else { + CORE_DUMP; + } + + return (ptList); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: EllipseRlanRegionClass::calcHorizExtents() ****/ +/******************************************************************************************/ +void EllipseRlanRegionClass::calcHorizExtents(double latVal, + double &lonA, + double &lonB, + bool &flag) const +{ + double yval = latVal - centerLatitude; + + double B = (mxA(0, 1) + mxA(1, 0)) * yval / mxA(0, 0); + double C = (mxA(1, 1) * yval * yval - 1.0) / mxA(0, 0); + + double D = B * B - 4 * C; + + if (D >= 0) { + flag = true; + double sqrtD = sqrt(D); + lonA = centerLongitude + (-B - sqrtD) / 2; + lonB = centerLongitude + (-B + sqrtD) / 2; + } else { + flag = false; + } + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: EllipseRlanRegionClass::calcVertExtents() ****/ +/******************************************************************************************/ +void EllipseRlanRegionClass::calcVertExtents(double lonVal, + double &latA, + double &latB, + bool &flag) const +{ + double xval = lonVal - centerLongitude; + + double B = (mxA(0, 1) + mxA(1, 0)) * xval / mxA(1, 1); + double C = (mxA(0, 0) * xval * xval - 1.0) / mxA(1, 1); + + double D = B * B - 4 * C; + + if (D >= 0) { + flag = true; + double sqrtD = sqrt(D); + latA = centerLatitude + (-B - sqrtD) / 2; + latB = centerLatitude + (-B + sqrtD) / 2; + } else { + flag = false; + } + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: EllipseRlanRegionClass::getMaxDist() ****/ +/******************************************************************************************/ +double EllipseRlanRegionClass::getMaxDist() const +{ + return (semiMajorAxis); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: PolygonRlanRegionClass::PolygonRlanRegionClass() ****/ +/******************************************************************************************/ +PolygonRlanRegionClass::PolygonRlanRegionClass( + DoubleTriplet rlanLLA, + DoubleTriplet rlanUncerts_m, + const std::vector> &rlanPolygon, + RLANBoundary polygonTypeVal, + bool fixedHeightAMSLVal) : + polygonType(polygonTypeVal) +{ + fixedHeightAMSL = fixedHeightAMSLVal; + + std::tie(centerLatitude, centerLongitude, centerHeightInput) = rlanLLA; + std::tie(std::ignore, std::ignore, heightUncertainty) = rlanUncerts_m; + + Vector3 centerPosnNoHeight = EcefModel::geodeticToEcef(centerLatitude, + centerLongitude, + 0.0); + + upVec = centerPosnNoHeight.normalized(); + eastVec = (Vector3(-upVec.y(), upVec.x(), 0.0)).normalized(); + northVec = upVec.cross(eastVec); + + centerLongitude = floor((centerLongitude / polygonResolution) + 0.5) * polygonResolution; + centerLatitude = floor((centerLatitude / polygonResolution) + 0.5) * polygonResolution; + cosVal = cos(centerLatitude * M_PI / 180.0); + oneOverCosVal = 1.0 / cosVal; + + std::vector> *ii_list = new std::vector>(); + for (int i = 0; i < (int)rlanPolygon.size(); ++i) { + double longitude, latitude; + + if (polygonType == LINEAR_POLY) { + latitude = rlanPolygon[i].first; + longitude = rlanPolygon[i].second; + } else if (polygonType == RADIAL_POLY) { + double angle = rlanPolygon[i].first; + double length = rlanPolygon[i].second; + + Vector3 position = centerPosnNoHeight + + (length / 1000.0) * + (northVec * cos(angle * M_PI / 180.0) + + eastVec * sin(angle * M_PI / 180.0)); + GeodeticCoord positionGeo = EcefModel::toGeodetic(position); + + longitude = positionGeo.longitudeDeg; + latitude = positionGeo.latitudeDeg; + } else { + throw std::runtime_error(ErrStream() + << "ERROR: INVALID polygonType = " << polygonType); + } + + int xval = (int)floor(((longitude - centerLongitude) * cosVal / polygonResolution) + + 0.5); + int yval = (int)floor(((latitude - centerLatitude) / polygonResolution) + 0.5); + ii_list->push_back(std::tuple(xval, yval)); + } + + polygon = new PolygonClass(ii_list); + + delete ii_list; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** DESTRUCTOR: PolygonRlanRegionClass::~PolygonRlanRegionClass() ****/ +/******************************************************************************************/ +PolygonRlanRegionClass::~PolygonRlanRegionClass() +{ + delete polygon; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: PolygonRlanRegionClass::configure() ****/ +/******************************************************************************************/ +void PolygonRlanRegionClass::configure(CConst::HeightTypeEnum rlanHeightType, TerrainClass *terrain) +{ + double bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum rlanHeightSource; + terrain->getTerrainHeight(centerLongitude, + centerLatitude, + centerTerrainHeight, + bldgHeight, + lidarHeightResult, + rlanHeightSource); + + // LOGGER_DEBUG(logger) << "rlanHeight: " << centerTerrainHeight << ", building height: " << + // bldgHeight << ", from: " << rlanHeightSource; + + if (rlanHeightType == CConst::AMSLHeightType) { + centerHeightAMSL = centerHeightInput; + } else if (rlanHeightType == CConst::AGLHeightType) { + centerHeightAMSL = centerHeightInput + centerTerrainHeight; + } else { + throw std::runtime_error(ErrStream() + << "ERROR: INVALID rlanHeightType = " << rlanHeightType); + } + + centerPosn = EcefModel::geodeticToEcef(centerLatitude, + centerLongitude, + centerHeightAMSL / 1000.0); + + minTerrainHeight = centerTerrainHeight; + maxTerrainHeight = centerTerrainHeight; + + int scanPtIdx; + std::vector scanPtList = getScan(CConst::xyAlignRegionNorthEastScanRegionMethod, + 1.0, + -1); + for (scanPtIdx = 0; scanPtIdx < (int)scanPtList.size(); ++scanPtIdx) { + LatLon scanPt = scanPtList[scanPtIdx]; + double terrainHeight; + terrain->getTerrainHeight(scanPt.second, + scanPt.first, + terrainHeight, + bldgHeight, + lidarHeightResult, + rlanHeightSource); + + if (terrainHeight > maxTerrainHeight) { + maxTerrainHeight = terrainHeight; + } else if (terrainHeight < minTerrainHeight) { + minTerrainHeight = terrainHeight; + } + } + + configuredFlag = true; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: PolygonRlanRegionClass::calcMinAOB() ****/ +/******************************************************************************************/ +double PolygonRlanRegionClass::calcMinAOB(LatLon ulsRxLatLon, + Vector3 ulsAntennaPointing, + double ulsRxHeightAMSL, + double &minAOBLon, + double &minAOBLat, + double &minAOBHeghtAMSL) +{ + arma::vec ptg(3); + ptg(0) = ulsAntennaPointing.dot(eastVec); + ptg(1) = ulsAntennaPointing.dot(northVec); + ptg(2) = ulsAntennaPointing.dot(upVec); + + arma::vec F(3); + F(0) = (ulsRxLatLon.second - centerLongitude) * cosVal; + F(1) = ulsRxLatLon.first - centerLatitude; + if (ulsRxHeightAMSL > centerHeightAMSL) { + F(2) = ulsRxHeightAMSL - getMaxHeightAMSL(); + minAOBHeghtAMSL = getMaxHeightAMSL(); + } else { + F(2) = ulsRxHeightAMSL - getMinHeightAMSL(); + minAOBHeghtAMSL = getMinHeightAMSL(); + } + F(2) *= (180.0 / M_PI) / CConst::earthRadius; + + arma::vec minLoc(2); + double minAOB = RlanRegionClass::calcMinAOB(polygon, polygonResolution, F, ptg, minLoc); + minAOBLon = centerLongitude + minLoc(0) * oneOverCosVal; + minAOBLat = centerLatitude + minLoc(1); + + return minAOB; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PolygonRlanRegionClass::getType() ****/ +/******************************************************************************************/ +RLANBoundary PolygonRlanRegionClass::getType() const +{ + return polygonType; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PolygonRlanRegionClass::closestPoint() ****/ +/******************************************************************************************/ +LatLon PolygonRlanRegionClass::closestPoint(LatLon latlon, bool &contains) const +{ + double latitude = 0.0; + double longitude = 0.0; + + int xval, yval; + xval = (int)floor((latlon.second - centerLongitude) * cosVal / polygonResolution + + 0.5); // longitude + yval = (int)floor((latlon.first - centerLatitude) / polygonResolution + 0.5); // latitude + + bool edge; + contains = polygon->in_bdy_area(xval, yval, &edge); + if (edge) { + contains = true; + } + + if (!contains) { + double ptX, ptY; + std::tie(ptX, ptY) = polygon->closestPoint(std::tuple(xval, yval)); + + longitude = centerLongitude + ptX * polygonResolution * oneOverCosVal; + latitude = centerLatitude + ptY * polygonResolution; + } + + return (std::pair(latitude, longitude)); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PolygonRlanRegionClass::getBoundary() ****/ +/******************************************************************************************/ +std::vector PolygonRlanRegionClass::getBoundary(TerrainClass *terrain) const +{ + std::vector ptList; + + if (!configuredFlag) { + throw std::runtime_error(ErrStream() + << "ERROR: PolygonRlanRegionClass::getBoundary() " + "RlanRegion not configured"); + } + + int ptIdx; + int numRLANPoints = polygon->num_bdy_pt[0]; + + for (ptIdx = 0; ptIdx < numRLANPoints; ptIdx++) { + int xval = polygon->bdy_pt_x[0][ptIdx]; + int yval = polygon->bdy_pt_y[0][ptIdx]; + double longitude = centerLongitude + xval * polygonResolution * oneOverCosVal; + double latitude = centerLatitude + yval * polygonResolution; + + double heightAMSL; + if (fixedHeightAMSL) { + heightAMSL = centerHeightAMSL; + } else { + double terrainHeight, bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum rlanHeightSource; + terrain->getTerrainHeight(longitude, + latitude, + terrainHeight, + bldgHeight, + lidarHeightResult, + rlanHeightSource); + heightAMSL = terrainHeight + centerHeightAMSL - centerTerrainHeight; + } + GeodeticCoord rlanEllipsePtGeo = GeodeticCoord::fromLatLon(latitude, + longitude, + heightAMSL / 1000.0); + ptList.push_back(rlanEllipsePtGeo); + } + + return (ptList); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PolygonRlanRegionClass::getScan() ****/ +/******************************************************************************************/ +std::vector PolygonRlanRegionClass::getScan(CConst::ScanRegionMethodEnum method, + double scanResolutionM, + int pointsPerDegree) +{ + std::vector ptList; + + int minx, maxx, miny, maxy; + polygon->comp_bdy_min_max(minx, maxx, miny, maxy); + + if ((method == CConst::xyAlignRegionNorthEastScanRegionMethod) || + (method == CConst::xyAlignRegionMajorMinorScanRegionMethod)) { + int minScanXIdx = (int)floor(minx * polygonResolution * (M_PI / 180.0) * + CConst::earthRadius / scanResolutionM); + int maxScanXIdx = (int)floor(maxx * polygonResolution * (M_PI / 180.0) * + CConst::earthRadius / scanResolutionM) + + 1; + int minScanYIdx = (int)floor(miny * polygonResolution * (M_PI / 180.0) * + CConst::earthRadius / scanResolutionM); + int maxScanYIdx = (int)floor(maxy * polygonResolution * (M_PI / 180.0) * + CConst::earthRadius / scanResolutionM) + + 1; + + int ix, iy; + bool isEdge; + for (iy = minScanYIdx; iy <= maxScanYIdx; ++iy) { + int yIdx = (int)floor(iy * scanResolutionM * (180.0 / M_PI) / + (CConst::earthRadius * polygonResolution) + + 0.5); + for (ix = minScanXIdx; ix <= maxScanXIdx; ++ix) { + int xIdx = (int)floor( + ix * scanResolutionM * (180.0 / M_PI) / + (CConst::earthRadius * polygonResolution) + + 0.5); + bool inBdyArea = polygon->in_bdy_area(xIdx, yIdx, &isEdge); + if (inBdyArea || isEdge) { + double longitude = centerLongitude + + xIdx * polygonResolution * oneOverCosVal; + double latitude = centerLatitude + yIdx * polygonResolution; + ptList.push_back( + std::pair(latitude, longitude)); + } + } + } + } else if (method == CConst::latLonAlignGridScanRegionMethod) { + // Scan points aligned with lat / lon grid + int ix, iy; + + int NX = (int)floor((maxx - minx) * polygonResolution * oneOverCosVal * + pointsPerDegree) + + 2; + int NY = (int)floor((maxy - miny) * polygonResolution * pointsPerDegree) + 2; + int **S = (int **)malloc((NX) * sizeof(int *)); + for (ix = 0; ix < NX; ++ix) { + S[ix] = (int *)malloc((NY) * sizeof(int)); + for (iy = 0; iy < NY; ++iy) { + S[ix][iy] = 0; + } + } + + int latN0 = (int)floor((centerLatitude + miny * polygonResolution) * + pointsPerDegree); + int lonN0 = (int)floor( + (centerLongitude + minx * polygonResolution * oneOverCosVal) * + pointsPerDegree); + + for (iy = 1; iy < NY; ++iy) { + double latVal = ((double)(latN0 + iy)) / pointsPerDegree; + double yVal = (latVal - centerLatitude) / polygonResolution; + bool flag; + double xA, xB; + polygon->calcHorizExtents(yVal, xA, xB, flag); + if (flag) { + double lonA = centerLongitude + + xA * polygonResolution * oneOverCosVal; + double lonB = centerLongitude + + xB * polygonResolution * oneOverCosVal; + int iA = ((int)floor(lonA * pointsPerDegree)) - lonN0; + int iB = ((int)floor(lonB * pointsPerDegree)) - lonN0; + for (ix = iA; ix <= iB; ++ix) { + S[ix][iy] = 1; + S[ix][iy - 1] = 1; + } + } + } + + for (ix = 1; ix < NX; ++ix) { + double lonVal = ((double)(lonN0 + ix)) / pointsPerDegree; + double xVal = (lonVal - centerLongitude) / polygonResolution; + bool flag; + double yA, yB; + polygon->calcVertExtents(xVal, yA, yB, flag); + if (flag) { + double latA = centerLatitude + yA * polygonResolution; + double latB = centerLatitude + yB * polygonResolution; + int iA = ((int)floor(latA * pointsPerDegree)) - latN0; + int iB = ((int)floor(latB * pointsPerDegree)) - latN0; + for (iy = iA; iy <= iB; ++iy) { + S[ix][iy] = 1; + S[ix - 1][iy] = 1; + } + } + } + + for (iy = NY - 1; iy >= 0; --iy) { + for (ix = 0; ix < NX; ++ix) { + if (S[ix][iy]) { + double lonVal = (lonN0 + ix + 0.5) / pointsPerDegree; + double latVal = (latN0 + iy + 0.5) / pointsPerDegree; + ptList.push_back(std::pair(latVal, lonVal)); + } + } + } + + std::vector> *vListS = calcScanPointVirtices(S, NX, NY); + + std::vector> *ii_list = + new std::vector>(); + for (int i = 0; i < (int)vListS->size(); ++i) { + std::tie(ix, iy) = (*vListS)[i]; + double lonVal = ((double)lonN0 + ix) / pointsPerDegree; + double latVal = ((double)latN0 + iy) / pointsPerDegree; + + int xval = (int)floor( + ((lonVal - centerLongitude) * cosVal / polygonResolution) + 0.5); + int yval = (int)floor(((latVal - centerLatitude) / polygonResolution) + + 0.5); + ii_list->push_back(std::tuple(xval, yval)); + } + boundaryPolygon = new PolygonClass(ii_list); + + delete ii_list; + delete vListS; + for (ix = 0; ix < NX; ++ix) { + free(S[ix]); + } + free(S); + } else { + CORE_DUMP; + } + + return (ptList); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PolygonRlanRegionClass::getMaxDist() ****/ +/******************************************************************************************/ +double PolygonRlanRegionClass::getMaxDist() const +{ + int ptIdx; + double dist; + double maxDist = 0.0; + int numRLANPoints = polygon->num_bdy_pt[0]; + + for (ptIdx = 0; ptIdx < numRLANPoints; ptIdx++) { + int xval = polygon->bdy_pt_x[0][ptIdx]; + int yval = polygon->bdy_pt_y[0][ptIdx]; + dist = sqrt(((double)xval) * xval + ((double)yval) * yval) * + (polygonResolution * M_PI / 180.0) * CConst::earthRadius; + if (dist > maxDist) { + maxDist = dist; + } + } + return (maxDist); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: RlanRegionClass::getBoundaryPolygon() ****/ +/******************************************************************************************/ +std::vector RlanRegionClass::getBoundaryPolygon(TerrainClass *terrain) const +{ + std::vector ptList; + + if (!configuredFlag) { + throw std::runtime_error(ErrStream() + << "ERROR: RlanRegionClass::getBoundaryPolygon() " + "RlanRegion not configured"); + } + + if (boundaryPolygon) { + int ptIdx; + int numRLANPoints = boundaryPolygon->num_bdy_pt[0]; + + for (ptIdx = 0; ptIdx < numRLANPoints; ptIdx++) { + int xval = boundaryPolygon->bdy_pt_x[0][ptIdx]; + int yval = boundaryPolygon->bdy_pt_y[0][ptIdx]; + double longitude = centerLongitude + + xval * polygonResolution * oneOverCosVal; + double latitude = centerLatitude + yval * polygonResolution; + + double heightAMSL; + if (fixedHeightAMSL) { + heightAMSL = centerHeightAMSL; + } else { + double terrainHeight, bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum rlanHeightSource; + terrain->getTerrainHeight(longitude, + latitude, + terrainHeight, + bldgHeight, + lidarHeightResult, + rlanHeightSource); + heightAMSL = terrainHeight + centerHeightAMSL - centerTerrainHeight; + } + GeodeticCoord rlanEllipsePtGeo = + GeodeticCoord::fromLatLon(latitude, longitude, heightAMSL / 1000.0); + ptList.push_back(rlanEllipsePtGeo); + } + } + + return (ptList); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: RlanRegionClass::calcScanPointVirtices() ****/ +/******************************************************************************************/ +std::vector> *RlanRegionClass::calcScanPointVirtices(int **S, + int NX, + int NY) const +{ + int ix, iy; + int minx, miny, maxx, maxy; + /**************************************************************************************/ + /* Find minx, miny, maxx, maxy */ + /**************************************************************************************/ + bool initFlag = false; + for (ix = 0; ix < NX; ++ix) { + for (iy = 0; iy < NY; ++iy) { + if (S[ix][iy]) { + if ((!initFlag) || (ix < minx)) { + minx = ix; + } + if ((!initFlag) || (ix > maxx)) { + maxx = ix; + } + if ((!initFlag) || (iy < miny)) { + miny = iy; + } + if ((!initFlag) || (iy > maxy)) { + maxy = iy; + } + initFlag = true; + } + } + } + if (!initFlag) { + throw std::runtime_error(ErrStream() + << "ERROR: RlanRegionClass::calcScanPointVirtices() " + "Invalid scan matrix"); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Create vlist and initialize to 4 corners, counterclockwise orientation */ + /**************************************************************************************/ + std::vector> *vlist = new std::vector>(); + vlist->push_back(std::tuple(minx, miny)); + vlist->push_back(std::tuple(maxx + 1, miny)); + vlist->push_back(std::tuple(maxx + 1, maxy + 1)); + vlist->push_back(std::tuple(minx, maxy + 1)); + /**************************************************************************************/ + + bool cont = true; + int vA = 0; + + while (cont) { + int vB = vA + 1; + if (vB == (int)vlist->size()) { + vB = 0; + cont = false; + } + int vAx, vAy, vBx, vBy; + std::tie(vAx, vAy) = (*vlist)[vA]; + std::tie(vBx, vBy) = (*vlist)[vB]; + int dx = (vBx > vAx ? 1 : vBx < vAx ? -1 : 0); + int dy = (vBy > vAy ? 1 : vBy < vAy ? -1 : 0); + int incx = -dy; + int incy = dx; + int vx0 = vAx; + int vy0 = vAy; + initFlag = true; + int prevn; + while ((vx0 != vBx) || (vy0 != vBy)) { + int vx1 = vx0 + dx; + int vy1 = vy0 + dy; + ix = (((dx == 1) || (incx == 1)) ? vx0 : vx0 - 1); + iy = (((dy == 1) || (incy == 1)) ? vy0 : vy0 - 1); + int n = 0; + while (S[ix][iy] == 0) { + ix += incx; + iy += incy; + n++; + } + if (initFlag) { + if (n) { + (*vlist)[vA] = std::make_tuple(vx0 + n * incx, + vy0 + n * incy); + } + initFlag = false; + } else if (prevn != n) { + vlist->insert(vlist->begin() + vB, + std::make_tuple(vx0 + prevn * incx, + vy0 + prevn * incy)); + vB++; + vlist->insert(vlist->begin() + vB, + std::make_tuple(vx0 + n * incx, vy0 + n * incy)); + vB++; + } + + prevn = n; + vx0 = vx1; + vy0 = vy1; + +#if 0 + for(int k=0; ksize(); ++k) { + int printx, printy; + std::tie (printx, printy) = (*vlist)[k]; + printf("(%d,%d) ", printx, printy); + } + printf("\n"); +#endif + } + if (prevn) { + (*vlist)[vB] = std::make_tuple(vx0 + prevn * incx, vy0 + prevn * incy); + } + vA = vB; + } + + return (vlist); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: RlanRegionClass::calcMinAOB() ****/ +/******************************************************************************************/ +double RlanRegionClass::calcMinAOB(PolygonClass *poly, + double polyResolution, + arma::vec &F, + arma::vec &ptg, + arma::vec &minLoc) +{ + double minAOB; + + bool found = false; + if (((ptg(2) < 0.0) && (F(2) > 0.0)) || ((ptg(2) > 0.0) && (F(2) < 0.0))) { + double dist = -F(2) / ptg(2); + double xproj = F(0) + dist * ptg(0); + double yproj = F(1) + dist * ptg(1); + + int minx, maxx, miny, maxy; + poly->comp_bdy_min_max(minx, maxx, miny, maxy); + if ((xproj >= (minx - 1) * polyResolution) && + (xproj <= (maxx + 1) * polyResolution) && + (yproj >= (miny - 1) * polyResolution) && + (yproj <= (maxy + 1) * polyResolution)) { + int xval = (int)floor(xproj / polyResolution + 0.5); + int yval = (int)floor(yproj / polyResolution + 0.5); + + bool edge; + bool contains = poly->in_bdy_area(xval, yval, &edge); + + if (contains || edge) { + minLoc(0) = xproj; + minLoc(1) = yproj; + minAOB = 0.0; + found = true; + } + } + } + + if (!found) { + double maxCosAOB = -1.0; + for (int segIdx = 0; segIdx < poly->num_segment; ++segIdx) { + int prevIdx = poly->num_bdy_pt[segIdx] - 1; + arma::vec prevVirtex(3); + prevVirtex(0) = poly->bdy_pt_x[segIdx][prevIdx] * polyResolution; + prevVirtex(1) = poly->bdy_pt_y[segIdx][prevIdx] * polyResolution; + prevVirtex(2) = 0.0; + for (int ptIdx = 0; ptIdx < poly->num_bdy_pt[segIdx]; ++ptIdx) { + arma::vec virtex(3); + virtex(0) = poly->bdy_pt_x[segIdx][ptIdx] * polyResolution; + virtex(1) = poly->bdy_pt_y[segIdx][ptIdx] * polyResolution; + virtex(2) = 0.0; + + double D2 = dot(virtex - prevVirtex, virtex - prevVirtex); + double D1 = 2 * dot(virtex - prevVirtex, prevVirtex - F); + double D0 = dot(prevVirtex - F, prevVirtex - F); + + double C0 = dot(prevVirtex - F, ptg); + double C1 = dot(virtex - prevVirtex, ptg); + + double eps = (D0 * C1 - C0 * D1 / 2) / (D2 * C0 - C1 * D1 / 2); + + double cosAOB = C0 / sqrt(D0); + if (cosAOB > maxCosAOB) { + maxCosAOB = cosAOB; + minLoc(0) = prevVirtex(0); + minLoc(1) = prevVirtex(1); + } + + if ((eps > 0.0) && (eps < 1.0)) { + cosAOB = (C0 + C1 * eps) / sqrt(D0 + eps * (D1 + eps * D2)); + if (cosAOB > maxCosAOB) { + maxCosAOB = cosAOB; + minLoc(0) = (1.0 - eps) * prevVirtex(0) + + eps * virtex(0); + minLoc(1) = (1.0 - eps) * prevVirtex(1) + + eps * virtex(1); + } + } + + prevIdx = ptIdx; + prevVirtex = virtex; + } + } + + minAOB = acos(maxCosAOB) * 180.0 / M_PI; + } + + return minAOB; +} +/******************************************************************************************/ diff --git a/src/afc-engine/RlanRegion.h b/src/afc-engine/RlanRegion.h new file mode 100644 index 0000000..827e6ae --- /dev/null +++ b/src/afc-engine/RlanRegion.h @@ -0,0 +1,195 @@ +/******************************************************************************************/ +/**** FILE: RlanRegion.h ****/ +/**** Class to define uncertainty region in which RLAN may be. There are 2 types of ****/ +/**** regions, ellipse and polygon. ****/ +/******************************************************************************************/ + +#ifndef RLAN_REGION_H +#define RLAN_REGION_H + +#include +#include +#include "cconst.h" +#include "AfcDefinitions.h" +#include "Vector3.h" +#include "GeodeticCoord.h" +#include "polygon.h" + +class TerrainClass; + +/******************************************************************************************/ +/**** CLASS: RlanRegionClass ****/ +/******************************************************************************************/ +class RlanRegionClass +{ + public: + RlanRegionClass(); + virtual ~RlanRegionClass(); + + virtual RLANBoundary getType() const = 0; + virtual LatLon closestPoint(LatLon latlon, bool &contains) const = 0; + virtual std::vector getBoundary(TerrainClass *terrain) const = 0; + virtual std::vector getScan(CConst::ScanRegionMethodEnum method, + double scanResolutionM, + int pointsPerDegree) = 0; + virtual double getMaxDist() const = 0; + virtual void configure(CConst::HeightTypeEnum rlanHeightType, + TerrainClass *terrain) = 0; + virtual double calcMinAOB(LatLon ulsRxLatLon, + Vector3 ulsAntennaPointing, + double ulsRxHeightAMSL, + double &minAOBLon, + double &minAOBLat, + double &minAOBHeghtAMSL) = 0; + + std::vector getBoundaryPolygon(TerrainClass *terrain) const; + double getMinHeightAGL() const; + double getMaxHeightAGL() const; + double getMinHeightAMSL() const; + double getMaxHeightAMSL() const; + double getCenterLongitude() const + { + return (centerLongitude); + } + double getCenterLatitude() const + { + return (centerLatitude); + } + double getCenterHeightAMSL() const + { + return (centerHeightAMSL); + } + double getCenterTerrainHeight() const + { + return (centerTerrainHeight); + } + double getHeightUncertainty() const + { + return (heightUncertainty); + } + bool getFixedHeightAMSL() const + { + return (fixedHeightAMSL); + } + Vector3 getCenterPosn() const + { + return (centerPosn); + } + + Vector3 computePointing(double azimuth, double elevation) const; + + static double calcMinAOB(PolygonClass *poly, + double polyResolution, + arma::vec &F, + arma::vec &ptg, + arma::vec &minLoc); + static double minRlanHeightAboveTerrain; + + protected: + std::vector> *calcScanPointVirtices(int **S, + int NX, + int NY) const; + + double centerLongitude; + double centerLatitude; + double cosVal, oneOverCosVal; + double centerHeightInput; + double centerHeightAMSL; + double centerTerrainHeight; + double minTerrainHeight; + double maxTerrainHeight; + double heightUncertainty; + Vector3 centerPosn; + Vector3 upVec; + Vector3 eastVec; + Vector3 northVec; + + bool fixedHeightAMSL; + bool configuredFlag; + + PolygonClass *boundaryPolygon; + double polygonResolution; +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CLASS: EllipseRlanRegionClass ****/ +/******************************************************************************************/ +class EllipseRlanRegionClass : RlanRegionClass +{ + public: + EllipseRlanRegionClass(DoubleTriplet rlanLLA, + DoubleTriplet rlanUncerts_m, + double rlanOrientationDeg, + bool fixedHeightAMSLVal); + + ~EllipseRlanRegionClass(); + + RLANBoundary getType() const; + LatLon closestPoint(LatLon latlon, bool &contains) const; + std::vector getBoundary(TerrainClass *terrain) const; + std::vector getScan(CConst::ScanRegionMethodEnum method, + double scanResolutionM, + int pointsPerDegree); + double getMaxDist() const; + void configure(CConst::HeightTypeEnum rlanHeightType, TerrainClass *terrain); + double calcMinAOB(LatLon ulsRxLatLon, + Vector3 ulsAntennaPointing, + double ulsRxHeightAMSL, + double &minAOBLon, + double &minAOBLat, + double &minAOBHeghtAMSL); + + private: + void calcHorizExtents(double latVal, double &lonA, double &lonB, bool &flag) const; + void calcVertExtents(double lonVal, double &latA, double &latB, bool &flag) const; + + double semiMinorAxis; + double semiMajorAxis; + double orientationDeg; + + Vector3 majorVec; + Vector3 minorVec; + + arma::mat mxA; + arma::mat mxB; + arma::mat mxC; +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CLASS: PolygonRlanRegionClass ****/ +/******************************************************************************************/ +class PolygonRlanRegionClass : RlanRegionClass +{ + public: + PolygonRlanRegionClass(DoubleTriplet rlanLLA, + DoubleTriplet rlanUncerts_m, + const std::vector> &rlanPolygon, + RLANBoundary polygonTypeVal, + bool fixedHeightAMSLVal); + + ~PolygonRlanRegionClass(); + + RLANBoundary getType() const; + LatLon closestPoint(LatLon latlon, bool &contains) const; + std::vector getBoundary(TerrainClass *terrain) const; + std::vector getScan(CConst::ScanRegionMethodEnum method, + double scanResolutionM, + int pointsPerDegree); + double getMaxDist() const; + void configure(CConst::HeightTypeEnum rlanHeightType, TerrainClass *terrain); + double calcMinAOB(LatLon ulsRxLatLon, + Vector3 ulsAntennaPointing, + double ulsRxHeightAMSL, + double &minAOBLon, + double &minAOBLat, + double &minAOBHeghtAMSL); + + private: + PolygonClass *polygon; + RLANBoundary polygonType; +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/UlsDatabase.cpp b/src/afc-engine/UlsDatabase.cpp new file mode 100644 index 0000000..e0d7c84 --- /dev/null +++ b/src/afc-engine/UlsDatabase.cpp @@ -0,0 +1,1005 @@ +#include "UlsDatabase.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "afclogging/ErrStream.h" +#include "AfcDefinitions.h" +#include "lininterp.h" +#include "global_defines.h" + +namespace +{ +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "UlsDatabase") + +} // end namespace + +/******************************************************************************************/ +/**** CONSTRUCTOR: UlsDatabase::UlsDatabase() ****/ +/******************************************************************************************/ +UlsDatabase::UlsDatabase() +{ + nullInitialize(); + + std::vector fieldIdxList; + std::vector prFieldIdxList; + std::vector antnameFieldIdxList; + std::vector antaobFieldIdxList; + std::vector antgainFieldIdxList; + std::vector rasFieldIdxList; + + columns << "fsid"; + fieldIdxList.push_back(&fsidIdx); + columns << "region"; + fieldIdxList.push_back(®ionIdx); + + columns << "callsign"; + fieldIdxList.push_back(&callsignIdx); + columns << "path_number"; + fieldIdxList.push_back(&pathNumberIdx); + columns << "radio_service"; + fieldIdxList.push_back(&radio_serviceIdx); + columns << "name"; + fieldIdxList.push_back(&nameIdx); + columns << "rx_callsign"; + fieldIdxList.push_back(&rx_callsignIdx); + columns << "rx_antenna_num"; + fieldIdxList.push_back(&rx_antenna_numIdx); + columns << "freq_assigned_start_mhz"; + fieldIdxList.push_back(&freq_assigned_start_mhzIdx); + columns << "freq_assigned_end_mhz"; + fieldIdxList.push_back(&freq_assigned_end_mhzIdx); + columns << "tx_lat_deg"; + fieldIdxList.push_back(&tx_lat_degIdx); + columns << "tx_long_deg"; + fieldIdxList.push_back(&tx_long_degIdx); + columns << "tx_ground_elev_m"; + fieldIdxList.push_back(&tx_ground_elev_mIdx); + columns << "tx_polarization"; + fieldIdxList.push_back(&tx_polarizationIdx); + columns << "tx_gain"; + fieldIdxList.push_back(&tx_gainIdx); + columns << "tx_eirp"; + fieldIdxList.push_back(&tx_eirpIdx); + columns << "tx_height_to_center_raat_m"; + fieldIdxList.push_back(&tx_height_to_center_raat_mIdx); + columns << "tx_architecture"; + fieldIdxList.push_back(&tx_architecture_mIdx); + columns << "azimuth_angle_to_tx"; + fieldIdxList.push_back(&azimuth_angle_to_tx_mIdx); + columns << "elevation_angle_to_tx"; + fieldIdxList.push_back(&elevation_angle_to_tx_mIdx); + columns << "rx_lat_deg"; + fieldIdxList.push_back(&rx_lat_degIdx); + columns << "rx_long_deg"; + fieldIdxList.push_back(&rx_long_degIdx); + columns << "rx_ground_elev_m"; + fieldIdxList.push_back(&rx_ground_elev_mIdx); + columns << "rx_height_to_center_raat_m"; + fieldIdxList.push_back(&rx_height_to_center_raat_mIdx); + columns << "rx_line_loss"; + fieldIdxList.push_back(&rx_line_loss_mIdx); + columns << "rx_gain"; + fieldIdxList.push_back(&rx_gainIdx); + columns << "rx_ant_diameter"; + fieldIdxList.push_back(&rx_antennaDiameterIdx); + columns << "rx_near_field_ant_diameter"; + fieldIdxList.push_back(&rx_near_field_ant_diameterIdx); + columns << "rx_near_field_dist_limit"; + fieldIdxList.push_back(&rx_near_field_dist_limitIdx); + columns << "rx_near_field_ant_efficiency"; + fieldIdxList.push_back(&rx_near_field_ant_efficiencyIdx); + columns << "rx_ant_category"; + fieldIdxList.push_back(&rx_antennaCategoryIdx); + columns << "status"; + fieldIdxList.push_back(&statusIdx); + columns << "mobile"; + fieldIdxList.push_back(&mobileIdx); + columns << "rx_ant_model"; + fieldIdxList.push_back(&rx_ant_modelNameIdx); + columns << "rx_ant_model_idx"; + fieldIdxList.push_back(&rx_ant_model_idxIdx); + + columns << "rx_diversity_height_to_center_raat_m"; + fieldIdxList.push_back(&rx_diversity_height_to_center_raat_mIdx); + columns << "rx_diversity_gain"; + fieldIdxList.push_back(&rx_diversity_gainIdx); + columns << "rx_diversity_ant_diameter"; + fieldIdxList.push_back(&rx_diversity_antennaDiameterIdx); + + columns << "p_rp_num"; + fieldIdxList.push_back(&p_rp_numIdx); + + prColumns << "prSeq"; + prFieldIdxList.push_back(&prSeqIdx); + prColumns << "pr_ant_type"; + prFieldIdxList.push_back(&prTypeIdx); + prColumns << "pr_lat_deg"; + prFieldIdxList.push_back(&pr_lat_degIdx); + prColumns << "pr_lon_deg"; + prFieldIdxList.push_back(&pr_lon_degIdx); + prColumns << "pr_height_to_center_raat_tx_m"; + prFieldIdxList.push_back(&pr_height_to_center_raat_tx_mIdx); + prColumns << "pr_height_to_center_raat_rx_m"; + prFieldIdxList.push_back(&pr_height_to_center_raat_rx_mIdx); + + prColumns << "pr_back_to_back_gain_tx"; + prFieldIdxList.push_back(&prTxGainIdx); + prColumns << "pr_ant_diameter_tx"; + prFieldIdxList.push_back(&prTxDiameterIdx); + prColumns << "pr_back_to_back_gain_rx"; + prFieldIdxList.push_back(&prRxGainIdx); + prColumns << "pr_ant_diameter_rx"; + prFieldIdxList.push_back(&prRxDiameterIdx); + prColumns << "pr_ant_category"; + prFieldIdxList.push_back(&prAntCategoryIdx); + prColumns << "pr_ant_model"; + prFieldIdxList.push_back(&prAntModelNameIdx); + prColumns << "pr_ant_model_idx"; + prFieldIdxList.push_back(&pr_ant_model_idxIdx); + prColumns << "pr_reflector_height_m"; + prFieldIdxList.push_back(&prReflectorHeightIdx); + prColumns << "pr_reflector_width_m"; + prFieldIdxList.push_back(&prReflectorWidthIdx); + + antnameColumns << "ant_idx"; + antnameFieldIdxList.push_back(&antname_ant_idxIdx); + antnameColumns << "ant_name"; + antnameFieldIdxList.push_back(&antname_ant_nameIdx); + + antaobColumns << "aob_idx"; + antaobFieldIdxList.push_back(&antaob_aob_idxIdx); + antaobColumns << "aob_deg"; + antaobFieldIdxList.push_back(&antaob_aob_degIdx); + + antgainColumns << "id"; + antgainFieldIdxList.push_back(&antgain_idIdx); + antgainColumns << "gain_db"; + antgainFieldIdxList.push_back(&antgain_gainIdx); + + rasColumns << "rasid"; + rasFieldIdxList.push_back(&ras_rasidIdx); + rasColumns << "region"; + rasFieldIdxList.push_back(&ras_regionIdx); + rasColumns << "name"; + rasFieldIdxList.push_back(&ras_nameIdx); + rasColumns << "location"; + rasFieldIdxList.push_back(&ras_locationIdx); + rasColumns << "startFreqMHz"; + rasFieldIdxList.push_back(&ras_startFreqMHzIdx); + rasColumns << "stopFreqMHz"; + rasFieldIdxList.push_back(&ras_stopFreqMHzIdx); + rasColumns << "exclusionZone"; + rasFieldIdxList.push_back(&ras_exclusionZoneIdx); + rasColumns << "rect1lat1"; + rasFieldIdxList.push_back(&ras_rect1lat1Idx); + rasColumns << "rect1lat2"; + rasFieldIdxList.push_back(&ras_rect1lat2Idx); + rasColumns << "rect1lon1"; + rasFieldIdxList.push_back(&ras_rect1lon1Idx); + rasColumns << "rect1lon2"; + rasFieldIdxList.push_back(&ras_rect1lon2Idx); + rasColumns << "rect2lat1"; + rasFieldIdxList.push_back(&ras_rect2lat1Idx); + rasColumns << "rect2lat2"; + rasFieldIdxList.push_back(&ras_rect2lat2Idx); + rasColumns << "rect2lon1"; + rasFieldIdxList.push_back(&ras_rect2lon1Idx); + rasColumns << "rect2lon2"; + rasFieldIdxList.push_back(&ras_rect2lon2Idx); + rasColumns << "radiusKm"; + rasFieldIdxList.push_back(&ras_radiusKmIdx); + rasColumns << "centerLat"; + rasFieldIdxList.push_back(&ras_centerLatIdx); + rasColumns << "centerLon"; + rasFieldIdxList.push_back(&ras_centerLonIdx); + rasColumns << "heightAGL"; + rasFieldIdxList.push_back(&ras_heightAGLIdx); + + int fIdx; + for (fIdx = 0; fIdx < (int)fieldIdxList.size(); ++fIdx) { + *fieldIdxList[fIdx] = fIdx; + } + for (fIdx = 0; fIdx < (int)prFieldIdxList.size(); ++fIdx) { + *prFieldIdxList[fIdx] = fIdx; + } + for (fIdx = 0; fIdx < (int)antnameFieldIdxList.size(); ++fIdx) { + *antnameFieldIdxList[fIdx] = fIdx; + } + for (fIdx = 0; fIdx < (int)antaobFieldIdxList.size(); ++fIdx) { + *antaobFieldIdxList[fIdx] = fIdx; + } + for (fIdx = 0; fIdx < (int)antgainFieldIdxList.size(); ++fIdx) { + *antgainFieldIdxList[fIdx] = fIdx; + } + for (fIdx = 0; fIdx < (int)rasFieldIdxList.size(); ++fIdx) { + *rasFieldIdxList[fIdx] = fIdx; + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** DESTRUCTOR: UlsDatabase::~UlsDatabase() ****/ +/******************************************************************************************/ +UlsDatabase::~UlsDatabase() +{ +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: UlsDatabase::nullInitialize ****/ +/**** Initialize all indices to -1 so coverity warnings are suppressed. ****/ +/******************************************************************************************/ +void UlsDatabase::nullInitialize() +{ + fsidIdx = -1; + regionIdx = -1; + callsignIdx = -1; + pathNumberIdx = -1; + radio_serviceIdx = -1; + nameIdx = -1; + rx_callsignIdx = -1; + rx_antenna_numIdx = -1; + freq_assigned_start_mhzIdx = -1; + freq_assigned_end_mhzIdx = -1; + tx_lat_degIdx = -1; + tx_long_degIdx = -1; + tx_ground_elev_mIdx = -1; + tx_polarizationIdx = -1; + tx_gainIdx = -1; + tx_eirpIdx = -1; + tx_height_to_center_raat_mIdx = -1; + tx_architecture_mIdx = -1; + azimuth_angle_to_tx_mIdx = -1; + elevation_angle_to_tx_mIdx = -1; + rx_lat_degIdx = -1; + rx_long_degIdx = -1; + rx_ground_elev_mIdx = -1; + rx_height_to_center_raat_mIdx = -1; + rx_line_loss_mIdx = -1; + rx_gainIdx = -1; + rx_antennaDiameterIdx = -1; + rx_near_field_ant_diameterIdx = -1; + rx_near_field_dist_limitIdx = -1; + rx_near_field_ant_efficiencyIdx = -1; + rx_antennaCategoryIdx = -1; + statusIdx = -1; + mobileIdx = -1; + rx_ant_modelNameIdx = -1; + rx_ant_model_idxIdx = -1; + p_rp_numIdx = -1; + + rx_diversity_height_to_center_raat_mIdx = -1; + rx_diversity_gainIdx = -1; + rx_diversity_antennaDiameterIdx = -1; + + prSeqIdx = -1; + prTypeIdx = -1; + pr_lat_degIdx = -1; + pr_lon_degIdx = -1; + pr_height_to_center_raat_rx_mIdx = -1; + pr_height_to_center_raat_tx_mIdx = -1; + + prTxGainIdx = -1; + prTxDiameterIdx = -1; + prRxGainIdx = -1; + prRxDiameterIdx = -1; + prAntCategoryIdx = -1; + prAntModelNameIdx = -1; + pr_ant_model_idxIdx = -1; + prReflectorHeightIdx = -1; + prReflectorWidthIdx = -1; + + antname_ant_idxIdx = -1; + antname_ant_nameIdx = -1; + + antaob_aob_idxIdx = -1; + antaob_aob_degIdx = -1; + + antgain_idIdx = -1; + antgain_gainIdx = -1; + + ras_rasidIdx = -1; + ras_regionIdx = -1; + ras_nameIdx = -1; + ras_locationIdx = -1; + ras_startFreqMHzIdx = -1; + ras_stopFreqMHzIdx = -1; + ras_exclusionZoneIdx = -1; + ras_rect1lat1Idx = -1; + ras_rect1lat2Idx = -1; + ras_rect1lon1Idx = -1; + ras_rect1lon2Idx = -1; + ras_rect2lat1Idx = -1; + ras_rect2lat2Idx = -1; + ras_rect2lon1Idx = -1; + ras_rect2lon2Idx = -1; + ras_radiusKmIdx = -1; + ras_centerLatIdx = -1; + ras_centerLonIdx = -1; + ras_heightAGLIdx = -1; +} +/******************************************************************************************/ + +void verifyResult(const QSqlQuery &ulsQueryRes) +{ + LOGGER_DEBUG(logger) << "Is Active: " << ulsQueryRes.isActive(); + LOGGER_DEBUG(logger) << "Is Select: " << ulsQueryRes.isSelect(); + if (!ulsQueryRes.isActive()) { + // Query encountered error + QSqlError err = ulsQueryRes.lastError(); + throw std::runtime_error(ErrStream() + << "UlsDatabase.cpp: Database query failed with code " + << err.type() << " " << err.text()); + } +} + +// construct and run sql query and return result +QSqlQuery runQueryWithBounds(const SqlScopedConnection &db, + const QStringList &columns, + const double &minLat, + const double &maxLat, + const double &minLon, + const double &maxLon); +QSqlQuery runQueryById(const SqlScopedConnection &db, + const QStringList &columns, + const int &fsid); + +void UlsDatabase::loadFSById(const QString &dbName, + std::vector &deniedRegionList, + std::vector &antennaList, + std::vector &target, + const int &fsid) +{ + LOGGER_DEBUG(logger) << "FSID: " << fsid; + + // create and open db connection + SqlConnectionDefinition config; + config.driverName = "QSQLITE"; + config.dbName = dbName; + + LOGGER_INFO(logger) << "Opening database: " << dbName; + SqlScopedConnection db(new SqlExceptionDb(config.newConnection())); + db->tryOpen(); + + LOGGER_DEBUG(logger) << "Querying uls database"; + QSqlQuery ulsQueryRes = runQueryById(db, columns, fsid); + + verifyResult(ulsQueryRes); + + fillTarget(db, deniedRegionList, antennaList, target, ulsQueryRes); +} + +void UlsDatabase::loadUlsData(const QString &dbName, + std::vector &deniedRegionList, + std::vector &antennaList, + std::vector &target, + const double &minLat, + const double &maxLat, + const double &minLon, + const double &maxLon) +{ + LOGGER_DEBUG(logger) << "Bounds: " << minLat << ", " << maxLat << "; " << minLon << ", " + << maxLon; + + // create and open db connection + SqlConnectionDefinition config; + config.driverName = "QSQLITE"; + config.dbName = dbName; + + LOGGER_INFO(logger) << "Opening database: " << dbName; + SqlScopedConnection db(new SqlExceptionDb(config.newConnection())); + db->tryOpen(); + + LOGGER_DEBUG(logger) << "Querying uls database"; + QSqlQuery ulsQueryRes = runQueryWithBounds(db, columns, minLat, maxLat, minLon, maxLon); + + verifyResult(ulsQueryRes); + + fillTarget(db, deniedRegionList, antennaList, target, ulsQueryRes); +} + +QSqlQuery runQueryWithBounds(const SqlScopedConnection &db, + const QStringList &columns, + const double &minLat, + const double &maxLat, + const double &minLon, + const double &maxLon) +{ + return SqlSelect(*db, "uls") + .cols(columns) + .where(QString("(rx_lat_deg BETWEEN %1 AND %2)" + "AND" + "(rx_long_deg BETWEEN %3 AND %4)") + .arg(std::min(minLat, maxLat)) + .arg(std::max(minLat, maxLat)) + .arg(std::min(minLon, maxLon)) + .arg(std::max(minLon, maxLon))) + .order("fsid") + .run(); +} + +QSqlQuery runQueryById(const SqlScopedConnection &db, + const QStringList &columns, + const int &fsid) +{ + return SqlSelect(*db, "uls") + .cols(columns) + .where(QString("fsid=%1").arg(fsid)) + .topmost(1) + .run(); +} + +void UlsDatabase::fillTarget(SqlScopedConnection &db, + std::vector &deniedRegionList, + std::vector &antennaList, + std::vector &target, + QSqlQuery &q) +{ + // resize vector to fit result + if (q.driver()->hasFeature(QSqlDriver::QuerySize)) { + // if the driver supports .size() then use it because is is more efficient + LOGGER_DEBUG(logger) << target.size() << " to " << q.size(); + target.resize(q.size()); + q.setForwardOnly(true); + } else { + if (!q.last()) { + // throw std::runtime_error(ErrStream() << "UlsDatabase.cpp: Failed to get + // last item. Check that lat/lon are within CONUS : " << q.at()); No FS's + // within 150 Km, return with empty list return; + } else { + LOGGER_DEBUG(logger) << target.size() << " to last " << q.at(); + target.resize(q.at() + 1); + q.first(); + q.previous(); + } + } + + /**************************************************************************************/ + /* Read RAS Table */ + /**************************************************************************************/ + QSqlQuery rasQueryRes = SqlSelect(*db, "ras").cols(rasColumns).run(); + int numRAS; + // resize vector to fit result + if (rasQueryRes.driver()->hasFeature(QSqlDriver::QuerySize)) { + // if the driver supports .size() then use it because is is more efficient + numRAS = rasQueryRes.size(); + rasQueryRes.setForwardOnly(true); + } else { + if (!rasQueryRes.last()) { + numRAS = 0; + } else { + numRAS = rasQueryRes.at() + 1; + rasQueryRes.first(); + rasQueryRes.previous(); + } + } + + while (rasQueryRes.next()) { + int rasid = rasQueryRes.value(ras_rasidIdx).toInt(); + std::string exclusionZoneStr = + rasQueryRes.value(ras_exclusionZoneIdx).toString().toStdString(); + DeniedRegionClass::GeometryEnum exclusionZoneType = DeniedRegionClass::nullGeometry; + + if (exclusionZoneStr == "One Rectangle") { + exclusionZoneType = DeniedRegionClass::rectGeometry; + } else if (exclusionZoneStr == "Two Rectangles") { + exclusionZoneType = DeniedRegionClass::rect2Geometry; + } else if (exclusionZoneStr == "Circle") { + exclusionZoneType = DeniedRegionClass::circleGeometry; + } else if (exclusionZoneStr == "Horizon Distance") { + exclusionZoneType = DeniedRegionClass::horizonDistGeometry; + } else { + CORE_DUMP; + } + + DeniedRegionClass *ras = (DeniedRegionClass *)NULL; + switch (exclusionZoneType) { + case DeniedRegionClass::rectGeometry: + case DeniedRegionClass::rect2Geometry: { + ras = (DeniedRegionClass *)new RectDeniedRegionClass(rasid); + + double rect1lat1 = rasQueryRes.value(ras_rect1lat1Idx).toDouble(); + double rect1lat2 = rasQueryRes.value(ras_rect1lat2Idx).toDouble(); + double rect1lon1 = rasQueryRes.value(ras_rect1lon1Idx).toDouble(); + double rect1lon2 = rasQueryRes.value(ras_rect1lon2Idx).toDouble(); + + ((RectDeniedRegionClass *)ras) + ->addRect(rect1lon1, rect1lon2, rect1lat1, rect1lat2); + + if (exclusionZoneType == DeniedRegionClass::rect2Geometry) { + double rect2lat1 = + rasQueryRes.value(ras_rect2lat1Idx).toDouble(); + double rect2lat2 = + rasQueryRes.value(ras_rect2lat2Idx).toDouble(); + double rect2lon1 = + rasQueryRes.value(ras_rect2lon1Idx).toDouble(); + double rect2lon2 = + rasQueryRes.value(ras_rect2lon2Idx).toDouble(); + + ((RectDeniedRegionClass *)ras) + ->addRect(rect2lon1, + rect2lon2, + rect2lat1, + rect2lat2); + } + } break; + case DeniedRegionClass::circleGeometry: + case DeniedRegionClass::horizonDistGeometry: { + double lonCircle = rasQueryRes.value(ras_centerLonIdx).toDouble(); + double latCircle = rasQueryRes.value(ras_centerLatIdx).toDouble(); + + bool horizonDistFlag = (exclusionZoneType == + DeniedRegionClass::horizonDistGeometry); + + ras = (DeniedRegionClass *)new CircleDeniedRegionClass( + rasid, + horizonDistFlag); + + ((CircleDeniedRegionClass *)ras)->setLongitudeCenter(lonCircle); + ((CircleDeniedRegionClass *)ras)->setLatitudeCenter(latCircle); + + if (!horizonDistFlag) { + double radius = + rasQueryRes.value(ras_radiusKmIdx).isNull() ? + quietNaN : + rasQueryRes.value(ras_radiusKmIdx) + .toDouble() * + 1.0e3; // Convert km to m + + ((CircleDeniedRegionClass *)ras)->setRadius(radius); + } else { + /**************************************************************************/ + /* heightAGL */ + /**************************************************************************/ + double heightAGL = + rasQueryRes.value(ras_heightAGLIdx).isNull() ? + quietNaN : + rasQueryRes.value(ras_heightAGLIdx) + .toDouble(); // Height value in m + ras->setHeightAGL(heightAGL); + /**************************************************************************/ + } + } break; + default: + break; + } + + if (ras) { + double startFreq = + rasQueryRes.value(ras_startFreqMHzIdx).isNull() ? + quietNaN : + rasQueryRes.value(ras_startFreqMHzIdx).toDouble() * + 1.0e6; // Convert MHz to Hz + double stopFreq = rasQueryRes.value(ras_stopFreqMHzIdx).isNull() ? + quietNaN : + rasQueryRes.value(ras_stopFreqMHzIdx).toDouble() * + 1.0e6; // Convert MHz to Hz + + ras->setStartFreq(startFreq); + ras->setStopFreq(stopFreq); + ras->setType(DeniedRegionClass::RASType); + + deniedRegionList.push_back(ras); + } else { + CORE_DUMP; + } + } + LOGGER_DEBUG(logger) << "READ " << numRAS << " entries from database "; + /**************************************************************************************/ + + /**************************************************************************************/ + /* Get list of antenna names */ + /**************************************************************************************/ + QSqlQuery antnameQueryRes = SqlSelect(*db, "antname").cols(antnameColumns).run(); + int numAntennaDB; + // resize vector to fit result + if (antnameQueryRes.driver()->hasFeature(QSqlDriver::QuerySize)) { + // if the driver supports .size() then use it because is is more efficient + numAntennaDB = antnameQueryRes.size(); + antnameQueryRes.setForwardOnly(true); + } else { + if (!antnameQueryRes.last()) { + numAntennaDB = 0; + } else { + numAntennaDB = antnameQueryRes.at() + 1; + antnameQueryRes.first(); + antnameQueryRes.previous(); + } + } + + std::vector antennaIdxMap; + std::vector antennaNameList; + + for (int antIdxDB = 0; antIdxDB < numAntennaDB; ++antIdxDB) { + antennaIdxMap.push_back(-1); + antennaNameList.push_back(""); + } + + while (antnameQueryRes.next()) { + int antIdxDB = antnameQueryRes.value(antname_ant_idxIdx).toInt(); + std::string antennaName = + antnameQueryRes.value(antname_ant_nameIdx).toString().toStdString(); + antennaNameList[antIdxDB] = antennaName; + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Get list of antenna aob values */ + /**************************************************************************************/ + std::vector antennaAOBList; + if (numAntennaDB) { + QSqlQuery antaobQueryRes = SqlSelect(*db, "antaob").cols(antaobColumns).run(); + int numAntennaAOB; + // resize vector to fit result + if (antaobQueryRes.driver()->hasFeature(QSqlDriver::QuerySize)) { + // if the driver supports .size() then use it because is is more efficient + numAntennaAOB = antaobQueryRes.size(); + antaobQueryRes.setForwardOnly(true); + } else { + if (!antaobQueryRes.last()) { + numAntennaAOB = 0; + } else { + numAntennaAOB = antaobQueryRes.at() + 1; + antaobQueryRes.first(); + antaobQueryRes.previous(); + } + } + + for (int aobIdx = 0; aobIdx < numAntennaAOB; ++aobIdx) { + antennaAOBList.push_back(quietNaN); + } + + while (antaobQueryRes.next()) { + int aobIdx = antaobQueryRes.value(antaob_aob_idxIdx).toInt(); + double aobRad = antaobQueryRes.value(antaob_aob_degIdx).toDouble() * M_PI / + 180.0; + antennaAOBList[aobIdx] = aobRad; + } + } + /**************************************************************************************/ + + while (q.next()) { + int r = q.at(); + int fsid = q.value(fsidIdx).toInt(); + int numPR = q.value(p_rp_numIdx).toInt(); + + target.at(r).fsid = fsid; + target.at(r).region = q.value(regionIdx).toString().toStdString(); + target.at(r).callsign = q.value(callsignIdx).toString().toStdString(); + target.at(r).pathNumber = q.value(pathNumberIdx).toInt(); + target.at(r).radioService = q.value(radio_serviceIdx).toString().toStdString(); + target.at(r).entityName = q.value(nameIdx).toString().toStdString(); + target.at(r).rxCallsign = q.value(rx_callsignIdx).toString().toStdString(); + target.at(r).rxAntennaNumber = q.value(rx_antenna_numIdx).toInt(); + target.at(r).startFreq = q.value(freq_assigned_start_mhzIdx).toDouble(); + target.at(r).stopFreq = q.value(freq_assigned_end_mhzIdx).toDouble(); + target.at(r).txLatitudeDeg = q.value(tx_lat_degIdx).isNull() ? + quietNaN : + q.value(tx_lat_degIdx).toDouble(); + target.at(r).txLongitudeDeg = q.value(tx_long_degIdx).isNull() ? + quietNaN : + q.value(tx_long_degIdx).toDouble(); + target.at(r).txGroundElevation = q.value(tx_ground_elev_mIdx).isNull() ? + quietNaN : + q.value(tx_ground_elev_mIdx).toDouble(); + target.at(r).txPolarization = q.value(tx_polarizationIdx).toString().toStdString(); + target.at(r).txGain = q.value(tx_gainIdx).isNull() ? quietNaN : + q.value(tx_gainIdx).toDouble(); + target.at(r).txEIRP = q.value(tx_eirpIdx).toDouble(); + target.at(r).txHeightAboveTerrain = + q.value(tx_height_to_center_raat_mIdx).isNull() ? + quietNaN : + q.value(tx_height_to_center_raat_mIdx).toDouble(); + target.at(r).txArchitecture = + q.value(tx_architecture_mIdx).toString().toStdString(); + target.at(r).azimuthAngleToTx = + q.value(azimuth_angle_to_tx_mIdx).isNull() ? + quietNaN : + q.value(azimuth_angle_to_tx_mIdx).toDouble(); + target.at(r).elevationAngleToTx = + q.value(elevation_angle_to_tx_mIdx).isNull() ? + quietNaN : + q.value(elevation_angle_to_tx_mIdx).toDouble(); + target.at(r).rxLatitudeDeg = q.value(rx_lat_degIdx).toDouble(); + target.at(r).rxLongitudeDeg = q.value(rx_long_degIdx).toDouble(); + target.at(r).rxGroundElevation = q.value(rx_ground_elev_mIdx).isNull() ? + quietNaN : + q.value(rx_ground_elev_mIdx).toDouble(); + target.at(r).rxHeightAboveTerrain = + q.value(rx_height_to_center_raat_mIdx).isNull() ? + quietNaN : + q.value(rx_height_to_center_raat_mIdx).toDouble(); + target.at(r).rxLineLoss = q.value(rx_line_loss_mIdx).isNull() ? + quietNaN : + q.value(rx_line_loss_mIdx).toDouble(); + target.at(r).rxGain = q.value(rx_gainIdx).isNull() ? quietNaN : + q.value(rx_gainIdx).toDouble(); + target.at(r).rxAntennaDiameter = q.value(rx_antennaDiameterIdx).isNull() ? + quietNaN : + q.value(rx_antennaDiameterIdx).toDouble(); + + target.at(r).rxNearFieldAntDiameter = + q.value(rx_near_field_ant_diameterIdx).isNull() ? + quietNaN : + q.value(rx_near_field_ant_diameterIdx).toDouble(); + target.at(r).rxNearFieldDistLimit = + q.value(rx_near_field_dist_limitIdx).isNull() ? + quietNaN : + q.value(rx_near_field_dist_limitIdx).toDouble(); + target.at(r).rxNearFieldAntEfficiency = + q.value(rx_near_field_ant_efficiencyIdx).isNull() ? + quietNaN : + q.value(rx_near_field_ant_efficiencyIdx).toDouble(); + + target.at(r).hasDiversity = q.value(rx_diversity_gainIdx).isNull() ? false : true; + target.at(r).diversityGain = q.value(rx_diversity_gainIdx).isNull() ? + quietNaN : + q.value(rx_diversity_gainIdx).toDouble(); + target.at(r).diversityHeightAboveTerrain = + q.value(rx_diversity_height_to_center_raat_mIdx).isNull() ? + quietNaN : + q.value(rx_diversity_height_to_center_raat_mIdx).toDouble(); + target.at(r).diversityAntennaDiameter = + q.value(rx_diversity_antennaDiameterIdx).isNull() ? + quietNaN : + q.value(rx_diversity_antennaDiameterIdx).toDouble(); + + target.at(r).status = q.value(statusIdx).toString().toStdString(); + target.at(r).mobile = q.value(mobileIdx).toBool(); + target.at(r).rxAntennaModelName = + q.value(rx_ant_modelNameIdx).toString().toStdString(); + + int rxAntennaIdxDB = q.value(rx_ant_model_idxIdx).toInt(); + + AntennaClass *antennaPattern = (AntennaClass *)NULL; + + if (rxAntennaIdxDB != -1) { + if (antennaIdxMap[rxAntennaIdxDB] == -1) { + antennaPattern = + createAntennaPattern(db, + rxAntennaIdxDB, + antennaAOBList, + antennaNameList[rxAntennaIdxDB]); + antennaIdxMap[rxAntennaIdxDB] = antennaList.size(); + antennaList.push_back(antennaPattern); + } else { + antennaPattern = antennaList[antennaIdxMap[rxAntennaIdxDB]]; + } + } + target.at(r).rxAntenna = antennaPattern; + + target.at(r).numPR = numPR; + + std::string rxAntennaCategoryStr = + q.value(rx_antennaCategoryIdx).toString().toStdString(); + CConst::AntennaCategoryEnum rxAntennaCategory; + if (rxAntennaCategoryStr == "B1") { + rxAntennaCategory = CConst::B1AntennaCategory; + } else if (rxAntennaCategoryStr == "HP") { + rxAntennaCategory = CConst::HPAntennaCategory; + } else if (rxAntennaCategoryStr == "OTHER") { + rxAntennaCategory = CConst::OtherAntennaCategory; + } else { + rxAntennaCategory = CConst::UnknownAntennaCategory; + } + target.at(r).rxAntennaCategory = rxAntennaCategory; + + if (numPR) { + target.at(r).prType = std::vector(numPR); + target.at(r).prLatitudeDeg = std::vector(numPR); + target.at(r).prLongitudeDeg = std::vector(numPR); + target.at(r).prHeightAboveTerrainTx = std::vector(numPR); + target.at(r).prHeightAboveTerrainRx = std::vector(numPR); + + target.at(r).prTxGain = std::vector(numPR); + target.at(r).prTxAntennaDiameter = std::vector(numPR); + target.at(r).prRxGain = std::vector(numPR); + target.at(r).prRxAntennaDiameter = std::vector(numPR); + target.at(r).prAntCategory = std::vector( + numPR); + target.at(r).prAntModelName = std::vector(numPR); + + target.at(r).prReflectorHeight = std::vector(numPR); + target.at(r).prReflectorWidth = std::vector(numPR); + target.at(r).prAntenna = std::vector(numPR); + + QSqlQuery prQueryRes = SqlSelect(*db, "pr") + .cols(prColumns) + .where(QString("fsid=%1").arg(fsid)) + .run(); + + int querySize; + // resize vector to fit result + if (prQueryRes.driver()->hasFeature(QSqlDriver::QuerySize)) { + // if the driver supports .size() then use it because is is more + // efficient + querySize = prQueryRes.size(); + prQueryRes.setForwardOnly(true); + } else { + if (!prQueryRes.last()) { + querySize = 0; + } else { + querySize = prQueryRes.at() + 1; + prQueryRes.first(); + prQueryRes.previous(); + } + } + + if (querySize != numPR) { + throw std::runtime_error(ErrStream() + << "UlsDatabase.cpp: Inconsistent numPR " + "for FSID = " + << fsid); + } + + while (prQueryRes.next()) { + int prSeq = prQueryRes.value(prSeqIdx).toInt(); + int prIdx = prSeq - 1; + + target.at(r).prType[prIdx] = prQueryRes.value(prTypeIdx).isNull() ? + "" : + prQueryRes.value(prTypeIdx) + .toString() + .toStdString(); + target.at(r).prLatitudeDeg[prIdx] = + prQueryRes.value(pr_lat_degIdx).isNull() ? + quietNaN : + prQueryRes.value(pr_lat_degIdx).toDouble(); + target.at(r).prLongitudeDeg[prIdx] = + prQueryRes.value(pr_lon_degIdx).isNull() ? + quietNaN : + prQueryRes.value(pr_lon_degIdx).toDouble(); + target.at(r).prHeightAboveTerrainTx[prIdx] = + prQueryRes.value(pr_height_to_center_raat_tx_mIdx) + .isNull() ? + quietNaN : + prQueryRes.value(pr_height_to_center_raat_tx_mIdx) + .toDouble(); + target.at(r).prHeightAboveTerrainRx[prIdx] = + prQueryRes.value(pr_height_to_center_raat_rx_mIdx) + .isNull() ? + quietNaN : + prQueryRes.value(pr_height_to_center_raat_rx_mIdx) + .toDouble(); + + target.at(r).prTxGain[prIdx] = + prQueryRes.value(prTxGainIdx).isNull() ? + quietNaN : + prQueryRes.value(prTxGainIdx).toDouble(); + target.at(r).prTxAntennaDiameter[prIdx] = + prQueryRes.value(prTxDiameterIdx).isNull() ? + quietNaN : + prQueryRes.value(prTxDiameterIdx).toDouble(); + target.at(r).prRxGain[prIdx] = + prQueryRes.value(prRxGainIdx).isNull() ? + quietNaN : + prQueryRes.value(prRxGainIdx).toDouble(); + target.at(r).prRxAntennaDiameter[prIdx] = + prQueryRes.value(prRxDiameterIdx).isNull() ? + quietNaN : + prQueryRes.value(prRxDiameterIdx).toDouble(); + + std::string prAntCategoryStr = + prQueryRes.value(prAntCategoryIdx).toString().toStdString(); + CConst::AntennaCategoryEnum prAntCategory; + if (prAntCategoryStr == "B1") { + prAntCategory = CConst::B1AntennaCategory; + } else if (prAntCategoryStr == "HP") { + prAntCategory = CConst::HPAntennaCategory; + } else if (prAntCategoryStr == "OTHER") { + prAntCategory = CConst::OtherAntennaCategory; + } else { + prAntCategory = CConst::UnknownAntennaCategory; + } + target.at(r).prAntCategory[prIdx] = prAntCategory; + + target.at(r).prAntModelName[prIdx] = + prQueryRes.value(prAntModelNameIdx) + .toString() + .toStdString(); + + target.at(r).prReflectorHeight[prIdx] = + prQueryRes.value(prReflectorHeightIdx).isNull() ? + quietNaN : + prQueryRes.value(prReflectorHeightIdx).toDouble(); + target.at(r).prReflectorWidth[prIdx] = + prQueryRes.value(prReflectorWidthIdx).isNull() ? + quietNaN : + prQueryRes.value(prReflectorWidthIdx).toDouble(); + + int prAntennaIdxDB = prQueryRes.value(pr_ant_model_idxIdx).toInt(); + + antennaPattern = (AntennaClass *)NULL; + + if (prAntennaIdxDB != -1) { + if (antennaIdxMap[prAntennaIdxDB] == -1) { + antennaPattern = createAntennaPattern( + db, + prAntennaIdxDB, + antennaAOBList, + antennaNameList[prAntennaIdxDB]); + antennaIdxMap[prAntennaIdxDB] = antennaList.size(); + antennaList.push_back(antennaPattern); + } else { + antennaPattern = + antennaList[antennaIdxMap[prAntennaIdxDB]]; + } + } + target.at(r).prAntenna[prIdx] = antennaPattern; + } + } + } + LOGGER_DEBUG(logger) << target.size() << " rows retreived"; +} + +AntennaClass *UlsDatabase::createAntennaPattern(SqlScopedConnection &db, + int rxAntennaIdxDB, + std::vector antennaAOBList, + std::string antennaName) +{ + int numAntennaAOB = antennaAOBList.size(); + int idmin = numAntennaAOB * rxAntennaIdxDB; + int idmax = idmin + numAntennaAOB - 1; + QSqlQuery antgainQueryRes = + SqlSelect(*db, "antgain") + .cols(antgainColumns) + .where(QString("(id BETWEEN %1 AND %2)").arg(idmin).arg(idmax)) + .order("id") + .run(); + + int querySize; + // resize vector to fit result + if (antgainQueryRes.driver()->hasFeature(QSqlDriver::QuerySize)) { + // if the driver supports .size() then use it because is is more efficient + querySize = antgainQueryRes.size(); + antgainQueryRes.setForwardOnly(true); + } else { + if (!antgainQueryRes.last()) { + querySize = 0; + } else { + querySize = antgainQueryRes.at() + 1; + antgainQueryRes.first(); + antgainQueryRes.previous(); + } + } + + if (querySize != numAntennaAOB) { + LOGGER_DEBUG(logger) + << "ERROR Creating antenna " << antennaName + << ": numAntennaAOB = " << numAntennaAOB << ", querySize = " << querySize; + } + + std::vector> sampledData; + + std::tuple pt; + std::get<1>(pt) = quietNaN; + for (int aobIdx = 0; aobIdx < numAntennaAOB; ++aobIdx) { + std::get<0>(pt) = antennaAOBList[aobIdx]; + sampledData.push_back(pt); + } + + while (antgainQueryRes.next()) { + int id = antgainQueryRes.value(antgain_idIdx).toInt(); + double gain = antgainQueryRes.value(antgain_gainIdx).toDouble(); + int aobIdx = id - idmin; + std::get<1>(sampledData[aobIdx]) = gain; + } + + AntennaClass *antenna = new AntennaClass(CConst::antennaLUT_Boresight, antennaName.c_str()); + + LinInterpClass *gainTable = new LinInterpClass(sampledData); + + antenna->setBoresightGainTable(gainTable); + + return (antenna); +} diff --git a/src/afc-engine/UlsDatabase.h b/src/afc-engine/UlsDatabase.h new file mode 100644 index 0000000..6c5c2e1 --- /dev/null +++ b/src/afc-engine/UlsDatabase.h @@ -0,0 +1,224 @@ +// UlsDatabase.h: header file for reading ULS database data on startup +// author: Sam Smucny + +#ifndef AFCENGINE_ULS_DATABASE_H_ +#define AFCENGINE_ULS_DATABASE_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "denied_region.h" +#include "antenna.h" +#include "cconst.h" + +const int maxNumPR = 3; + +struct UlsRecord { + int fsid; + + std::string region; + std::string callsign; + int pathNumber; + std::string radioService; + std::string entityName; + std::string rxCallsign; + int rxAntennaNumber; + double startFreq, stopFreq; + double txLatitudeDeg, txLongitudeDeg; + double txGroundElevation; + std::string txPolarization; + double txGain; + double txEIRP; + double txHeightAboveTerrain; + std::string txArchitecture; + double azimuthAngleToTx; + double elevationAngleToTx; + double rxLatitudeDeg, rxLongitudeDeg; + double rxGroundElevation; + double rxHeightAboveTerrain; + double rxLineLoss; + double rxGain; + CConst::AntennaCategoryEnum rxAntennaCategory; + double rxAntennaDiameter; + double rxNearFieldAntDiameter; + double rxNearFieldDistLimit; + double rxNearFieldAntEfficiency; + + bool hasDiversity; + double diversityGain; + double diversityAntennaDiameter; + double diversityHeightAboveTerrain; + + std::string status; + bool mobile; + std::string rxAntennaModelName; + AntennaClass *rxAntenna; + int numPR; + std::vector prLatitudeDeg; + std::vector prLongitudeDeg; + std::vector prHeightAboveTerrainTx; + std::vector prHeightAboveTerrainRx; + std::vector prType; + + std::vector prTxGain; + std::vector prTxAntennaDiameter; + std::vector prRxGain; + std::vector prRxAntennaDiameter; + std::vector prAntCategory; + std::vector prAntModelName; + std::vector prAntenna; + + std::vector prReflectorHeight; + std::vector prReflectorWidth; +}; + +class UlsDatabase +{ + public: + UlsDatabase(); + ~UlsDatabase(); + + void nullInitialize(); + + // Loads all FS within lat/lon bounds + void loadUlsData(const QString &dbName, + std::vector &deniedRegionList, + std::vector &antennaList, + std::vector &target, + const double &minLat = -90, + const double &maxLat = 90, + const double &minLon = -180, + const double &maxLon = 180); + + // Loads a single FS by looking up its Id + void loadFSById(const QString &dbName, + std::vector &deniedRegionList, + std::vector &antennaList, + std::vector &target, + const int &fsid); + UlsRecord getFSById(const QString &dbName, + std::vector &deniedRegionList, + std::vector &antennaList, + const int &fsid) + { + // list of size 1 + auto list = std::vector(); + loadFSById(dbName, deniedRegionList, antennaList, list, fsid); + if (list.size() != 1) + throw std::runtime_error("FS not found"); + return list.at(0); + }; + + void fillTarget(SqlScopedConnection &db, + std::vector &deniedRegionList, + std::vector &antennaList, + std::vector &target, + QSqlQuery &ulsQueryRes); + + AntennaClass *createAntennaPattern(SqlScopedConnection &db, + int rxAntennaIdxDB, + std::vector antennaAOBList, + std::string antennaName); + + QStringList columns; + QStringList prColumns; + QStringList rasColumns; + QStringList antnameColumns; + QStringList antaobColumns; + QStringList antgainColumns; + + int fsidIdx; + int regionIdx; + int callsignIdx; + int pathNumberIdx; + int radio_serviceIdx; + int nameIdx; + int rx_callsignIdx; + int rx_antenna_numIdx; + int freq_assigned_start_mhzIdx; + int freq_assigned_end_mhzIdx; + int tx_lat_degIdx; + int tx_long_degIdx; + int tx_ground_elev_mIdx; + int tx_polarizationIdx; + int tx_gainIdx; + int tx_eirpIdx; + int tx_height_to_center_raat_mIdx; + int tx_architecture_mIdx; + int azimuth_angle_to_tx_mIdx; + int elevation_angle_to_tx_mIdx; + int rx_lat_degIdx; + int rx_long_degIdx; + int rx_ground_elev_mIdx; + int rx_height_to_center_raat_mIdx; + int rx_line_loss_mIdx; + int rx_gainIdx; + int rx_antennaDiameterIdx; + int rx_near_field_ant_diameterIdx; + int rx_near_field_dist_limitIdx; + int rx_near_field_ant_efficiencyIdx; + int rx_antennaCategoryIdx; + int statusIdx; + int mobileIdx; + int rx_ant_modelNameIdx; + int rx_ant_model_idxIdx; + int p_rp_numIdx; + + int rx_diversity_height_to_center_raat_mIdx; + int rx_diversity_gainIdx; + int rx_diversity_antennaDiameterIdx; + + int prSeqIdx; + int prTypeIdx; + int pr_lat_degIdx; + int pr_lon_degIdx; + int pr_height_to_center_raat_tx_mIdx; + int pr_height_to_center_raat_rx_mIdx; + + int prTxGainIdx; + int prTxDiameterIdx; + int prRxGainIdx; + int prRxDiameterIdx; + int prAntCategoryIdx; + int prAntModelNameIdx; + int pr_ant_model_idxIdx; + int prReflectorHeightIdx; + int prReflectorWidthIdx; + + int antname_ant_idxIdx; + int antname_ant_nameIdx; + + int antaob_aob_idxIdx; + int antaob_aob_degIdx; + + int antgain_idIdx; + int antgain_gainIdx; + + int ras_rasidIdx; + int ras_regionIdx; + int ras_nameIdx; + int ras_locationIdx; + int ras_startFreqMHzIdx; + int ras_stopFreqMHzIdx; + int ras_exclusionZoneIdx; + int ras_rect1lat1Idx; + int ras_rect1lat2Idx; + int ras_rect1lon1Idx; + int ras_rect1lon2Idx; + int ras_rect2lat1Idx; + int ras_rect2lat2Idx; + int ras_rect2lon1Idx; + int ras_rect2lon2Idx; + int ras_radiusKmIdx; + int ras_centerLatIdx; + int ras_centerLonIdx; + int ras_heightAGLIdx; +}; + +#endif /* AFCENGINE_ULS_DATABASE_H */ diff --git a/src/afc-engine/UlsMeasurementAnalysis.cpp b/src/afc-engine/UlsMeasurementAnalysis.cpp new file mode 100644 index 0000000..c367e22 --- /dev/null +++ b/src/afc-engine/UlsMeasurementAnalysis.cpp @@ -0,0 +1,678 @@ +#include "EcefModel.h" +#include "UlsMeasurementAnalysis.h" +#include "gdal_priv.h" +#include "cpl_conv.h" // for CPLMalloc() +#include +#include +#include +#include +#include +#include +#include "GdalHelpers.h" + +#include "cconst.h" +#include "EcefModel.h" +#include "UlsMeasurementAnalysis.h" +#include "gdal_priv.h" +#include "cpl_conv.h" // for CPLMalloc() + +extern void point_to_point(double elev[], + double tht_m, + double rht_m, + double eps_dielect, + double sgm_conductivity, + double eno_ns_surfref, + double frq_mhz, + int radio_climate, + int pol, + double conf, + double rel, + double &dbloss, + std::string &strmode, + int &errnum); + +namespace +{ +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "UlsMeasurementAnalysis") + +static bool itmInitFlag = true; +}; // end namespace + +namespace UlsMeasurementAnalysis +{ +void dumpHeightProfile(const char *prefix, const double *heights); + +QVector computeApproximateGreatCircleLine(const QPointF &from, + const QPointF &to, + int numpts, + double *tdist) +{ + QVector latlons; + latlons.fill(from, numpts); + + double delx = to.x() - from.x(); + double dely = to.y() - from.y(); + + // Do a straight linear interpolation, then use straight line distance. + for (int i = 1; i < numpts; ++i) { + double frac = i / double(numpts - 1); + latlons[i] += QPointF(delx * frac, dely * frac); + } + + // And distance. + if (tdist != NULL) { + // Vector3 fvec = EcefModel::geodeticToEcef(from.x(), from.y(), 0); + // Vector3 tvec = EcefModel::geodeticToEcef(to.x(), to.y(), 0); + // *tdist = (fvec - tvec).len(); + + double lon1Rad = from.y() * M_PI / 180.0; + double lat1Rad = from.x() * M_PI / 180.0; + double lon2Rad = to.y() * M_PI / 180.0; + double lat2Rad = to.x() * M_PI / 180.0; + double slat = sin((lat2Rad - lat1Rad) / 2); + double slon = sin((lon2Rad - lon1Rad) / 2); + *tdist = 2 * CConst::averageEarthRadius * + asin(sqrt(slat * slat + cos(lat1Rad) * cos(lat2Rad) * slon * slon)) * + 1.0e-3; + } + + return latlons; +} + +QVector computeGreatCircleLine(const QPointF &from, + const QPointF &to, + int numpts, + double *tdist) +{ + // We're going to do what the fortran program does. It's kinda stupid, though. + + // Please excuse fortran style variable names. I have decrypted them where possible. + double earthRad = CConst::averageEarthRadius / 1000.0; + + // double fLonRad, tLonRad; + double fLatRad; + double tLatRad; + + fLatRad = from.x() * M_PI / 180.0; + // fLonRad = from.y() * M_PI / 180.0; + + tLatRad = to.x() * M_PI / 180.0; + // tLonRad = to.y() * M_PI / 180.0; + + double deltaLat = to.x() - from.x(); + double aDeltaLat = fabs(deltaLat); + double deltaLon = to.y() - from.y(); + double aDeltaLon = fabs(deltaLon); + + // printf("deltaLon = %.8g, deltaLat = %.8g\n", deltaLon, deltaLat); + + double wLat, eLat; + + if (deltaLon > 0) { // Moving, uh, east? + wLat = fLatRad; + eLat = tLatRad; + } else { + wLat = tLatRad; + eLat = fLatRad; + } + + // printf("wLat,eLat = %.8g, %.8g\n", wLat, eLat); + + // Okay, now compute the azimuths at points w and e. + double sdLat, sdLon, sadLn; + sdLat = sin(0.5 * aDeltaLat * M_PI / 180.0); + sdLon = sin(0.5 * aDeltaLon * M_PI / 180.0); + sadLn = sin(aDeltaLon * M_PI / 180.0); + + double ceLat, cwLat; + cwLat = cos(wLat); + ceLat = cos(eLat); + + // printf("cwLat = %.8g; ceLat = %.8g\n", cwLat, ceLat); + + double P = 2.0 * (sdLat * sdLat + sdLon * sdLon * cwLat * ceLat); + + // printf("P = %.8g\n", P); + double sgc = sqrt(P * (2.0 - P)); + + // Continue computing azimuth... + sdLat = sin(eLat - wLat); + // printf("sdLat = %.8g\n", sdLat); + double cwaz = (2.0 * ceLat * sin(wLat) * sdLon * sdLon + sdLat) / sgc; + // printf("cwaz = %.8g\n", cwaz); + double swaz = (sadLn * ceLat) / sgc; + + double wAzimuth = atan2(swaz, cwaz) * 180 / M_PI; + double ceaz = (2.0 * cwLat * sin(eLat) * sdLon * sdLon - sdLat) / sgc; + // printf("ceaz = %.8g\n", ceaz); + double seaz = (sadLn * cwLat) / sgc; + // printf("seaz = %.9g\n", seaz); + double eAzimuth = atan2(seaz, ceaz) * 180 / M_PI; + eAzimuth = 360 - eAzimuth; + + double targetAz; // receiveAz; + + // printf("wAz = %.8g, eAz = %.8g\n", wAzimuth, eAzimuth); + + if (deltaLon < 0.0) { + targetAz = eAzimuth; + // receiveAz = wAzimuth; + } else { + targetAz = wAzimuth; + // receiveAz = eAzimuth; + } + + // And finish the great circle. + + double cgc = 1.0 - P; + + double greatCircleAngle = atan2(sgc, cgc); + double greatCircleDistance = greatCircleAngle * earthRad; + + if (tdist != NULL) + *tdist = greatCircleDistance; + + // printf("Computed that distance is %.8g km; azimuth = %.8g\n", greatCircleDistance, + // targetAz); + + // Okay, done with that. Now we need to interpolate along the great circle. + // We use some of the values computed above; specifically, targetAz. + // We already have tLatRad and tLonRad from above. + // We will be interpolating along greatCircleDistance and converting distances along the + // great circle into lat/lons. + QVector latlons; + latlons.fill(QPointF(0, 0), numpts); + latlons[0] = from; + + double delta = greatCircleDistance / (numpts - 1); + + double coLat = M_PI / 2.0 - fLatRad; + double cosco, sinco; + sincos(coLat, &sinco, &cosco); + + double cosTargetAz = cos(targetAz * M_PI / 180.0); + double fromY = from.y(); + + QVector::iterator it = latlons.begin(); + ++it; + + for (int i = 2; i < numpts; ++i, ++it) { + double tgcDist = (i - 1) * delta; + // printf("tgcDist = %.8g * %.8g = %.8g km away\n", (i + 1) / (double)numpts, + // greatCircleDistance, tgcDist); + double tgc = tgcDist / earthRad; + + // printf("END POINT: %.8g, %.8g, %.8g, %.8g\n", tLatRad, tLonRad, targetAz * M_PI / + // 180.0, tgc); + + // Now through cosines, sines, ... to get the distance. + double cosgc, singc; + sincos(tgc, &singc, &cosgc); + + // printf("cos/sin co/gc = %.8g, %.8g, %.8g, %.8g\n", cosco, sinco, cosgc, singc); + double cosb = cosco * cosgc + sinco * singc * cosTargetAz; + // printf("cosb = %.8g\n", cosb); + double arg = 1.0 - cosb * cosb; + if (arg < 0.0) + arg = 0.0; + double B = atan2(sqrt(arg), cosb); + double arc = (cosgc - cosco * cosb) / (sinco * sin(B)); + arg = 1.0 - arc * arc; + if (arg < 0.0) + arg = 0.0; + + // And finally compute the end point distances. + double rdLon = atan2(sqrt(arg), arc); + double zrLat = ((M_PI / 2.0) - fabs(B)) * 180 / M_PI; + // printf("zrLat = %.8g, B=%.8g\n", zrLat, B); + if (cosb < 0.0) + zrLat = -zrLat; + double zrLon; + + if (targetAz > 180.0) + zrLon = fromY - fabs(rdLon) * 180 / M_PI; + else + zrLon = fromY + fabs(rdLon) * 180 / M_PI; + + // printf("Results in %.8g, %.8g\n", zrLat, zrLon); + + it->setX(zrLat); + it->setY(zrLon); + } + + *it = to; + + return latlons; +} + +QVector computeGreatCircleLineMM(const QPointF &from, + const QPointF &to, + int numpts, + double *tdist) +{ + double lon1Rad = from.y() * M_PI / 180.0; + double lat1Rad = from.x() * M_PI / 180.0; + double lon2Rad = to.y() * M_PI / 180.0; + double lat2Rad = to.x() * M_PI / 180.0; + double slat = sin((lat2Rad - lat1Rad) / 2); + double slon = sin((lon2Rad - lon1Rad) / 2); + *tdist = 2 * CConst::averageEarthRadius * + asin(sqrt(slat * slat + cos(lat1Rad) * cos(lat2Rad) * slon * slon)) * 1.0e-3; + + Vector3 posn1 = Vector3(cos(lat1Rad) * cos(lon1Rad), + cos(lat1Rad) * sin(lon1Rad), + sin(lat1Rad)); + Vector3 posn2 = Vector3(cos(lat2Rad) * cos(lon2Rad), + cos(lat2Rad) * sin(lon2Rad), + sin(lat2Rad)); + + double dotprod = posn1.dot(posn2); + if (dotprod > 1.0) { + dotprod = 1.0; + } else if (dotprod < -1.0) { + dotprod = -1.0; + } + + double greatCircleAngle = acos(dotprod); + + Vector3 uVec = (posn1 + posn2).normalized(); + Vector3 wVec = posn1.cross(posn2).normalized(); + Vector3 vVec = wVec.cross(uVec); + + QVector latlons(numpts); + + for (int ptIdx = 0; ptIdx < numpts; ++ptIdx) { + double theta_i = (greatCircleAngle * (2 * ptIdx - (numpts - 1))) / + (2 * (numpts - 1)); + Vector3 posn_i = uVec * cos(theta_i) + vVec * sin(theta_i); + double lon_i = atan2(posn_i.y(), posn_i.x()); + double lat_i = atan2(posn_i.z(), posn_i.x() * cos(lon_i) + posn_i.y() * sin(lon_i)); + + latlons[ptIdx] = QPointF(lat_i * 180.0 / M_PI, lon_i * 180.0 / M_PI); + } + + return latlons; +} + +QVector computePartialGreatCircleLine(const QPointF &from, + const QPointF &to, + int numptsTotal, + int numptsPartial, + double *tdist) +{ + QVector latlonGc = computeGreatCircleLine(from, to, numptsPartial + 1, tdist); + + for (int p = 0; p < latlonGc.count(); ++p) { + qDebug() << "partial = " << latlonGc[p].x() << latlonGc[p].y(); + } + + /// Now fill in between the lines using approximation. + QVector ret; + ret.reserve(numptsTotal); + int numPerStep = numptsTotal / numptsPartial; + int remainingPoints = numptsTotal; + + for (int i = 0; i < numptsPartial; ++i) { + const QPointF &f = latlonGc[i], &t = latlonGc[i + 1]; + int thisStepCount = numPerStep; + if (remainingPoints < thisStepCount) + thisStepCount = remainingPoints; + else + remainingPoints -= thisStepCount; + QVector r = computeApproximateGreatCircleLine(f, + t, + thisStepCount + 1, + NULL); + + for (int pr = 0; pr < r.count(); ++pr) { + qDebug() << " partial from " << f.x() << f.y() << " to " << t.x() << t.y() + << " [" << pr << "] = " << r[pr].x() << r[pr].y(); + } + + const int thisCnt = r.count(); + for (int p = 1; p < thisCnt; ++p) { + ret << r[p]; + } + } + + return ret; +} + +double *computeElevationVector(const TerrainClass *terrain, + bool includeBldg, + bool cdsmFlag, + const QPointF &from, + const QPointF &to, + int numpts, + double *cdsmFracPtr) +{ + double tdist; + double terrainHeight, bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum heightSource; + + // QVector latlons = computeApproximateGreatCircleLine(from, to, numpts, &tdist); + QVector latlons = computeGreatCircleLineMM(from, to, numpts, &tdist); + + double bldgDistRes = 1.0; // 1 meter + int maxBldgStep = std::min(100, (int)floor(tdist * 1000 / bldgDistRes)); + int stepIdx; + int numBldgPtTX = 0; + int numBldgPtRX = 0; + + // printf("Returned %d points instead of %d.\n", latlons.count(), numpts); + + double *ret = (double *)calloc(sizeof(double), numpts + 2); + ret[0] = numpts - 1; + ret[1] = (tdist / (numpts - 1)) * 1000.0; + + if (includeBldg) { + /*********************************************************************************************************/ + // Compute numBldgPtTX so building at TX can be removed + /*********************************************************************************************************/ + const QPointF &ptTX = latlons.at(0); + terrain->getTerrainHeight(ptTX.y(), + ptTX.x(), + terrainHeight, + bldgHeight, + lidarHeightResult, + heightSource); + if (lidarHeightResult == MultibandRasterClass::BUILDING) { + bool found = false; + for (stepIdx = 1; (stepIdx < maxBldgStep) && (!found); stepIdx++) { + double ptIdxDbl = stepIdx * bldgDistRes / ret[1]; + int n0 = (int)floor(ptIdxDbl); + int n1 = n0 + 1; + double ptx = latlons.at(n0).x() * (n1 - ptIdxDbl) + + latlons.at(n1).x() * (ptIdxDbl - n0); + double pty = latlons.at(n0).y() * (n1 - ptIdxDbl) + + latlons.at(n1).y() * (ptIdxDbl - n0); + terrain->getTerrainHeight(pty, + ptx, + terrainHeight, + bldgHeight, + lidarHeightResult, + heightSource); + if (lidarHeightResult != MultibandRasterClass::BUILDING) { + found = true; + numBldgPtTX = n1; + } + } + if (!found) { + numBldgPtTX = (int)floor(maxBldgStep * bldgDistRes / ret[1]); + } + } else { + numBldgPtTX = 0; + } + /*********************************************************************************************************/ + + /*********************************************************************************************************/ + // Compute numBldgPtRX so building at RX can be removed + /*********************************************************************************************************/ + const QPointF &ptRX = latlons.at(numpts - 1); + terrain->getTerrainHeight(ptRX.y(), + ptRX.x(), + terrainHeight, + bldgHeight, + lidarHeightResult, + heightSource); + if (lidarHeightResult == MultibandRasterClass::BUILDING) { + bool found = false; + for (stepIdx = 1; (stepIdx < maxBldgStep) && (!found); stepIdx++) { + double ptIdxDbl = (tdist * 1000 - stepIdx * bldgDistRes) / ret[1]; + int n0 = (int)floor(ptIdxDbl); + int n1 = n0 + 1; + double ptx = latlons.at(n0).x() * (n1 - ptIdxDbl) + + latlons.at(n1).x() * (ptIdxDbl - n0); + double pty = latlons.at(n0).y() * (n1 - ptIdxDbl) + + latlons.at(n1).y() * (ptIdxDbl - n0); + terrain->getTerrainHeight(pty, + ptx, + terrainHeight, + bldgHeight, + lidarHeightResult, + heightSource); + if (lidarHeightResult != MultibandRasterClass::BUILDING) { + found = true; + numBldgPtRX = numpts - n1; + } + } + if (!found) { + numBldgPtRX = (int)floor(maxBldgStep * bldgDistRes / ret[1]); + } + } else { + numBldgPtRX = 0; + } + /*********************************************************************************************************/ + } + + int cdsmCount = 0; + double *pos = ret + 2; + for (int i = 0; i < numpts; ++i, ++pos) { + const QPointF &pt = latlons.at(i); + bool cdsmFlagVal = (((i == 0) || (i == numpts - 1)) ? false : cdsmFlag); + + terrain->getTerrainHeight(pt.y(), + pt.x(), + terrainHeight, + bldgHeight, + lidarHeightResult, + heightSource, + cdsmFlagVal); + if (includeBldg && (lidarHeightResult == MultibandRasterClass::BUILDING) && + (i >= numBldgPtTX) && (i <= numpts - 1 - numBldgPtRX)) { + *pos = terrainHeight + bldgHeight; + } else { + *pos = terrainHeight; + } + + if (heightSource == CConst::cdsmHeightSource) { + cdsmCount++; + } + } + if (cdsmFracPtr) { + if (numpts < 3) { + *cdsmFracPtr = 0.0; + } else { + *cdsmFracPtr = cdsmCount / (numpts - 2); + } + } + + // print_values(ret, 2, numpts + 2, 8); + + return ret; +} + +double greatCircleDistance(double lat1, double lon1, double lat2, double lon2) +{ + double lat1rad = deg2rad(lat1); + double lon1rad = deg2rad(lon1); + double lat2rad = deg2rad(lat2); + double lon2rad = deg2rad(lon2); + // double deltalat = lat2rad - lat1rad; + double deltalon = lon2rad - lon1rad; + + // law of cosines + // inaccurate for small distances? + double centerAngle = std::acos(std::sin(lat1rad) * std::sin(lat2rad) + + std::cos(lat1rad) * std::cos(lat2rad) * std::cos(deltalon)); + /*double hav = 2*std::asin( + std::sqrt(sqr(std::sin(deltalat/2)) + + std::cos(lat1rad) * + std::cos(lat2rad) * + sqr(std::sin(deltalon/2)))); + std::cout << "hav: " << rad2deg(hav) << std::endl;*/ + return rad2deg(centerAngle); +} + +void computeElevationVector(const TerrainClass *terrain, + bool includeBldg, + const QPointF &from, + const QPointF &to, + int numpts, + std::vector &hi, + std::vector &di, + double ae) +{ + // For compatability with P.452 + double tdist; + QVector latlons = computeApproximateGreatCircleLine(from, to, numpts, &tdist); + + // printf("Returned %d points instead of %d.\n", latlons.count(), numpts); + + hi.resize(numpts); + + for (int i = 0; i < numpts; i++) { + const QPointF &pt = latlons.at(i); + + double terrainHeight, bldgHeight; + MultibandRasterClass::HeightResult lidarHeightResult; + CConst::HeightSourceEnum heightSource; + terrain->getTerrainHeight(pt.y(), + pt.x(), + terrainHeight, + bldgHeight, + lidarHeightResult, + heightSource); + + if (includeBldg && (lidarHeightResult == MultibandRasterClass::BUILDING)) { + // CORE DUMP, not yet implemented, need to remove building at TX and RX + // locations + printf("%d", *((int *)NULL)); + hi[i] = terrainHeight + bldgHeight; + } else { + hi[i] = terrainHeight; + } + } + + double angularDistance = greatCircleDistance(from.x(), from.y(), to.x(), to.y()); + double pathDistance = deg2rad(angularDistance) * ae; + + for (int i = 0; i < numpts; i++) { + di[i] = (pathDistance * i) / (numpts - 1); + } + + return; +} + +double runPointToPoint(const TerrainClass *terrain, + bool includeBldg, + QPointF transLocLatLon, + double transHt, + QPointF receiveLocLatLon, + double receiveHt, + double lineOfSightDistanceKm, + double eps_dielect, + double sgm_conductivity, + double eno_ns_surfref, + double frq_mhz, + int radio_climate, + int pol, + double conf, + double rel, + int numpts, + char *prefix, + double **heightProfilePtr) +{ + if (!(*heightProfilePtr)) { + *heightProfilePtr = computeElevationVector(terrain, + includeBldg, + false, + transLocLatLon, + receiveLocLatLon, + numpts, + (double *)NULL); + } + + double rv; + std::string strmode; + // char strmode[50]; + int errnum; + + if (itmInitFlag) { + LOGGER_INFO(logger) << "ITM Parameter: eps_dielect = " << eps_dielect; + LOGGER_INFO(logger) << "ITM Parameter: sgm_conductivity = " << sgm_conductivity; + LOGGER_INFO(logger) << "ITM Parameter: pol = " << pol; + itmInitFlag = false; + } + point_to_point(*heightProfilePtr, + transHt, + receiveHt, + eps_dielect, + sgm_conductivity, + eno_ns_surfref, + frq_mhz, + radio_climate, + pol, + conf, + rel, + rv, + strmode, + errnum); + // qDebug() << " point_to_point" << rv << strmode << errnum; + + if (prefix != NULL) { + dumpHeightProfile(prefix, *heightProfilePtr); + } + + terrain->numITM++; + + return rv; +} + +bool isLOS(const TerrainClass *terrain, + QPointF transLocLatLon, + double transHt, + QPointF receiveLocLatLon, + double receiveHt, + double lineOfSightDistanceKm, + int numpts, + double **heightProfilePtr, + double *cdsmFracPtr) +{ + if (!(*heightProfilePtr)) { + *heightProfilePtr = computeElevationVector(terrain, + true, + true, + transLocLatLon, + receiveLocLatLon, + numpts, + cdsmFracPtr); + } + + double txHeightAMSL = (*heightProfilePtr)[2] + transHt; + double rxHeightAMSL = (*heightProfilePtr)[2 + numpts - 1] + receiveHt; + + int ptIdx; + bool losFlag = true; + for (ptIdx = 0; (ptIdx < numpts) && (losFlag); ++ptIdx) { + double ptHeight = (*heightProfilePtr)[2 + ptIdx]; + double signalHeight = (txHeightAMSL * (numpts - 1 - ptIdx) + rxHeightAMSL * ptIdx) / + (numpts - 1); + + double clearance = signalHeight - ptHeight; + + if (clearance < 0.0) { + losFlag = false; + } + } + + return losFlag; +} + +/******************************************************************************************/ + +void dumpHeightProfile(const char *prefix, const double *heights) +{ + int numHeights = int(heights[0]); + + for (int x = 0; x < numHeights; ++x) { + double ht = heights[2 + x]; + qDebug() << "HEIGHTPROFILE" << prefix << ht; + } +} + +long long numInvalidSRTM; +long long numSRTM; +} diff --git a/src/afc-engine/UlsMeasurementAnalysis.h b/src/afc-engine/UlsMeasurementAnalysis.h new file mode 100644 index 0000000..96ea526 --- /dev/null +++ b/src/afc-engine/UlsMeasurementAnalysis.h @@ -0,0 +1,67 @@ +#ifndef ULS_MEASUREMENT_ANALYSIS_H +#define ULS_MEASUREMENT_ANALYSIS_H + +#include +#include +#include +#include "terrain.h" + +namespace UlsMeasurementAnalysis +{ +int analyzeMeasurementSites(QString fullCmd, int subArgc, char **subArgv); +int analyzeMeasurementBubble(QString fullCmd, int subArgc, char **subArgv); +int analyzeApproximatePathDifferences(QString fullCmd, int subArgc, char **subArgv); + +double *computeElevationVector(const TerrainClass *terrain, + bool includeBldg, + bool cdsmFlag, + const QPointF &from, + const QPointF &to, + int numpts, + double *cdsmFracPtr); + +void computeElevationVector(const TerrainClass *terrain, + bool includeBldg, + bool cdsmFlag, + const QPointF &from, + const QPointF &to, + int numpts, + std::vector &hi, + std::vector &di, + double ae); + +double runPointToPoint(const TerrainClass *terrain, + bool includeBldg, + QPointF transLocLatLon, + double transHt, + QPointF receiveLocLatLon, + double receiveHt, + double lineOfSightDistanceKm, + double eps_dielect, + double sgm_conductivity, + double eno_ns_surfref, + double frq_mhz, + int radio_climate, + int pol, + double conf, + double rel, + int numpts, + char *prefix, + double **heightProfilePtr); + +bool isLOS(const TerrainClass *terrain, + QPointF transLocLatLon, + double transHt, + QPointF receiveLocLatLon, + double receiveHt, + double lineOfSightDistanceKm, + int numpts, + double **heightProfilePtr, + double *cdsmFracPtr); + +extern long long numInvalidSRTM; +extern long long numSRTM; + +}; + +#endif diff --git a/src/afc-engine/Vector3.h b/src/afc-engine/Vector3.h new file mode 100644 index 0000000..87cbcef --- /dev/null +++ b/src/afc-engine/Vector3.h @@ -0,0 +1,244 @@ +#ifndef VECTOR_H +#define VECTOR_H + +#include +#include +#include + +#include "MathHelpers.h" + +using namespace MathHelpers; + +/** + * A class representing a 3-Dimensional Vector in floating point precision + * + */ +class Vector3 +{ + public: + /** + * Construct a vector from its 3 coordinates + * + * @param x X coordinate to set + * @param y Y coordinate to set + * @param z Z coordinate to set + */ + Vector3(double xVal = 0.0, double yVal = 0.0, double zVal = 0.0) + { + data[0] = xVal; + data[1] = yVal; + data[2] = zVal; + } + + /** + * Copy constructor + * + * @param other Vector to copy + */ + Vector3(const Vector3 &other) + { + data[0] = other.data[0]; + data[1] = other.data[1]; + data[2] = other.data[2]; + } + + /** + * Construct Vector from underlying armadillo vector + * + * @param vec Armadillo 3 point vector + */ + // Vector3(const arma::vec3 &vec) { + // data[0] = vec[0]; + // data[1] = vec[1]; + // data[2] = vec[2]; + // } + + /** + * Get the x coordinate + * + * @return x coordinate + */ + inline double x() const + { + return data[0]; + } + + /** + * Get the y coordinate + * + * @return y coordinate + */ + inline double y() const + { + return data[1]; + } + + /** + * Get the z coordinate + * + * @return z coordinate + */ + inline double z() const + { + return data[2]; + } + + inline void normalize() + { + double l = len(); + data[0] /= l; + data[1] /= l; + data[2] /= l; + } + + /** + * Perform a cross-product of vector with other vector and return resulting vector + * + * @param other The other vector to perform cross product with + * + * @return The resulting vector + */ + inline Vector3 cross(const Vector3 &other) const + { + return Vector3(data[1] * other.data[2] - data[2] * other.data[1], + data[2] * other.data[0] - data[0] * other.data[2], + data[0] * other.data[1] - data[1] * other.data[0]); + } + + /** + * Perform a dot-product of vector with other vector and return result + * + * @param other The other vector to perform dot product with + * + * @return The result of dot product + */ + inline double dot(const Vector3 &other) const + { + return data[0] * other.data[0] + data[1] * other.data[1] + + data[2] * other.data[2]; + } + + /** + * Perform a dot-product of vector with other vector after normalizing each + * to have length one + * + * @param other The other vector to perform dot product with + * + * @return The result of the dot product + */ + inline double normDot(const Vector3 &other) const + { + double l1 = len(); + double l2 = other.len(); + return dot(other) / (l1 * l2); + } + + /** + * The total length of this vector as a 3-D cartesian vector + * + * @return The length + */ + inline double len() const + { + return sqrt(sqr(data[0]) + sqr(data[1]) + sqr(data[2])); + } + + /** + * Get a copy of this vector but normalized to have length of one + * + * @return normalize vector + */ + inline Vector3 normalized() const + { + double l = len(); + return Vector3(data[0] / l, data[1] / l, data[2] / l); + } + + /** + * The angle between this vector and other vector + * + * @param other The other vector + * + * @return angle in radians + */ + inline double angleBetween(const Vector3 &other) const + { + double dpVal = normDot(other); + if (dpVal > 1.0) { + dpVal = 1.0; + } else if (dpVal < -1.0) { + dpVal = -1.0; + } + return acos(dpVal); + } + + inline Vector3 operator+(const Vector3 &other) const + { + return Vector3(data[0] + other.data[0], + data[1] + other.data[1], + data[2] + other.data[2]); + } + + inline Vector3 operator-(const Vector3 &other) const + { + return Vector3(data[0] - other.data[0], + data[1] - other.data[1], + data[2] - other.data[2]); + } + + inline Vector3 operator-() const + { + return Vector3(-data[0], -data[1], -data[2]); + } + + // inline Vector3 operator * (const Vector3 &other) const { + // return Vector3(data % other.data); + // } + + // inline Vector3 operator / (const Vector3 &other) const { + // return Vector3(data / other.data); + // } + + inline Vector3 operator*(const double scalar) const + { + return Vector3(data[0] * scalar, data[1] * scalar, data[2] * scalar); + } + + inline friend Vector3 operator*(const double scalar, const Vector3 &vector) + { + return vector * scalar; + } + + inline Vector3 operator=(const Vector3 &other) + { + data[0] = other.data[0]; + data[1] = other.data[1]; + data[2] = other.data[2]; + return *this; + } + + inline bool operator==(const Vector3 &other) + { + bool retval = (data[0] == other.data[0]) && (data[1] == other.data[1]) && + (data[2] == other.data[2]); + + return retval; + } + friend std::ostream &operator<<(std::ostream &os, const Vector3 &vector) + { + return os << "(" << vector.x() << ", " << vector.y() << ", " << vector.z() + << ")"; + } + + friend QDebug operator<<(QDebug stream, const Vector3 &vector) + { + stream.nospace() << "(" << vector.x() << ", " << vector.y() << ", " + << vector.z() << ")"; + return stream.space(); + } + + protected: + double data[3]; +}; + +#endif diff --git a/src/afc-engine/antenna.cpp b/src/afc-engine/antenna.cpp new file mode 100644 index 0000000..4eb73fe --- /dev/null +++ b/src/afc-engine/antenna.cpp @@ -0,0 +1,425 @@ +/******************************************************************************************/ +/**** FILE: antenna.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cconst.h" +#include "antenna.h" +#include "spline.h" +#include "global_fn.h" +#include "list.h" +#include "lininterp.h" +#include "afclogging/Logging.h" +#include "afclogging/ErrStream.h" +#include "AfcDefinitions.h" + +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "AntennaClass") + +/******************************************************************************************/ +/**** FUNCTION: AntennaClass::AntennaClass ****/ +/******************************************************************************************/ +AntennaClass::AntennaClass(int p_type, const char *p_strid) +{ + if (p_strid) { + strid = strdup(p_strid); + } else { + strid = (char *)NULL; + } + type = p_type; + + is_omni = (type == CConst::antennaOmni ? 1 : 0); + tilt_rad = quietNaN; + gain_fwd_db = quietNaN; + gain_back_db = quietNaN; + horizGainTable = (LinInterpClass *)NULL; + vertGainTable = (LinInterpClass *)NULL; + offBoresightGainTable = (LinInterpClass *)NULL; + + h_width = 360.0; + vg0 = quietNaN; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AntennaClass::~AntennaClass ****/ +/******************************************************************************************/ +AntennaClass::~AntennaClass() +{ + if (strid) { + free(strid); + } + if (horizGainTable) { + delete horizGainTable; + } + if (vertGainTable) { + delete vertGainTable; + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AntennaClass:: "get_" functions ****/ +/******************************************************************************************/ +char *AntennaClass::get_strid() +{ + return (strid); +} +int AntennaClass::get_type() +{ + return (type); +} +int AntennaClass::get_is_omni() +{ + return (is_omni); +} + +void AntennaClass::setBoresightGainTable(LinInterpClass *offBoresightGainTableVal) +{ + offBoresightGainTable = offBoresightGainTableVal; +} + +/******************************************************************************************/ +std::vector AntennaClass::readMultipleBoresightAntennas(std::string filename) +{ + int i, fieldIdx, antIdx; + double x_start, x_stop, u, xval, phase0, phaseRad; + char *chptr; + FILE *fp; + DblDblClass pt; + std::ostringstream errStr; + + if (filename.empty()) { + throw std::runtime_error(ErrStream() + << "ERROR: No multiple boresight antenna file specified"); + } + + if (!(fp = fopen(filename.c_str(), "rb"))) { + throw std::runtime_error(ErrStream() << "ERROR: Unable to open multiple boresight " + "antenna file \"" + << filename << "\""); + } + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + LOGGER_INFO(logger) << "Reading multiple boresight antenna file: " << filename; + + int linenum = 0; + bool foundLabelLine = false; + + std::vector antennaList; + + std::vector *> lutGainList; + + std::string line; + + while (fgetline(fp, line, false)) { + linenum++; + std::vector fieldList = splitCSV(line); + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + int fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if ((lineType == unknownLineType) && (!foundLabelLine)) { + lineType = labelLineType; + foundLabelLine = 1; + } + if ((lineType == unknownLineType) && (foundLabelLine)) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + std::string field; + switch (lineType) { + case labelLineType: + for (fieldIdx = 0; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + if (fieldIdx == 0) { + if (field != "Off-axis angle (deg)") { + throw std::runtime_error( + ErrStream() + << "ERROR: Invalid antenna data " + "file \"" + << filename << "(" << linenum + << ")\" invalid \"Off-axis angle " + "(deg)\" label = " + << field); + } + } else { + ListClass *lutGain = + new ListClass(0); + lutGainList.push_back(lutGain); + AntennaClass *antenna = new AntennaClass( + CConst::antennaLUT_Boresight, + field.c_str()); + antennaList.push_back(antenna); + } + } + break; + case dataLineType: + phaseRad = strtod(fieldList.at(0).c_str(), &chptr) * M_PI / 180; + for (fieldIdx = 1; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + double gainVal = strtod(fieldList.at(fieldIdx).c_str(), + &chptr); + lutGainList[fieldIdx - 1]->append( + DblDblClass(phaseRad, gainVal)); + } + break; + + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + throw std::runtime_error( + ErrStream() << "ERROR reading Antenna File: lineType = " + << lineType << " INVALID value"); + break; + } + } + + if (fp) { + fclose(fp); + } + + for (antIdx = 0; antIdx < static_cast(lutGainList.size()); antIdx++) { + SplineClass *spline = new SplineClass(lutGainList[antIdx]); + + ListClass *sampledData = new ListClass(0); + + x_start = 0; + x_stop = M_PI; + + phase0 = (*lutGainList[antIdx])[0].x(); + for (i = 0; i <= CConst::antenna_num_interp_pts - 1; i++) { + u = (double)i / (CConst::antenna_num_interp_pts - 1); + xval = x_start * (1.0 - u) + x_stop * u; + pt.setX(xval); + while (xval >= phase0 + 2 * M_PI) { + xval -= 2 * M_PI; + } + while (xval < phase0) { + xval += 2 * M_PI; + } + pt.setY(spline->splineval(xval)); + sampledData->append(pt); + } + + LinInterpClass *gainTable = new LinInterpClass(sampledData); + + antennaList[antIdx]->setBoresightGainTable(gainTable); + + delete spline; + delete sampledData; + delete lutGainList[antIdx]; + } + + return (antennaList); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AntennaClass::gainDB ****/ +/**** This routine computes the antenna power gain for the specified sectorted antenna ****/ +/**** in the direction of the vector(dx, dy, dz). ****/ +/******************************************************************************************/ +double AntennaClass::gainDB(double dx, double dy, double dz, double h_angle_rad) +{ + double theta = 0.0; + double phi = 0.0; + double gain_db = 0.0; + + if (type == CConst::antennaOmni) { + gain_db = 0.0; + } else if (type == CConst::antennaLUT_H) { + phi = atan2(dy, dx); + phi -= h_angle_rad; + while (phi >= M_PI) { + phi -= 2 * M_PI; + } + while (phi < -M_PI) { + phi += 2 * M_PI; + } + gain_db = horizGainTable->lininterpval(phi); + } else if (type == CConst::antennaLUT_V) { + theta = atan2(dz, sqrt(dx * dx + dy * dy)); + gain_db = vertGainTable->lininterpval(theta); + } else if (type == CConst::antennaLUT) { + phi = atan2(dy, dx); + phi -= h_angle_rad; + while (phi >= M_PI) { + phi -= 2 * M_PI; + } + while (phi < -M_PI) { + phi += 2 * M_PI; + } + theta = atan2(dz, sqrt(dx * dx + dy * dy)); + + double pi_minus_theta = M_PI - theta; + while (pi_minus_theta >= M_PI) { + pi_minus_theta -= 2 * M_PI; + } + + double gv1 = vertGainTable->lininterpval(theta); + double gv2 = vertGainTable->lininterpval(pi_minus_theta); + double gh = horizGainTable->lininterpval(phi); + + gain_db = (1.0 - fabs(phi) / M_PI) * (gv1 - gain_fwd_db) + + (fabs(phi) / M_PI) * (gv2 - gain_back_db) + gh; + } else { + throw std::runtime_error(ErrStream() << "ERROR in AntennaClass::gainDB: type = " + << type << " INVALID value"); + } + + return (gain_db); +} +/******************************************************************************************/ +/**** FUNCTION: AntennaClass::gainDB ****/ +/**** This routine computes the antenna power gain for the specified sectorted antenna ****/ +/**** in the direction of phi, theta. ****/ +/******************************************************************************************/ +double AntennaClass::gainDB(double phi, double theta) +{ + double gain_db = 0.0; + + if (type == CConst::antennaOmni) { + gain_db = 0.0; + } else if (type == CConst::antennaLUT_H) { + while (phi >= M_PI) { + phi -= 2 * M_PI; + } + while (phi < -M_PI) { + phi += 2 * M_PI; + } + gain_db = horizGainTable->lininterpval(phi); + } else if (type == CConst::antennaLUT_V) { + gain_db = vertGainTable->lininterpval(theta); + } else if (type == CConst::antennaLUT) { + while (phi >= M_PI) { + phi -= 2 * M_PI; + } + while (phi < -M_PI) { + phi += 2 * M_PI; + } + + double pi_minus_theta = M_PI - theta; + while (pi_minus_theta >= M_PI) { + pi_minus_theta -= 2 * M_PI; + } + + double gv1 = vertGainTable->lininterpval(theta); + double gv2 = vertGainTable->lininterpval(pi_minus_theta); + double gh = horizGainTable->lininterpval(phi); + + gain_db = (1.0 - fabs(phi) / M_PI) * (gv1 - gain_fwd_db) + + (fabs(phi) / M_PI) * (gv2 - gain_back_db) + gh; + } else { + throw std::runtime_error(ErrStream() << "ERROR in AntennaClass::gainDB: type = " + << type << " INVALID value"); + } + + return (gain_db); +} +/******************************************************************************************/ +/**** FUNCTION: AntennaClass::gainDB ****/ +/**** This routine computes the antenna power gain for the specified sectorted antenna ****/ +/**** for an angle off boresight of theta. ****/ +/******************************************************************************************/ +double AntennaClass::gainDB(double theta) +{ + double gain_db = 0.0; + + if (type == CConst::antennaLUT_Boresight) { + gain_db = offBoresightGainTable->lininterpval(theta); + } else { + throw std::runtime_error(ErrStream() << "ERROR in AntennaClass::gainDB: type = " + << type << " INVALID value"); + } + + return (gain_db); +} +/******************************************************************************************/ +/**** FUNCTION: check_antenna_gain ****/ +/**** This routine writes antenna gain in two column format to the specified file. ****/ +/**** The first column is angle in degrees from -180 to 180 and the second column is ****/ +/**** antenna gain in dB. The purpose of this routine is to privide a means of ****/ +/**** verifying the integrity of the spline interpolation used on the antenna data. ****/ +/**** orient == 0 : Horizontal pattern ****/ +/**** orient == 1 : Vertical pattern ****/ +/******************************************************************************************/ +int AntennaClass::checkGain(const char *flname, int orient, int numpts) +{ + int i; + double phase_deg, phase_rad, dx, dy, dz, gain_db; + FILE *fp; + + if (numpts <= 0) { + throw std::runtime_error(ErrStream() + << "ERROR in routine check_antenna_gain(), numpts = " + << numpts << " must be > 0"); + } + + if (!flname) { + throw std::runtime_error(ErrStream() << "ERROR in routine check_antenna_gain(), No " + "filename specified"); + } + + if (!(fp = fopen(flname, "w"))) { + throw std::runtime_error(ErrStream() << "ERROR in routine check_antenna_gain(), " + "unable to write to file \"" + << flname << "\""); + } + + LOGGER_INFO(logger) << "Checking " << (orient == 0 ? "HORIZONTAL" : "VERTICAL") + << " antenna gain. Writing " << numpts << " points to file \"" + << flname << "\""; + + for (i = 0; i <= numpts - 1; i++) { + phase_deg = -180.0 + 360.0 * i / numpts; + phase_rad = phase_deg * M_PI / 180.0; + dx = cos(phase_rad); + dy = sin(phase_rad); + if (orient == 0) { + dz = sin(tilt_rad); + gain_db = gainDB(dx, dy, dz, 0.0); + } else { + gain_db = gainDB(dx, 0.0, dy, 0.0); + } + LOGGER_DEBUG(logger) << i << " " << phase_deg << " " << gain_db; + } + + fclose(fp); + + return (1); +} +/******************************************************************************************/ diff --git a/src/afc-engine/antenna.h b/src/afc-engine/antenna.h new file mode 100644 index 0000000..420e323 --- /dev/null +++ b/src/afc-engine/antenna.h @@ -0,0 +1,43 @@ +/******************************************************************************************/ +/**** FILE: antenna.h ****/ +/******************************************************************************************/ + +#ifndef ANTENNA_H +#define ANTENNA_H + +#include + +class LinInterpClass; + +class AntennaClass +{ + public: + AntennaClass(int type, const char *strid = (char *)NULL); + ~AntennaClass(); + static std::vector readMultipleBoresightAntennas( + std::string filename); + void setBoresightGainTable(LinInterpClass *offBoresightGainTableVal); + double gainDB(double dx, double dy, double dz, double h_angle_rad); + double gainDB(double phi, double theta); + double gainDB(double theta); + int checkGain(const char *flname, int orient, int numpts); + char *get_strid(); + int get_type(); + int get_is_omni(); + double h_width; + static int color; + double vg0; + + private: + char *strid; + int type, is_omni; + double tilt_rad; + double gain_fwd_db; /* gain_v( tilt_rad ) */ + double gain_back_db; /* gain_v( PI - tilt_rad ) */ + LinInterpClass *horizGainTable; + LinInterpClass *vertGainTable; + LinInterpClass *offBoresightGainTable; +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/calcitu1245.cpp b/src/afc-engine/calcitu1245.cpp new file mode 100644 index 0000000..ea139ee --- /dev/null +++ b/src/afc-engine/calcitu1245.cpp @@ -0,0 +1,155 @@ +#include "calcitu1245.h" + +using namespace std; + +namespace calcItu1245 +{ + +double mymin(const double &a, const double &b) +{ + if (a < b) + return a; + else + return b; +} + +double mymax(const double &a, const double &b) +{ + if (a > b) + return a; + else + return b; +} + +double CalcITU1245(const double &angleDeg, const double &maxGain, const double &Dlambda) +{ + double cAngleDeg = angleDeg; + + while (cAngleDeg > 180.0) { + cAngleDeg = cAngleDeg - 360.0; + } + + cAngleDeg = fabs(cAngleDeg); + + // double Dlambda = pow(10, ((maxGain - 7.7)/20)); + double g1 = 2.0 + 15.0 * log10(Dlambda); + double psiM = 20.0 * (1.0 / Dlambda) * pow((maxGain - g1), 0.5); + double psiR = 12.02 * pow(Dlambda, -0.6); + double rv; + + // qDebug() << Dlambda; + + if (Dlambda > 100.0) { + if (cAngleDeg >= 0.0 && cAngleDeg < psiM) { + rv = maxGain - 2.5 * pow(10, -3.0) * pow((Dlambda * cAngleDeg), 2.0); + } else if (cAngleDeg >= psiM && cAngleDeg < mymax(psiM, psiR)) { + rv = g1; + } else if (cAngleDeg >= mymax(psiM, psiR) && cAngleDeg < 48.0) { + rv = 29.0 - 25.0 * log10(cAngleDeg); + } else { + rv = -13.0; + } + } else if (Dlambda <= 100.0) { + // qDebug() << cAngleDeg << psiM; + if (cAngleDeg >= 0.0 && cAngleDeg < psiM) { + rv = maxGain - 2.5 * pow(10, -3.0) * pow((Dlambda * cAngleDeg), 2.0); + } else if (cAngleDeg >= psiM && cAngleDeg < 48.0) { + rv = 39.0 - 5.0 * log10(Dlambda) - 25.0 * log10(cAngleDeg); + // qDebug() << "rv = 39 - 5 * log10(" << Dlambda << ") - 25 * log10(" << + // cAngleDeg << ") = "<< rv; + } else { + rv = -3.0 - 5.0 * log10(Dlambda); + } + } + return rv; +} + +double CalcITU1245psiM(const double &maxGain) +{ + double Dlambda = pow(10, ((maxGain - 7.7) / 20)); + double g1 = 2.0 + 15.0 * log10(Dlambda); + double psiM = 20.0 * (1.0 / Dlambda) * pow((maxGain - g1), 0.5); + + return (psiM); +} + +double CalcFCCPattern(const double &angleDeg, const double &maxGain, const double &Dlambda) +{ + double cAngleDeg = angleDeg; + + while (cAngleDeg >= 360.0) { + cAngleDeg = cAngleDeg - 360.0; + } + + if (cAngleDeg > 180.0) { + double gt180 = cAngleDeg - 180; + cAngleDeg = cAngleDeg - gt180 * 2.0; + } + + double rv; + + if (cAngleDeg < 5.0) { + rv = CalcITU1245(cAngleDeg, maxGain, Dlambda); + } + + else if (cAngleDeg >= 5.0 && cAngleDeg < 10.0) { + rv = maxGain - 25.0; + } else if (cAngleDeg >= 10.0 && cAngleDeg < 15.0) { + rv = maxGain - 29.0; + } else if (cAngleDeg >= 15.0 && cAngleDeg < 20.0) { + rv = maxGain - 33.0; + } else if (cAngleDeg >= 20.0 && cAngleDeg < 30.0) { + rv = maxGain - 36.0; + } else if (cAngleDeg >= 30.0 && cAngleDeg < 100.0) { + rv = maxGain - 42.0; + } else if (cAngleDeg >= 100.0 && cAngleDeg <= 180.0) { + rv = maxGain - 55.0; + } else { + rv = maxGain - 55.0; + } + return rv; +} + +double CalcETSIClass4(const double &angleDeg, const double &maxGain, const double &Dlambda) +{ + double cAngleDeg = angleDeg; + + while (cAngleDeg >= 360.0) { + cAngleDeg = cAngleDeg - 360.0; + } + + if (cAngleDeg > 180.0) { + double gt180 = cAngleDeg - 180; + cAngleDeg = cAngleDeg - gt180 * 2.0; + } + + double rv; + + if (cAngleDeg < 5.0) { + rv = CalcITU1245(cAngleDeg, maxGain, Dlambda); + } else if (cAngleDeg < 10.0) { + double slope = (16.0 - 5.0) / (5.0 - 10.0); + + rv = slope * (cAngleDeg - 5.0) + 16.0; + } else if (cAngleDeg < 20.0) { + double slope = (5.0 - -7.0) / (10.0 - 20.0); + rv = slope * (cAngleDeg - 10.0) + 5.0; + } else if (cAngleDeg < 50.0) { + double slope = (-7.0 - -18.0) / (20.0 - 50.0); + rv = slope * (cAngleDeg - 20.0) + -7.0; + } else if (cAngleDeg < 70.0) { + double slope = (-18.0 - -20.0) / (50.0 - 70.0); + rv = slope * (cAngleDeg - 50.0) + -18.0; + } else if (cAngleDeg < 85.0) { + double slope = (-20.0 - -24.0) / (70.0 - 85.0); + rv = slope * (cAngleDeg - 70.0) + -20.0; + } else if (cAngleDeg < 105.0) { + double slope = (-24.0 - -30.0) / (85.0 - 105.0); + rv = slope * (cAngleDeg - 85.0) + -24.0; + } else { + rv = -30.0; + } + + return rv; +} +} diff --git a/src/afc-engine/calcitu1245.h b/src/afc-engine/calcitu1245.h new file mode 100644 index 0000000..5d42d9e --- /dev/null +++ b/src/afc-engine/calcitu1245.h @@ -0,0 +1,30 @@ +#ifndef CALCITU1245_H +#define CALCITU1245_H + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +namespace calcItu1245 +{ + +double mymin(const double &a, const double &b); + +double mymax(const double &a, const double &b); + +double CalcITU1245(const double &angleDeg, const double &maxGain, const double &Dlambda); + +double CalcITU1245psiM(const double &maxGain); + +double CalcFCCPattern(const double &angleDeg, const double &maxGain, const double &Dlambda); + +double CalcETSIClass4(const double &angleDeg, const double &maxGain, const double &Dlambda); +} + +#endif // CALCITU1245_H diff --git a/src/afc-engine/calcitu1336_4.cpp b/src/afc-engine/calcitu1336_4.cpp new file mode 100644 index 0000000..48d9546 --- /dev/null +++ b/src/afc-engine/calcitu1336_4.cpp @@ -0,0 +1,40 @@ +#include "calcitu1245.h" + +#include + +using namespace std; + +namespace calcItu1336_4 +{ + +double CalcITU1336_omni_avg(const double &elAngleDeg, + const double &maxGain, + const double & /* frequencyHz */) +{ + // ITU-R F.1336-4 (02/2014) + // Section 2.2 Omnidirectional Antenna, average side-lobe pattern + + double theta3 = 107.6 * exp(-maxGain * log(10.0) / 10.0); // EQN 1b + + double k = 0.0; // sec 2.4 + + double theta5 = theta3 * sqrt(1.25 - log10(k + 1.0) / 1.2); // Eqn 1d + + double absElAngleDeg = fabs(elAngleDeg); + + double gain; + + if (absElAngleDeg < theta3) { + double r = absElAngleDeg / theta3; + gain = maxGain - 12 * r * r; + } else if (absElAngleDeg < theta5) { + gain = maxGain - 15.0 + 10 * log10(k + 1.0); + } else { + double r = absElAngleDeg / theta3; + gain = maxGain - 15.0 + 10 * log10(exp(-1.5 * log(r)) + k); + } + + return (gain); +} + +} diff --git a/src/afc-engine/calcitu1336_4.h b/src/afc-engine/calcitu1336_4.h new file mode 100644 index 0000000..ea62189 --- /dev/null +++ b/src/afc-engine/calcitu1336_4.h @@ -0,0 +1,23 @@ +#ifndef CALCITU1336_4_H +#define CALCITU1336_4_H + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +namespace calcItu1336_4 +{ + +double CalcITU1336_omni_avg(const double &elAngleDeg, + const double &maxGain, + const double &frequencyHz); + +} + +#endif // CALCITU1336_4_H diff --git a/src/afc-engine/calcitu699.cpp b/src/afc-engine/calcitu699.cpp new file mode 100644 index 0000000..d170896 --- /dev/null +++ b/src/afc-engine/calcitu699.cpp @@ -0,0 +1,60 @@ +#include "calcitu699.h" + +using namespace std; + +namespace calcItu699 +{ + +double CalcITU699(const double &angleDeg, const double &maxGain, const double &Dlambda) +{ + double cAngleDeg = angleDeg; + + while (cAngleDeg > 180.0) { + cAngleDeg = cAngleDeg - 360.0; + } + + cAngleDeg = fabs(cAngleDeg); + + // double Dlambda = pow(10, ((maxGain - 7.7)/20)); + double g1 = 2.0 + 15.0 * log10(Dlambda); + double psiM = 20.0 * (1.0 / Dlambda) * pow((maxGain - g1), 0.5); + double psiR = 15.85 * pow(Dlambda, -0.6); + double rv; + + // qDebug() << Dlambda; + + if (Dlambda > 100.0) { + if (cAngleDeg >= 0.0 && cAngleDeg < psiM) { + rv = maxGain - 2.5 * pow(10, -3.0) * pow((Dlambda * cAngleDeg), 2.0); + } else if (cAngleDeg >= psiM && cAngleDeg < std::max(psiM, psiR)) { + rv = g1; + } else if (cAngleDeg >= std::max(psiM, psiR) && cAngleDeg < 120.0) { + rv = 32.0 - 25.0 * log10(cAngleDeg); + } else { + rv = -20.0; + } + } else if (Dlambda <= 100.0) { + // qDebug() << cAngleDeg << psiM; + if (cAngleDeg >= 0.0 && cAngleDeg < psiM) { + rv = maxGain - 2.5 * pow(10, -3.0) * pow((Dlambda * cAngleDeg), 2.0); + } else if (cAngleDeg >= psiM && cAngleDeg < 100.0 / Dlambda) { + rv = g1; + } else if (cAngleDeg >= std::max(psiM, 100.0 / Dlambda) && cAngleDeg < 48.0) { + rv = 52.0 - 10.0 * log10(Dlambda) - 25.0 * log10(cAngleDeg); + } else { + rv = 10.0 - 10.0 * log10(Dlambda); + } + } + return rv; +} + +double CalcITU699psiM(const double &maxGain) +{ + double Dlambda = pow(10, ((maxGain - 7.7) / 20)); + double g1 = 2.0 + 15.0 * log10(Dlambda); + double psiM = 20.0 * (1.0 / Dlambda) * pow((maxGain - g1), 0.5); + + return (psiM); +} + +} diff --git a/src/afc-engine/calcitu699.h b/src/afc-engine/calcitu699.h new file mode 100644 index 0000000..51b884c --- /dev/null +++ b/src/afc-engine/calcitu699.h @@ -0,0 +1,22 @@ +#ifndef CALCITU699_H +#define CALCITU699_H + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +namespace calcItu699 +{ + +double CalcITU699(const double &angleDeg, const double &maxGain, const double &Dlambda); + +double CalcITU699psiM(const double &maxGain); +} + +#endif // CALCITU699_H diff --git a/src/afc-engine/cconst.cpp b/src/afc-engine/cconst.cpp new file mode 100644 index 0000000..457ff0f --- /dev/null +++ b/src/afc-engine/cconst.cpp @@ -0,0 +1,122 @@ +/******************************************************************************************/ +/**** FILE: cconst.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include +#include + +#include "cconst.h" +#include "str_type.h" +#include "global_defines.h" + +/******************************************************************************************/ +/**** Static Constants ****/ +/******************************************************************************************/ +const double CConst::c = 2.99792458e8; +const double CConst::u0 = 4 * M_PI * 1.0e-7; +const double CConst::e0 = 1 / (CConst::c * CConst::c * CConst::u0); +const double CConst::logTable[] = {0.0, + log(2.0) / log(10.0), + log(3.0) / log(10.0), + log(4.0) / log(10.0), + log(5.0) / log(10.0), + log(6.0) / log(10.0), + log(7.0) / log(10.0), + log(8.0) / log(10.0), + log(9.0) / log(10.0)}; +const double CConst::earthRadius = 6378.137e3; +const double CConst::averageEarthRadius = 6371.0e3; +const double CConst::geoRadius = 42164.0e3; +const double CConst::boltzmannConstant = 1.3806488e-23; +const double CConst::T0 = 290; +const double CConst::atmPressurehPa = 1013; + +const int CConst::unii5StartFreqMHz = 5925; +const int CConst::unii5StopFreqMHz = 6425; +const int CConst::unii7StartFreqMHz = 6525; +const int CConst::unii7StopFreqMHz = 6875; +const int CConst::unii8StartFreqMHz = 6875; +const int CConst::unii8StopFreqMHz = 7125; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Static StrTypeClass lists ****/ +/******************************************************************************************/ +const StrTypeClass CConst::strPathLossModelList[] = { + {CConst::unknownPathLossModel, "UNKNOWN"}, + {CConst::FSPLPathLossModel, "FSPL"}, + {CConst::ITMBldgPathLossModel, "ITM with building data"}, + {CConst::FCC6GHzReportAndOrderPathLossModel, "FCC 6GHz Report & Order"}, + {CConst::CustomPathLossModel, "Custom"}, + {CConst::ISEDDBS06PathLossModel, "ISED DBS-06"}, + {CConst::BrazilPathLossModel, "Brazilian Propagation Model"}, + {CConst::OfcomPathLossModel, "Ofcom Propagation Model"}, + {CConst::CoalitionOpt6PathLossModel, "ITM with no building data"}, + + {-1, (char *)0}}; + +const StrTypeClass CConst::strLOSOptionList[] = { + {CConst::UnknownLOSOption, "UNKNOWN"}, + {CConst::BldgDataLOSOption, "BLDG_DATA"}, + {CConst::BldgDataReqTxLOSOption, "BLDG_DATA_REQ_TX"}, + {CConst::BldgDataReqRxLOSOption, "BLDG_DATA_REQ_RX"}, + {CConst::BldgDataReqTxRxLOSOption, "BLDG_DATA_REQ_TX_RX"}, + {CConst::CdsmLOSOption, "CDSM"}, + {CConst::ForceLOSLOSOption, "FORCE_LOS"}, + {CConst::ForceNLOSLOSOption, "FORCE_NLOS"}, + + {-1, (char *)0}}; + +const StrTypeClass CConst::strITMClutterMethodList[] = { + {CConst::ForceTrueITMClutterMethod, "FORCE_TRUE"}, + {CConst::ForceFalseITMClutterMethod, "FORCE_FALSE"}, + {CConst::BldgDataITMCLutterMethod, "BLDG_DATA"}, + + {-1, (char *)0}}; + +const StrTypeClass CConst::strPropEnvList[] = {{CConst::unknownPropEnv, "UNKNOWN"}, + {CConst::urbanPropEnv, "URBAN"}, + {CConst::suburbanPropEnv, "SUBURBAN"}, + {CConst::ruralPropEnv, "RURAL"}, + {CConst::barrenPropEnv, "BARREN"}, + {-1, (char *)0}}; + +const StrTypeClass CConst::strPropEnvMethodList[] = {{CConst::nlcdPointPropEnvMethod, "NLCD Point"}, + {CConst::popDensityMapPropEnvMethod, + "Population Density Map"}, + {CConst::urbanPropEnvMethod, "Urban"}, + {CConst::suburbanPropEnvMethod, "Suburban"}, + {CConst::ruralPropEnvMethod, "Rural"}, + {-1, (char *)0}}; + +const StrTypeClass CConst::strULSAntennaTypeList[] = {{CConst::F1336OmniAntennaType, "F.1336 Omni"}, + {CConst::F1245AntennaType, "F.1245"}, + {CConst::F699AntennaType, "F.699"}, + {CConst::R2AIP07AntennaType, "WINNF-AIP-07"}, + {CConst::R2AIP07CANAntennaType, + "WINNF-AIP-07-CAN"}, + {-1, (char *)0}}; + +const StrTypeClass CConst::strHeightSourceList[] = {{CConst::unknownHeightSource, "UNKNOWN"}, + {CConst::globalHeightSource, "GLOBAL"}, + {CConst::depHeightSource, "3DEP"}, + {CConst::srtmHeightSource, "SRTM"}, + {CConst::cdsmHeightSource, "CDSM"}, + {CConst::lidarHeightSource, "LiDAR"}, + {-1, (char *)0}}; + +const StrTypeClass CConst::strSpectralAlgorithmList[] = {{CConst::pwrSpectralAlgorithm, "pwr"}, + {CConst::psdSpectralAlgorithm, "psd"}, + {-1, (char *)0}}; + +const StrTypeClass CConst::strPRTypeList[] = {{CConst::backToBackAntennaPRType, "Ant"}, + {CConst::billboardReflectorPRType, "Ref"}, + {-1, (char *)0}}; + +const StrTypeClass CConst::strAntennaCategoryList[] = {{CConst::HPAntennaCategory, "HP"}, + {CConst::B1AntennaCategory, "B1"}, + {CConst::OtherAntennaCategory, "OTHER"}, + {CConst::UnknownAntennaCategory, "UNKNOWN"}, + {-1, (char *)0}}; diff --git a/src/afc-engine/cconst.h b/src/afc-engine/cconst.h new file mode 100644 index 0000000..934a3f6 --- /dev/null +++ b/src/afc-engine/cconst.h @@ -0,0 +1,318 @@ +#ifndef CCONST_H +#define CCONST_H + +class StrTypeClass; + +class CConst +{ + public: + static const double c; // speed of light in m/s + static const double u0; // Permeability of free space H/m + static const double e0; // Permittivity of free space F/m + static const double logTable[]; // logTable[i] = LOG10(1+i) i = [0,8] + + static const double earthRadius; // Radius of earth in m + static const double averageEarthRadius; // Average radius of earth in m + static const double geoRadius; // Radius of geosynchronous orbit + static const double boltzmannConstant; // Boltzman's constant + static const double T0; // Room temperature for noise figure calculations + static const double atmPressurehPa; // Atmospheric pressure value used in P.452 + + static const int unii5StartFreqMHz; // UNII-5 Start Freq (MHz) + static const int unii5StopFreqMHz; // UNII-5 Stop Freq (MHz) + static const int unii7StartFreqMHz; // UNII-7 Start Freq (MHz) + static const int unii7StopFreqMHz; // UNII-7 Stop Freq (MHz) + static const int unii8StartFreqMHz; // UNII-8 Start Freq (MHz) + static const int unii8StopFreqMHz; // UNII-8 Stop Freq (MHz) + + /**************************************************************************************/ + /**** BuildingType ****/ + /**************************************************************************************/ + enum BuildingTypeEnum { + noBuildingType, // outdoor + traditionalBuildingType, + thermallyEfficientBuildingType + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** PropEnv ****/ + /**************************************************************************************/ + enum PropEnvEnum { + unknownPropEnv, // 'X' + urbanPropEnv, // 'U' + suburbanPropEnv, // 'S' + ruralPropEnv, // 'R' + barrenPropEnv // 'B' + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** PropEnvMethod ****/ + /**************************************************************************************/ + enum PropEnvMethodEnum { + nlcdPointPropEnvMethod, + popDensityMapPropEnvMethod, + urbanPropEnvMethod, + suburbanPropEnvMethod, + ruralPropEnvMethod, + + unknownPropEnvMethod + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** NLCDLandCat ****/ + /**************************************************************************************/ + enum NLCDLandCatEnum { + deciduousTreesNLCDLandCat, + coniferousTreesNLCDLandCat, + highCropFieldsNLCDLandCat, + noClutterNLCDLandCat, + villageCenterNLCDLandCat, + tropicalRainForestNLCDLandCat, + + unknownNLCDLandCat + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** UserType ****/ + /**************************************************************************************/ + enum UserTypeEnum { + unknownUserType, + corporateUserType, + publicUserType, + homeUserType + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** AntennaType ****/ + /**************************************************************************************/ + enum AntennaTypeEnum { + antennaOmni, + antennaLUT_H, + antennaLUT_V, + antennaLUT_Boresight, + antennaLUT + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** ULSType ****/ + /**************************************************************************************/ + enum ULSTypeEnum { ESULSType, AMTULSType }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** PRType ****/ + /**************************************************************************************/ + enum PRTypeEnum { + backToBackAntennaPRType, + billboardReflectorPRType, + unknownPRType + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** ULSAntennaType ****/ + /**************************************************************************************/ + enum ULSAntennaTypeEnum { + OmniAntennaType, + F1336OmniAntennaType, + F1245AntennaType, + F699AntennaType, + R2AIP07AntennaType, + R2AIP07CANAntennaType, + LUTAntennaType, + UnknownAntennaType + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** PathLossModel ****/ + /**************************************************************************************/ + enum PathLossModelEnum { + unknownPathLossModel, + ITMBldgPathLossModel, + CoalitionOpt6PathLossModel, + FCC6GHzReportAndOrderPathLossModel, + CustomPathLossModel, + ISEDDBS06PathLossModel, + BrazilPathLossModel, + OfcomPathLossModel, + FSPLPathLossModel + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Global Parameters ****/ + /**************************************************************************************/ + enum GParam { antenna_num_interp_pts = 361 }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** SimulationEnum ****/ + /**************************************************************************************/ + enum SimulationEnum { + FixedSimulation, + MobileSimulation, + RLANSensingSimulation, + showFSPwrAtRLANSimulation, + FSToFSSimulation + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** LengthUnit ****/ + /**************************************************************************************/ + enum LengthUnitEnum { + mmLengthUnit, + cmLengthUnit, + mLengthUnit, + KmLengthUnit, + milLengthUnit, + inLengthUnit, + ftLengthUnit + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** AngleUnit ****/ + /**************************************************************************************/ + enum AngleUnitEnum { radianAngleUnit = 0, degreeAngleUnit }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** PSDDBUnit ****/ + /**************************************************************************************/ + enum PSDDBUnitEnum { WPerHzPSDDBUnit = 0, WPerMHzPSDDBUnit, WPer4KHzPSDDBUnit }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** LidarFormatEnum ****/ + /**************************************************************************************/ + enum LidarFormatEnum { + fromVectorLidarFormat, // building data comes from vector data + fromRasterLidarFormat // building data comes from raster data + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** HeightSource ****/ + /**************************************************************************************/ + enum HeightSourceEnum { + unknownHeightSource, + globalHeightSource, + depHeightSource, + srtmHeightSource, + cdsmHeightSource, + lidarHeightSource + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** HeightType ****/ + /**************************************************************************************/ + enum HeightTypeEnum { AMSLHeightType, AGLHeightType }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** LOSOption ****/ + /**************************************************************************************/ + enum LOSOptionEnum { + UnknownLOSOption, + BldgDataLOSOption, + BldgDataReqTxLOSOption, + BldgDataReqRxLOSOption, + BldgDataReqTxRxLOSOption, + CdsmLOSOption, + ForceLOSLOSOption, + ForceNLOSLOSOption + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Winner2UnknownLOSMethod ****/ + /**************************************************************************************/ + enum Winner2UnknownLOSMethodEnum { + PLOSCombineWinner2UnknownLOSMethod, + PLOSThresholdWinner2UnknownLOSMethod + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** SpectralAlgorithm ****/ + /**************************************************************************************/ + enum SpectralAlgorithmEnum { pwrSpectralAlgorithm, psdSpectralAlgorithm }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** ITMClutterMethod ****/ + /**************************************************************************************/ + enum ITMClutterMethodEnum { + ForceTrueITMClutterMethod, + ForceFalseITMClutterMethod, + BldgDataITMCLutterMethod + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** ResponseCode ****/ + /**************************************************************************************/ + enum ResponseCodeEnum { + generalFailureResponseCode = -1, + successResponseCode = 0, + versionNotSupportedResponseCode = 100, + deviceDisallowedResponseCode = 101, + missingParamResponseCode = 102, + invalidValueResponseCode = 103, + unexpectedParamResponseCode = 106, + unsupportedSpectrumResponseCode = 300 + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** ScanPointBelowGroundMethod ****/ + /**************************************************************************************/ + enum ScanPointBelowGroundMethodEnum { + DiscardScanPointBelowGroundMethod, + TruncateScanPointBelowGroundMethod + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** ScanRegionMethod ****/ + /**************************************************************************************/ + enum ScanRegionMethodEnum { + xyAlignRegionNorthEastScanRegionMethod, + xyAlignRegionMajorMinorScanRegionMethod, + latLonAlignGridScanRegionMethod + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** AntennaCategory ****/ + /**************************************************************************************/ + enum AntennaCategoryEnum { + HPAntennaCategory, + B1AntennaCategory, + OtherAntennaCategory, + UnknownAntennaCategory + }; + /**************************************************************************************/ + + static const StrTypeClass strULSAntennaTypeList[]; + static const StrTypeClass strPropEnvList[]; + static const StrTypeClass strPropEnvMethodList[]; + static const StrTypeClass strPathLossModelList[]; + static const StrTypeClass strLOSOptionList[]; + static const StrTypeClass strITMClutterMethodList[]; + static const StrTypeClass strHeightSourceList[]; + static const StrTypeClass strSpectralAlgorithmList[]; + static const StrTypeClass strPRTypeList[]; + static const StrTypeClass strAntennaCategoryList[]; +}; + +#endif diff --git a/src/afc-engine/data_if.cpp b/src/afc-engine/data_if.cpp new file mode 100644 index 0000000..d611bdf --- /dev/null +++ b/src/afc-engine/data_if.cpp @@ -0,0 +1,248 @@ +/* GET and POST files from/to remote or local file storage */ + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef DATA_IF_STANDALONE + #include + #define LOGGER_DEFINE_GLOBAL(a, b) + #define LOGGER_DEBUG(a) qDebug() + #define LOGGER_ERROR(a) qDebug() +#else + #include "afclogging/Logging.h" +#endif +#include "data_if.h" + +#define ZLIB_COMPRESS_LEVEL 6 +#define ZLIB_WINDOW_BITS (MAX_WBITS + 16) +#define ZLIB_MEMORY_LEVEL 8 +#if GUNZIP_INPUT_FILES + #define ZLIB_MAX_FILE_SIZE 100000 +#endif +#define MAX_NET_DELAY 5000 /* wait for download/upload, ms */ + +LOGGER_DEFINE_GLOBAL(logger, "AfcDataIf") + +AfcDataIf::AfcDataIf(bool useUrl) +{ + AfcDataIf::_useUrl = useUrl; + AfcDataIf::_app = NULL; + if (AfcDataIf::_useUrl) { + if (!QCoreApplication::instance()) { + /* QNetworkAccessManager uses qt app events */ + int argc = 0; + char *argv = NULL; + AfcDataIf::_app = new QCoreApplication(argc, &argv); + } + } +} + +AfcDataIf::~AfcDataIf() +{ + if (AfcDataIf::_app) { + AfcDataIf::_app->quit(); + delete AfcDataIf::_app; + } +} + +bool AfcDataIf::readFile(QString fileName, QByteArray &data) +{ + LOGGER_DEBUG(logger) << "AfcDataIf::readFile(" << fileName << ")"; + if (!AfcDataIf::_useUrl) { + QFile inFile; + + inFile.setFileName(fileName); + if (inFile.open(QFile::ReadOnly)) { + QByteArray gzipped = inFile.readAll(); + QByteArray *indata = &gzipped; +#if GUNZIP_INPUT_FILES + QByteArray gunzipped; + if (AfcDataIf::gunzipBuffer(gzipped, gunzipped)) { + indata = &gunzipped; + } +#endif + data.replace(0, indata->size(), indata->data(), indata->size()); + inFile.close(); + return true; + } + LOGGER_ERROR(logger) << "AfcDataIf::readFile(" << fileName << ") QFile.open error"; + return false; + } else { + QTimer timer; /* use QNetworkRequest::setTransferTimeout after update to qt5.15 */ + QEventLoop loop; + timer.setSingleShot(true); + + QUrl url = QUrl(fileName); + QNetworkRequest req(url); + + QNetworkReply *reply = AfcDataIf::_mngr.get(req); + + QObject::connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); + QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + timer.start(MAX_NET_DELAY); + loop.exec(); + if (timer.isActive() && reply->error() == QNetworkReply::NoError) { + timer.stop(); + data = reply->readAll(); + delete reply; + return true; + } + LOGGER_ERROR(logger) + << "readFile(" << url.toString() << ") error " << reply->error(); + delete reply; + return false; + } +} + +bool AfcDataIf::gzipAndWriteFile(QString fileName, QByteArray &data) +{ + LOGGER_DEBUG(logger) << "gzipAndWriteFile(" << fileName << ")" + << " len: " << data.length(); + QByteArray gziped; + if (!AfcDataIf::gzipBuffer(data, gziped)) { + return false; + } + return AfcDataIf::writeFile(fileName, gziped); +} + +bool AfcDataIf::writeFile(QString fileName, QByteArray &data) +{ + LOGGER_DEBUG(logger) << "writeFile(" << fileName << ")" + << " len: " << data.length(); + if (!AfcDataIf::_useUrl) { + QFile outFile; + + outFile.setFileName(fileName); + if (outFile.open(QFile::WriteOnly)) { + if (outFile.write(data) == data.size()) { + outFile.close(); + return true; + } + outFile.close(); + } + return false; + } else { + QEventLoop loop; + QTimer timer; + QUrl url = QUrl(fileName); + QNetworkRequest request(url); + + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream"); + request.setHeader(QNetworkRequest::ContentLengthHeader, data.length()); + QNetworkReply *reply = AfcDataIf::_mngr.post(request, data); + QObject::connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); + QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + timer.start(MAX_NET_DELAY); + loop.exec(); + if (timer.isActive() && reply->error() == QNetworkReply::NoError) { + timer.stop(); + delete reply; + return true; + } + LOGGER_ERROR(logger) + << "writeFile(" << url.toString() << ") error " << reply->error(); + delete reply; + return false; + } +} + +bool AfcDataIf::gzipBuffer(QByteArray &input, QByteArray &output) +{ + if (!input.length()) { + return true; + } + + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.avail_in = 0; + strm.total_in = 0; + strm.total_out = 0; + strm.next_in = Z_NULL; + + int ret = deflateInit2(&strm, + ZLIB_COMPRESS_LEVEL, + Z_DEFLATED, + ZLIB_WINDOW_BITS, + ZLIB_MEMORY_LEVEL, + Z_DEFAULT_STRATEGY); + if (ret != Z_OK) { + LOGGER_ERROR(logger) << "deflateInit2 error"; + return false; + } + + output.clear(); + output.resize(input.size()); + + strm.avail_in = input.size(); + strm.next_in = (unsigned char *)input.data(); + strm.avail_out = output.size(); + strm.next_out = (unsigned char *)output.data(); + ret = deflate(&strm, Z_FINISH); + if (ret != Z_OK && ret != Z_STREAM_END) { + LOGGER_ERROR(logger) << "deflate error"; + deflateEnd(&strm); + return false; + } + + output.resize(output.size() - strm.avail_out); + + deflateEnd(&strm); + return true; +} + +#if GUNZIP_INPUT_FILES +bool AfcDataIf::gunzipBuffer(QByteArray &input, QByteArray &output) +{ + LOGGER_DEBUG(logger) << "AfcDataIf::gunzipBuffer()"; + if (!input.length()) { + LOGGER_ERROR(logger) << "AfcDataIf::gunzipBuffer() empty input buffer"; + return true; + } + + /* get output size */ + uint32_t sz = input.at(input.size() - 1); + sz <<= 8; + sz += input.at(input.size() - 2); + sz <<= 8; + sz += input.at(input.size() - 3); + sz <<= 8; + sz += input.at(input.size() - 4); + if (sz > ZLIB_MAX_FILE_SIZE) { + LOGGER_ERROR(logger) << std::hex << "AfcDataIf::gunzipBuffer() file too big " << sz; + return false; + } + std::cout << std::hex << "outsize " << sz; + output.clear(); + output.resize(sz * 2); + + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.avail_in = 0; + strm.next_in = Z_NULL; + int ret = inflateInit2(&strm, -MAX_WBITS); + if (ret != Z_OK) + return false; + strm.avail_in = input.size(); + strm.next_in = (unsigned char *)input.data(); + strm.avail_out = output.size(); + strm.next_out = (unsigned char *)output.data(); + ret = inflate(&strm, Z_FINISH); + if (ret != Z_OK && ret != Z_STREAM_END) { + LOGGER_ERROR(logger) << "inflate error " << ret; + inflateEnd(&strm); + return false; + } + + inflateEnd(&strm); + return true; +} +#endif diff --git a/src/afc-engine/data_if.h b/src/afc-engine/data_if.h new file mode 100644 index 0000000..11fcf61 --- /dev/null +++ b/src/afc-engine/data_if.h @@ -0,0 +1,26 @@ +/* GET and POST files from/to remote or local file storage */ + +#include +#include + +#define GUNZIP_INPUT_FILES 0 /* Do input files are gzipped? */ + +class AfcDataIf : public QObject +{ + Q_OBJECT + public: + AfcDataIf(bool useUrl); + ~AfcDataIf(); + bool readFile(QString fileName, QByteArray &data); + bool writeFile(QString fileName, QByteArray &data); + bool gzipAndWriteFile(QString fileName, QByteArray &data); + + private: + bool _useUrl; + QNetworkAccessManager _mngr; + bool gzipBuffer(QByteArray &indata, QByteArray &outdata); + QCoreApplication *_app; +#if GUNZIP_INPUT_FILES + bool gunzipBuffer(QByteArray &input, QByteArray &output); +#endif +}; diff --git a/src/afc-engine/dbldbl.cpp b/src/afc-engine/dbldbl.cpp new file mode 100644 index 0000000..09d6482 --- /dev/null +++ b/src/afc-engine/dbldbl.cpp @@ -0,0 +1,99 @@ +/******************************************************************************************/ +/**** FILE: dbldbl.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include +#include "dbldbl.h" + +DblDblClass::DblDblClass(double d0, double d1) +{ + dval0 = d0; + dval1 = d1; +} + +DblDblClass::~DblDblClass() +{ +} + +double DblDblClass::getDbl(int i) +{ + if (i == 0) { + return (dval0); + } else { + return (dval1); + } +} + +void DblDblClass::setX(double xval) +{ + dval0 = xval; +} + +void DblDblClass::setY(double yval) +{ + dval1 = yval; +} + +double DblDblClass::x() const +{ + return (dval0); +} + +double DblDblClass::y() const +{ + return (dval1); +} + +int DblDblClass::operator==(const DblDblClass &val) const +{ + if ((val.dval0 == dval0) && (val.dval1 == dval1)) { + return (1); + } else { + return (0); + } +} + +int DblDblClass::operator>(const DblDblClass &val) const +{ + if ((dval0 > val.dval0) || ((dval0 == val.dval0) && (dval1 > val.dval1))) { + return (1); + } else { + return (0); + } +} + +std::ostream &operator<<(std::ostream &s, const DblDblClass &val) +{ + s << "(" << val.dval0 << "," << val.dval1 << ")"; + return (s); +} + +int cvtStrToVal(char const *strptr, DblDblClass &val) +{ + const char *chptr = strptr; + char *nptr; + int i; + + for (i = 0; i <= 1; i++) { + switch (i) { + case 0: + val.dval0 = strtod(chptr, &nptr); + break; + case 1: + val.dval1 = strtod(chptr, &nptr); + break; + } + if (nptr == chptr) { + std::stringstream errorStr; + errorStr << "ERROR in cvtStrToVal() : Unable to cvt to DblDblClass \"" + << strptr << "\""; + throw std::runtime_error(errorStr.str()); + return (0); + } + chptr = nptr; + } + + return (chptr - strptr); +} diff --git a/src/afc-engine/dbldbl.h b/src/afc-engine/dbldbl.h new file mode 100644 index 0000000..36aef31 --- /dev/null +++ b/src/afc-engine/dbldbl.h @@ -0,0 +1,29 @@ +#ifndef DBLDBL_H +#define DBLDBL_H + +#include + +std::ostream &operator<<(std::ostream &s, const class DblDblClass &val); + +int cvtStrToVal(char const *, DblDblClass &); + +class DblDblClass +{ + public: + DblDblClass(double d0 = 0, double d1 = 0); + ~DblDblClass(); + int operator==(const DblDblClass &val) const; + int operator>(const DblDblClass &val) const; + friend std::ostream &operator<<(std::ostream &s, const DblDblClass &val); + friend int cvtStrToVal(char const *, DblDblClass &); + double getDbl(int i); + void setX(double xval); + void setY(double yval); + double x() const; + double y() const; + + private: + double dval0, dval1; +}; + +#endif diff --git a/src/afc-engine/denied_region.cpp b/src/afc-engine/denied_region.cpp new file mode 100644 index 0000000..dec4405 --- /dev/null +++ b/src/afc-engine/denied_region.cpp @@ -0,0 +1,181 @@ +/******************************************************************************************/ +/**** FILE : denied_region.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#include + +#include "denied_region.h" + +#include "afclogging/ErrStream.h" +#include "afclogging/Logging.h" +#include "afclogging/LoggingConfig.h" + +namespace +{ +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "DeniedRegionClass") +} + +/******************************************************************************************/ +/**** FUNCTION: DeniedRegionClass::DeniedRegionClass() ****/ +/******************************************************************************************/ +DeniedRegionClass::DeniedRegionClass(int idVal) : id(idVal) +{ + type = nullType; + startFreq = -1.0; + stopFreq = -1.0; + heightAGL = -1.0; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: DeniedRegionClass::~DeniedRegionClass() ****/ +/******************************************************************************************/ +DeniedRegionClass::~DeniedRegionClass() +{ +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: RectDeniedRegionClass::RectDeniedRegionClass() ****/ +/******************************************************************************************/ +RectDeniedRegionClass::RectDeniedRegionClass(int idVal) : DeniedRegionClass(idVal) +{ + rectList = std::vector>(0); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: RectDeniedRegionClass::~RectDeniedRegionClass() ****/ +/******************************************************************************************/ +RectDeniedRegionClass::~RectDeniedRegionClass() +{ +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: RectDeniedRegionClass::RectDeniedRegionClass() ****/ +/******************************************************************************************/ +void RectDeniedRegionClass::addRect(double lon1, double lon2, double lat1, double lat2) +{ + double lonStart = std::min(lon1, lon2); + double lonStop = std::max(lon1, lon2); + double latStart = std::min(lat1, lat2); + double latStop = std::max(lat1, lat2); + rectList.push_back(std::make_tuple(lonStart, lonStop, latStart, latStop)); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: RectDeniedRegionClass::intersect() ****/ +/******************************************************************************************/ +bool RectDeniedRegionClass::intersect(double longitude, + double latitude, + double maxDist, + double txHeightAGL) const +{ + int rectIdx; + double deltaLon, deltaLat, dist; + bool intersectFlag = false; + for (rectIdx = 0; (rectIdx < (int)rectList.size()) && (!intersectFlag); ++rectIdx) { + double rectLonStart, rectLonStop, rectLatStart, rectLatStop; + std::tie(rectLonStart, rectLonStop, rectLatStart, rectLatStop) = rectList[rectIdx]; + + int sx = (longitude < rectLonStart ? -1 : longitude > rectLonStop ? 1 : 0); + + int sy = (latitude < rectLatStart ? -1 : latitude > rectLatStop ? 1 : 0); + + if ((sx == 0) && (sy == 0)) { + intersectFlag = true; + } else if (sx == 0) { + deltaLat = (sy == -1 ? rectLatStart - latitude : latitude - rectLatStop); + dist = deltaLat * CConst::earthRadius * M_PI / 180.0; + intersectFlag = (dist <= maxDist); + } else if (sy == 0) { + double cosVal = cos(latitude * M_PI / 180.0); + deltaLon = (sx == -1 ? rectLonStart - longitude : longitude - rectLonStop); + dist = deltaLon * CConst::earthRadius * M_PI / 180.0 * cosVal; + intersectFlag = (dist <= maxDist); + } else { + double cosVal = cos(latitude * M_PI / 180.0); + double cosSq = cosVal * cosVal; + deltaLat = (sy == -1 ? rectLatStart - latitude : latitude - rectLatStop); + deltaLon = (sx == -1 ? rectLonStart - longitude : longitude - rectLonStop); + dist = CConst::earthRadius * M_PI / 180.0 * + sqrt(deltaLat * deltaLat + deltaLon * deltaLon * cosSq); + intersectFlag = (dist <= maxDist); + } + } + + return (intersectFlag); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: CircleDeniedRegionClass::CircleDeniedRegionClass() ****/ +/******************************************************************************************/ +CircleDeniedRegionClass::CircleDeniedRegionClass(int idVal, bool horizonDistFlagVal) : + DeniedRegionClass(idVal), horizonDistFlag(horizonDistFlagVal) +{ + longitudeCenter = 0.0; + latitudeCenter = 0.0; + radius = 0.0; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: CircleDeniedRegionClass::~CircleDeniedRegionClass() ****/ +/******************************************************************************************/ +CircleDeniedRegionClass::~CircleDeniedRegionClass() +{ +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: CircleDeniedRegionClass::computeRadius() ****/ +/******************************************************************************************/ +double CircleDeniedRegionClass::computeRadius(double txHeightAGL) const +{ + double returnVal; + + if (!horizonDistFlag) { + returnVal = radius; + } else { + returnVal = sqrt(2 * CConst::earthRadius * 4.0 / 3) * + (sqrt(heightAGL) + sqrt(txHeightAGL)); + } + + return returnVal; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: CircleDeniedRegionClass::intersect() ****/ +/******************************************************************************************/ +bool CircleDeniedRegionClass::intersect(double longitude, + double latitude, + double maxDist, + double txHeightAGL) const +{ + double drRadius = computeRadius(txHeightAGL); + + double deltaLon = longitudeCenter - longitude; + double deltaLat = latitudeCenter - latitude; + double cosVal = cos(latitude * M_PI / 180.0); + double cosSq = cosVal * cosVal; + + double dist = CConst::earthRadius * M_PI / 180.0 * + (sqrt(deltaLat * deltaLat + deltaLon * deltaLon * cosSq)); + + bool retval = (dist <= drRadius + maxDist); + + return (retval); +} +/******************************************************************************************/ diff --git a/src/afc-engine/denied_region.h b/src/afc-engine/denied_region.h new file mode 100644 index 0000000..db4db39 --- /dev/null +++ b/src/afc-engine/denied_region.h @@ -0,0 +1,211 @@ +/******************************************************************************************/ +/**** FILE : denied_region.h ****/ +/******************************************************************************************/ + +#ifndef DENIED_REGION_H +#define DENIED_REGION_H + +#include "Vector3.h" +#include "cconst.h" +#include "pop_grid.h" + +class GdalDataDir; +class WorldData; +class AfcManager; +class AntennaClass; + +template +class ListClass; + +/******************************************************************************************/ +/**** CLASS: DeniedRegionClass ****/ +/******************************************************************************************/ +class DeniedRegionClass +{ + public: + DeniedRegionClass(int idVal); + virtual ~DeniedRegionClass(); + + /**************************************************************************************/ + /**** Geometry ****/ + /**************************************************************************************/ + enum GeometryEnum { + nullGeometry, + rectGeometry, + rect2Geometry, + circleGeometry, + horizonDistGeometry + }; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Type ****/ + /**************************************************************************************/ + enum TypeEnum { nullType, RASType, userSpecifiedType }; + /**************************************************************************************/ + + virtual GeometryEnum getGeometry() const = 0; + + virtual bool intersect(double longitude, + double latitude, + double maxDist, + double txHeightAGL) const = 0; + + int getID() const + { + return id; + } + + TypeEnum getType() const + { + return (type); + } + void setType(TypeEnum typeVal) + { + type = typeVal; + } + + void setStartFreq(double startFreqVal) + { + startFreq = startFreqVal; + return; + } + void setStopFreq(double stopFreqVal) + { + stopFreq = stopFreqVal; + return; + } + void setHeightAGL(double heightAGLVal) + { + heightAGL = heightAGLVal; + return; + } + + double getStartFreq() const + { + return startFreq; + } + double getStopFreq() const + { + return stopFreq; + } + double getHeightAGL() const + { + return heightAGL; + } + + protected: + int id; + TypeEnum type; + + double startFreq; + double stopFreq; + + double heightAGL; +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CLASS: RectDeniedRegionClass ****/ +/******************************************************************************************/ +class RectDeniedRegionClass : public DeniedRegionClass +{ + public: + RectDeniedRegionClass(int idVal); + ~RectDeniedRegionClass(); + + GeometryEnum getGeometry() const + { + if (rectList.size() == 1) { + return rectGeometry; + } else if (rectList.size() == 2) { + return rect2Geometry; + } else { + return nullGeometry; + } + } + + bool intersect(double longitude, + double latitude, + double maxDist, + double txHeightAGL) const; + + int getNumRect() const + { + return rectList.size(); + } + std::tuple getRect(int rectIdx) const + { + return rectList[rectIdx]; + } + + void addRect(double lon1, double lon2, double lat1, double lat2); + + private: + std::vector> rectList; +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CLASS: CircleDeniedRegionClass ****/ +/******************************************************************************************/ +class CircleDeniedRegionClass : public DeniedRegionClass +{ + public: + CircleDeniedRegionClass(int idVal, bool horizonDistFlagVal); + ~CircleDeniedRegionClass(); + + GeometryEnum getGeometry() const + { + if (!horizonDistFlag) { + return circleGeometry; + } else { + return horizonDistGeometry; + } + } + + bool intersect(double longitude, + double latitude, + double maxDist, + double txHeightAGL) const; + + void setLongitudeCenter(double longitudeCenterVal) + { + longitudeCenter = longitudeCenterVal; + return; + } + void setLatitudeCenter(double latitudeCenterVal) + { + latitudeCenter = latitudeCenterVal; + return; + } + void setRadius(double radiusVal) + { + radius = radiusVal; + return; + } + + double getLongitudeCenter() const + { + return longitudeCenter; + } + double getLatitudeCenter() const + { + return latitudeCenter; + } + bool getHorizonDistFlag() const + { + return horizonDistFlag; + } + + double computeRadius(double txHeightAGL) const; + + private: + bool horizonDistFlag; + double longitudeCenter; + double latitudeCenter; + double radius; +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/freq_band.cpp b/src/afc-engine/freq_band.cpp new file mode 100644 index 0000000..f383824 --- /dev/null +++ b/src/afc-engine/freq_band.cpp @@ -0,0 +1,22 @@ +/******************************************************************************************/ +/**** FILE: freq_band.cpp ****/ +/******************************************************************************************/ + +#include "freq_band.h" + +/******************************************************************************************/ +/**** CONSTRUCTOR: FreqBandClass::FreqBandClass() ****/ +/******************************************************************************************/ +FreqBandClass::FreqBandClass(std::string nameVal, int startFreqMHzVal, int stopFreqMHzVal) : + name(nameVal), startFreqMHz(startFreqMHzVal), stopFreqMHz(stopFreqMHzVal) +{ +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** DESTRUCTOR: FreqBandClass::~FreqBandClass() ****/ +/******************************************************************************************/ +FreqBandClass::~FreqBandClass() +{ +} +/******************************************************************************************/ diff --git a/src/afc-engine/freq_band.h b/src/afc-engine/freq_band.h new file mode 100644 index 0000000..87cd074 --- /dev/null +++ b/src/afc-engine/freq_band.h @@ -0,0 +1,53 @@ +/******************************************************************************************/ +/**** FILE : freq_band.h ****/ +/******************************************************************************************/ + +#ifndef FREQ_BAND_H +#define FREQ_BAND_H + +#include "cconst.h" +#include "Vector3.h" + +/******************************************************************************************/ +/**** CLASS: FreqBandClass ****/ +/******************************************************************************************/ +class FreqBandClass +{ + public: + FreqBandClass(std::string nameVal, int startFreqMHzVal, int stopFreqMHzVal); + ~FreqBandClass(); + + void setName(std::string nameVal) + { + name = nameVal; + } + void setStartFreqMHz(int startFreqMHzVal) + { + startFreqMHz = startFreqMHzVal; + } + void setStopFreqMHz(int stopFreqMHzVal) + { + stopFreqMHz = stopFreqMHzVal; + } + + std::string getName() const + { + return name; + } + int getStartFreqMHz() const + { + return startFreqMHz; + } + int getStopFreqMHz() const + { + return stopFreqMHz; + } + + private: + std::string name; + int startFreqMHz; + int stopFreqMHz; +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/global_defines.h b/src/afc-engine/global_defines.h new file mode 100644 index 0000000..3a6020a --- /dev/null +++ b/src/afc-engine/global_defines.h @@ -0,0 +1,18 @@ +/******************************************************************************************/ +/**** FILE: global_defines.h ****/ +/******************************************************************************************/ + +#ifndef GLOBAL_DEFINES_H +#define GLOBAL_DEFINES_H + +#define MAX_LINE_SIZE 5000 +#define CHDELIM " \t\n" /* Delimiting characters, used for string parsing */ + +#define IVECTOR(nn) (int *)((nn) ? malloc((nn) * sizeof(int)) : NULL) +#define DVECTOR(nn) (double *)((nn) ? malloc((nn) * sizeof(double)) : NULL) +#define CVECTOR(nn) (char *)((nn) ? malloc(((nn) + 1) * sizeof(char)) : NULL) + +#define CORE_DUMP printf("%d", *((int *)NULL)) +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/global_fn.cpp b/src/afc-engine/global_fn.cpp new file mode 100644 index 0000000..7dbd82f --- /dev/null +++ b/src/afc-engine/global_fn.cpp @@ -0,0 +1,225 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ + #include +#else + #include + #include + #include + #include +#endif + +#include "global_fn.h" + +/******************************************************************************************/ +/**** Read a line into string s, return length. From "C Programming Language" Pg. 29 ****/ +/**** Modified to be able to read both DOS and UNIX files. ****/ +/**** 2013.03.11: use std::string so not necessary to pre-allocate storage. ****/ +/**** Return value is number of characters read from FILE, which may or may not equal ****/ +/**** the length of string s depending on whether '\r' or '\n' has been removed from ****/ +/**** the string. ****/ +/******************************************************************************************/ +int fgetline(FILE *file, std::string &s, bool keepcr) +{ + int c, i; + + s.clear(); + for (i = 0; (c = fgetc(file)) != EOF && c != '\n'; i++) { + s += c; + } + if ((i >= 1) && (s[i - 1] == '\r')) { + s.erase(i - 1, 1); + // i--; + } + if (c == '\n') { + if (keepcr) { + s += c; + } + i++; + } + return (i); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Read a line into string s, return length. From "C Programming Language" Pg. 29 ****/ +/**** Modified to be able to read both DOS and UNIX files. ****/ +/******************************************************************************************/ +int fgetline(FILE *file, char *s) +{ + int c, i; + + for (i = 0; (c = fgetc(file)) != EOF && c != '\n'; i++) { + s[i] = c; + } + if ((i >= 1) && (s[i - 1] == '\r')) { + i--; + } + if (c == '\n') { + s[i] = c; + i++; + } + s[i] = '\0'; + return (i); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Split string into vector of strings using specified delim. ****/ +/******************************************************************************************/ +std::vector &split(const std::string &s, char delim, std::vector &elems) +{ + std::stringstream ss(s); + std::string item; + while (std::getline(ss, item, delim)) { + elems.push_back(item); + } + return elems; +} + +std::vector split(const std::string &s, char delim) +{ + std::vector elems; + split(s, delim, elems); + return elems; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Split string into vector of strings for each CSV field properly treating fields ****/ +/**** enclosed in double quotes with embedded commas. Also, double quotes can be ****/ +/**** embedded in a field using 2 double quotes "". ****/ +/**** This format is compatible with excel and libreoffice CSV files. ****/ +/******************************************************************************************/ +std::vector splitCSV(const std::string &line) +{ + int i, fieldLength; + int fieldStartIdx = -1; + std::ostringstream s; + std::vector elems; + std::string field; + bool skipChar = false; + + // state 0 = looking for next field + // state 1 = found beginning of field, not ", looking for end of field (comma) + // state 2 = found beginning of field, is ", looking for end of field ("). + // state 3 = found end of field, is ", pass over 0 or more spaces until (comma) + int state = 0; + + for (i = 0; i < (int)line.length(); i++) { + if (skipChar) { + skipChar = false; + } else { + switch (state) { + case 0: + if (line.at(i) == '\"') { + fieldStartIdx = i + 1; + state = 2; + } else if (line.at(i) == ',') { + field.clear(); + elems.push_back(field); + } else if (line.at(i) == ' ') { + // do nothing + } else { + fieldStartIdx = i; + state = 1; + } + break; + case 1: + if (line.at(i) == ',') { + fieldLength = i - fieldStartIdx; + field = line.substr(fieldStartIdx, fieldLength); + + std::size_t start = field.find_first_not_of(" \n" + "\t"); + std::size_t end = field.find_last_not_of(" \n\t"); + if (start == std::string::npos) { + field.clear(); + } else { + field = field.substr(start, + end - start + 1); + } + + elems.push_back(field); + state = 0; + } + break; + case 2: + if (line.at(i) == '\"') { + if ((i + 1 < (int)line.length()) && + (line.at(i + 1) == '\"')) { + skipChar = true; + } else { + fieldLength = i - fieldStartIdx; + field = line.substr(fieldStartIdx, + fieldLength); + std::size_t k = field.find("\"\""); + while (k != std::string::npos) { + field.erase(k, 1); + k = field.find("\"\""); + } + elems.push_back(field); + state = 3; + } + } + break; + case 3: + if (line.at(i) == ' ') { + // do nothing + } else if (line.at(i) == ',') { + state = 0; + } else { + s << "ERROR: Unable to splitCSV() for command \"" + << line << "\" invalid quotes.\n"; + throw std::runtime_error(s.str()); + } + break; + default: + CORE_DUMP; + break; + } + } + if (i == ((int)line.length()) - 1) { + if (state == 0) { + field.clear(); + elems.push_back(field); + } else if (state == 1) { + fieldLength = i - fieldStartIdx + 1; + field = line.substr(fieldStartIdx, fieldLength); + + std::size_t start = field.find_first_not_of(" \n\t"); + std::size_t end = field.find_last_not_of(" \n\t"); + if (start == std::string::npos) { + field.clear(); + } else { + field = field.substr(start, end - start + 1); + } + + elems.push_back(field); + state = 0; + } else if (state == 2) { + s << "ERROR: Unable to splitCSV() for command \"" << line + << "\" unmatched quote.\n"; + throw std::runtime_error(s.str()); + } else if (state == 3) { + state = 0; + } + } + } + + if (state != 0) { + CORE_DUMP; + } + + return elems; +} +/******************************************************************************************/ diff --git a/src/afc-engine/global_fn.h b/src/afc-engine/global_fn.h new file mode 100644 index 0000000..bfbc2ca --- /dev/null +++ b/src/afc-engine/global_fn.h @@ -0,0 +1,18 @@ +/******************************************************************************************/ +/**** FILE: global_fn.h ****/ +/******************************************************************************************/ + +#ifndef GLOBAL_FN_H +#define GLOBAL_FN_H + +#include +#include +#include +#include "global_defines.h" + +int fgetline(FILE *file, std::string &s, bool keepcr = true); +int fgetline(FILE *, char *); +std::vector split(const std::string &s, char delim); +std::vector splitCSV(const std::string &cmd); + +#endif diff --git a/src/afc-engine/lidar_blacklist_entry.cpp b/src/afc-engine/lidar_blacklist_entry.cpp new file mode 100644 index 0000000..d14a638 --- /dev/null +++ b/src/afc-engine/lidar_blacklist_entry.cpp @@ -0,0 +1,59 @@ +/******************************************************************************************/ +/**** FILE : channel_pair.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#include + +#include "lidar_blacklist_entry.h" +#include "EcefModel.h" + +/******************************************************************************************/ +/**** FUNCTION: LidarBlacklistEntryClass::LidarBlacklistEntryClass() ****/ +/******************************************************************************************/ +LidarBlacklistEntryClass::LidarBlacklistEntryClass(double lonDegVal, + double latDegVal, + double radiusMeterVal) : + lonDeg(lonDegVal), latDeg(latDegVal), radiusMeter(radiusMeterVal) +{ + centerPosition = EcefModel::geodeticToEcef(latDeg, lonDeg, 0.0); + radiusSqKm = radiusMeter * radiusMeter * 1.0e-6; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: LidarBlacklistEntryClass::~LidarBlacklistEntryClass() ****/ +/******************************************************************************************/ +LidarBlacklistEntryClass::~LidarBlacklistEntryClass() +{ +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: LidarBlacklistEntryClass::contains() ****/ +/******************************************************************************************/ +bool LidarBlacklistEntryClass::contains(double ptLonDeg, double ptLatDeg) +{ + bool cFlag; + + Vector3 ptPosition = EcefModel::geodeticToEcef(ptLatDeg, ptLonDeg, 0.0); + Vector3 u = ptPosition - centerPosition; + + if (u.dot(u) < radiusSqKm) { + cFlag = true; + // std::cout << "PT: " << ptLatDeg << " " << ptLonDeg << " BLACKLIST_CENTER: " + // << latDeg << " " << lonDeg << " DIST = " << u.len()*1000 << " BLACKLIST_RAD + // = " << radiusMeter << std::endl; + } else { + cFlag = false; + } + + return (cFlag); +} +/******************************************************************************************/ diff --git a/src/afc-engine/lidar_blacklist_entry.h b/src/afc-engine/lidar_blacklist_entry.h new file mode 100644 index 0000000..47ebe6b --- /dev/null +++ b/src/afc-engine/lidar_blacklist_entry.h @@ -0,0 +1,27 @@ +/******************************************************************************************/ +/**** FILE : lidar_blacklist_entry.h ****/ +/******************************************************************************************/ + +#ifndef LIDAR_BLACKLIST_ENTRY_H +#define LIDAR_BLACKLIST_ENTRY_H + +#include "Vector3.h" + +/******************************************************************************************/ +/**** CLASS: LidarBlacklistEntryClass ****/ +/******************************************************************************************/ +class LidarBlacklistEntryClass +{ + public: + LidarBlacklistEntryClass(double lonDegVal, double latDegVal, double radiusMeterVal); + ~LidarBlacklistEntryClass(); + + bool contains(double ptLonDeg, double ptLatDeg); + + private: + double lonDeg, latDeg, radiusMeter, radiusSqKm; + Vector3 centerPosition; +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/lininterp.cpp b/src/afc-engine/lininterp.cpp new file mode 100644 index 0000000..7dd1dc1 --- /dev/null +++ b/src/afc-engine/lininterp.cpp @@ -0,0 +1,215 @@ +/******************************************************************************************/ +/**** FILE: lininterp.cpp ****/ +/******************************************************************************************/ +#include +#include +#include + +#include "global_defines.h" +#include "list.h" +#include "lininterp.h" + +/******************************************************************************************/ +/**** CONSTRUCTOR: LinInterpClass::LinInterpClass ****/ +/******************************************************************************************/ +LinInterpClass::LinInterpClass(ListClass *dataList, double xshift, double yshift) +{ + n = dataList->getSize(); + + a = DVECTOR(n - 1); + b = DVECTOR(n - 1); + x = DVECTOR(n); + + makelininterpcoeffs(dataList, xshift, yshift); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: LinInterpClass::LinInterpClass ****/ +/******************************************************************************************/ +LinInterpClass::LinInterpClass(std::vector> dataList, + double xshift, + double yshift) +{ + n = dataList.size(); + + a = DVECTOR(n - 1); + b = DVECTOR(n - 1); + x = DVECTOR(n); + + makelininterpcoeffs(dataList, xshift, yshift); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: LinInterpClass::~LinInterpClass ****/ +/******************************************************************************************/ +LinInterpClass::~LinInterpClass() +{ + free(a); + free(b); + free(x); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: LinInterpClass::makelininterpcoeffs ****/ +/**** Return value: 1 if successfull, 0 if not successful ****/ +/******************************************************************************************/ +int LinInterpClass::makelininterpcoeffs(ListClass *dataList, + double xshift, + double yshift) +{ + int i; + double x0, x1, y0, y1; + + x0 = (*dataList)[0].x() + xshift; + y0 = (*dataList)[0].y() + yshift; + x[0] = x0; + for (i = 1; i <= dataList->getSize() - 1; i++) { + x1 = (*dataList)[i].x() + xshift; + y1 = (*dataList)[i].y() + yshift; + x[i] = x1; + + a[i - 1] = y0; + b[i - 1] = (y1 - y0) / (x1 - x0); + + x0 = x1; + y0 = y1; + } + + return (1); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: LinInterpClass::makelininterpcoeffs ****/ +/**** Return value: 1 if successfull, 0 if not successful ****/ +/******************************************************************************************/ +int LinInterpClass::makelininterpcoeffs(std::vector> dataList, + double xshift, + double yshift) +{ + int i; + double x0, x1, y0, y1; + + x0 = std::get<0>(dataList[0]) + xshift; + y0 = std::get<1>(dataList[0]) + yshift; + x[0] = x0; + for (i = 1; i <= (int)dataList.size() - 1; i++) { + x1 = std::get<0>(dataList[i]) + xshift; + y1 = std::get<1>(dataList[i]) + yshift; + x[i] = x1; + + a[i - 1] = y0; + b[i - 1] = (y1 - y0) / (x1 - x0); + + x0 = x1; + y0 = y1; + } + + return (1); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: LinInterpClass::lininterpval ****/ +/**** this evaluates the linear interpolation curve as defined by the last call to ****/ +/**** makeslininterpoeffs. ****/ +/**** ****/ +/**** xpoint = desired function argument ****/ +/**** ****/ +/******************************************************************************************/ +double LinInterpClass::lininterpval(double xpoint) const +{ + int s = -1; + double h, z; + + if ((xpoint >= x[0]) && (xpoint <= x[n - 1])) { + s = lininterp_getintindex(xpoint); + } else if (xpoint > x[n - 1]) { + s = n - 2; + } else if (xpoint < x[0]) { + s = 0; + } else { + fprintf(stdout, "ERROR in routine lininterpval()\n"); + fprintf(stdout, "input x out of range in lininterp evaluation routine.\n"); + fprintf(stdout, "input x = %5.3f\n", xpoint); + fprintf(stdout, "first, last in array = %5.3f, %5.3f\n", x[0], x[n - 1]); + fprintf(stdout, "\n"); + CORE_DUMP; + } + + h = xpoint - x[s]; + z = b[s] * h + a[s]; + + return (z); +} +/******************************************************************************************/ +/**** FUNCTION: LinInterpClass::lininterpDerivativeVal ****/ +/**** Evaluated the first derivative of the lininterp. ****/ +/**** ****/ +/**** xpoint = desired function argument ****/ +/**** ****/ +/******************************************************************************************/ +double LinInterpClass::lininterpDerivativeVal(double xpoint) const +{ + int s = -1; + double z; + + if ((xpoint >= x[0]) && (xpoint <= x[n - 1])) { + s = lininterp_getintindex(xpoint); + } else if (xpoint > x[n - 1]) { + s = n - 2; + } else if (xpoint < x[0]) { + s = 0; + } else { + fprintf(stdout, "ERROR in routine lininterpval()\n"); + fprintf(stdout, "input x out of range in lininterp evaluation routine.\n"); + fprintf(stdout, "input x = %5.3f\n", xpoint); + fprintf(stdout, "first, last in array = %5.3f, %5.3f\n", x[0], x[n - 1]); + fprintf(stdout, "\n"); + CORE_DUMP; + } + + z = b[s]; + + return (z); +} +/******************************************************************************************/ +/**** FUNCTION: LinInterpClass::lininterp_getintindex ****/ +/******************************************************************************************/ +int LinInterpClass::lininterp_getintindex(double xtest) const +{ + int lowind, upind, testind, index; + + if ((xtest < x[0]) || (xtest > x[n - 1])) { + fprintf(stdout, "ERROR in routine getintindex()\n"); + fprintf(stdout, "input x out of range.\n"); + fprintf(stdout, "x =%5.3f\n", xtest); + fprintf(stdout, "range = %5.3f, %5.3f\n", x[0], x[n - 1]); + fprintf(stdout, "\n"); + CORE_DUMP; + } + + lowind = 0; + upind = n - 1; + + do { + testind = (lowind + upind) / 2; + if (xtest > x[testind]) { + lowind = testind; + } else { + upind = testind; + } + } while (upind - lowind > 1); + + if (xtest > x[upind]) { + index = upind; + } else { + index = lowind; + } + + return (index); +} +/******************************************************************************************/ diff --git a/src/afc-engine/lininterp.h b/src/afc-engine/lininterp.h new file mode 100644 index 0000000..814ab24 --- /dev/null +++ b/src/afc-engine/lininterp.h @@ -0,0 +1,44 @@ +/******************************************************************************************/ +/**** FILE: lininterp.h ****/ +/******************************************************************************************/ + +#ifndef LININTERP_H +#define LININTERP_H + +#include +#include +#include "dbldbl.h" + +template +class ListClass; + +/******************************************************************************************/ +/**** CLASS: LinInterpClass ****/ +/******************************************************************************************/ +class LinInterpClass +{ + public: + LinInterpClass(ListClass *dataList, + double xshift = 0.0, + double yshift = 0.0); + LinInterpClass(std::vector> dataList, + double xshift = 0.0, + double yshift = 0.0); + ~LinInterpClass(); + double lininterpval(double) const; + double lininterpDerivativeVal(double xpoint) const; + + private: + double *a, *b, *x; + int n; + int lininterp_getintindex(double) const; + int makelininterpcoeffs(ListClass *dataList, + double xshift, + double yshift); + int makelininterpcoeffs(std::vector> dataList, + double xshift, + double yshift); +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/list.cpp b/src/afc-engine/list.cpp new file mode 100644 index 0000000..52ffe17 --- /dev/null +++ b/src/afc-engine/list.cpp @@ -0,0 +1,1232 @@ +/******************************************************************************************/ +/**** FILE: list.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include "list.h" +#include "global_fn.h" + +#define CORE_DUMP printf("%d", *((int *)NULL)) + +/******************************************************************************************/ +/**** ListClass::ListClass ****/ +/******************************************************************************************/ +template +ListClass::ListClass(int n, int incr) +{ + a = (T *)malloc(n * sizeof(T)); + allocation_size = n; + if (incr <= 0) { + printf("ERROR in routine ListClass::ListClass(): allocation_increment = %d, must " + "be > 0\n", + incr); + CORE_DUMP; + } + allocation_increment = incr; + + size = 0; +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** ListClass::~ListClass ****/ +/******************************************************************************************/ +template +ListClass::~ListClass() +{ + free(a); +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** ListClass::operator[] ****/ +/******************************************************************************************/ +template +T &ListClass::operator[](int index) const +{ + return a[index]; +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** ListClass::operator== ****/ +/******************************************************************************************/ +template +int ListClass::operator==(ListClass &val) +{ + int i; + int equal = 1; + + if (size != val.getSize()) { + equal = 0; + } + + for (i = 0; i <= size - 1; i++) { + if (!(a[i] == val[i])) { + equal = 0; + } + } + + return (equal); +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** ListClass::getSize ****/ +/******************************************************************************************/ +template +int ListClass::getSize() const +{ + return (size); +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::ins_elem ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Inserts val into {list[0], list[1], ..., list[n-1]} ****/ +/**** If val is already in list and err=0 do nothing, else give ERROR and exit ****/ +/**** The value returned is the position of val in the list. ****/ +/******************************************************************************************/ +template +int ListClass::ins_elem(T val, const int err, int *insFlagPtr) +{ + int i, found; + int retval = -1; + + if (insFlagPtr) { + *insFlagPtr = 0; + } + + found = 0; + for (i = size - 1; (i >= 0) && (!found); i--) { + if (a[i] == val) { + found = 1; + retval = i; + } + } + + if (found) { + if (err) { + printf("ERROR in routine ListClass::ins_elem()\n"); + CORE_DUMP; + } + } else { + retval = size; + if ((size == allocation_size)) { + allocation_size += allocation_increment; + a = (T *)realloc((void *)a, allocation_size * sizeof(T)); + } + a[size++] = val; + if (insFlagPtr) { + *insFlagPtr = 1; + } + } + return (retval); +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::append ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Adds val to the end of {list[0], list[1], ..., list[n-1]} ****/ +/**** There is no check whether or not val is already in the list. ****/ +/******************************************************************************************/ +template +void ListClass::append(T val) +{ + if ((size == allocation_size)) { + allocation_size += allocation_increment; + a = (T *)realloc((void *)a, allocation_size * sizeof(T)); + } + + a[size++] = val; + + return; +}; +/******************************************************************************************/ +/**** FUNCTION: ListClass::del_elem ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Removes val from {list[0], list[1], ..., list[n-1]} ****/ +/**** If val is not in list and err=0 do nothing, else give ERROR and exit ****/ +/******************************************************************************************/ +template +void ListClass::del_elem(T val, const int err) +{ + int i; + + for (i = size - 1; i >= 0; i--) { + if (a[i] == val) { + a[i] = a[size - 1]; + size--; + return; + } + } + + if (err) { + printf("ERROR in routine del_elem()\n"); + CORE_DUMP; + } + return; +}; + +template +void ListClass::del_elem_idx(int index, const int err) +{ + if ((index < 0) || (index > size - 1)) { + if (err) { + printf("ERROR in routine del_elem_idx(): index out of range, index = %d " + "outside [0,%d]\n", + index, + size - 1); + CORE_DUMP; + } else { + return; + } + } + + a[index] = a[size - 1]; + size--; + + return; +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::toggle_elem ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** If val is not in the list, then add it; and if val is in the list, then remove ****/ +/**** it. ****/ +/******************************************************************************************/ +template +void ListClass::toggle_elem(T val) +{ + int i; + + for (i = size - 1; i >= 0; i--) { + if (a[i] == val) { + a[i] = a[size - 1]; + size--; + return; + } + } + + if ((size == allocation_size)) { + allocation_size += allocation_increment; + a = (T *)realloc((void *)a, allocation_size * sizeof(T)); + } + + a[size++] = val; + + return; +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** ListClass::pop ****/ +/******************************************************************************************/ +template +T ListClass::pop() +{ + T val; + + if (size) { + val = a[size - 1]; + size--; + } else { + printf("ERROR in routine pop(), can't pop list of size zero.\n"); + CORE_DUMP; + } + + return val; +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::reset ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Removes all values from the list, sets allocation_size to zero. ****/ +/******************************************************************************************/ +template +void ListClass::reset() +{ + size = 0; + + if (allocation_size) { + allocation_size = 0; + a = (T *)realloc((void *)a, allocation_size * sizeof(T)); + } + + return; +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::resize ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Resize allocation_size to the specified value, but can't be less than the size ****/ +/**** of the list ****/ +/******************************************************************************************/ +template +void ListClass::resize(int n) +{ + int new_alloc_size; + + if (n <= size) { + new_alloc_size = size; + } else { + new_alloc_size = n; + } + + if (allocation_size != new_alloc_size) { + allocation_size = new_alloc_size; + a = (T *)realloc((void *)a, allocation_size * sizeof(T)); + } + + return; +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::get_index ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Given list {list[0], list[1], ..., list[n-1]} and value val, return index i such ****/ +/**** that list[i] = val. If val is not in list and err=0 return -1, else give ERROR ****/ +/**** and exit ****/ +/******************************************************************************************/ +template +int ListClass::get_index(T val, const int err) const +{ + int i, found; + int retval = -1; + + found = 0; + for (i = size - 1; (i >= 0) && (!found); i--) { + if (a[i] == val) { + found = 1; + retval = i; + } + } + + if (!found) { + if (err) { + printf("ERROR in routine get_index()\n"); + CORE_DUMP; + } + } + + return (retval); +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::contains ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Given list {list[0], list[1], ..., list[n-1]} and value val, return 1 if val is ****/ +/**** contained in the list and 0 otherwise. ****/ +/******************************************************************************************/ +template +int ListClass::contains(T val) const +{ + int i, found; + + found = 0; + for (i = size - 1; (i >= 0) && (!found); i--) { + if (a[i] == val) { + found = 1; + } + } + + return (found); +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::get_idx_sorted ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Given the SORTED list {list[0], list[1], ..., list[n-1]} and value val, return ****/ +/**** index i such that list[i] = val. If val is not in list and err=0 return -1, ****/ +/**** else give ERROR and exit. ****/ +/******************************************************************************************/ +template +int get_index_sorted(ListClass *lc_t, T val, const int err) +{ + int i, i_min, i_max, done, found, retval; + + found = 0; + done = 0; + retval = -1; + + i_min = 0; + if (lc_t->getSize() == 0) { + done = 1; + } else if ((*lc_t)[i_min] == val) { + done = 1; + found = 1; + retval = i_min; + } else if ((*lc_t)[i_min] > val) { + done = 1; + } + + if (!done) { + i_max = lc_t->getSize() - 1; + if ((*lc_t)[i_max] == val) { + done = 1; + found = 1; + retval = i_max; + } else if (val > (*lc_t)[i_max]) { + done = 1; + } + } + + if (!done) { + if (lc_t->getSize() <= 2) { + done = 1; + } + } + + while (!done) { + i = (i_min + i_max) / 2; + if ((*lc_t)[i] == val) { + done = 1; + found = 1; + retval = i; + } else if (val > (*lc_t)[i]) { + i_min = i; + } else { + i_max = i; + } + + if (i_max <= i_min + 1) { + done = 1; + } + } + + if (!found) { + if (err) { + printf("ERROR in routine get_index_sorted()\n"); + CORE_DUMP; + } + } + + return (retval); +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::contains_sorted ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Given the SORTED list {list[0] < list[1] < ... < list[n-1]} and value val, ****/ +/**** return 1 if val is contained in the list and 0 otherwise. ****/ +/******************************************************************************************/ +template +int contains_sorted(ListClass *lc_t, T val) +{ + if (get_index_sorted(lc_t, val, 0) == -1) { + return (0); + } else { + return (1); + } +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::ins_elem_sorted ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Inserts val into the SORTED list {list[0], list[1], ..., list[n-1]} ****/ +/**** If val is already in list and err=0 do nothing, else give ERROR and exit ****/ +/**** The value returned is the position of val in the list. ****/ +/******************************************************************************************/ +template +int ins_elem_sorted(ListClass *lc_t, T val, const int err, int *insFlagPtr) +{ + int i, i_min, i_max, done, found, retval; + + found = 0; + done = 0; + retval = -1; + + i_min = 0; + if (lc_t->getSize() == 0) { + i_min = -1; + i_max = i_min + 1; + done = 1; + } else if ((*lc_t)[i_min] == val) { + done = 1; + found = 1; + retval = i_min; + } else if ((*lc_t)[i_min] > val) { + i_min = -1; + i_max = i_min + 1; + done = 1; + } + + if (!done) { + i_max = lc_t->getSize() - 1; + if ((*lc_t)[i_max] == val) { + done = 1; + found = 1; + retval = i_max; + } else if (val > (*lc_t)[i_max]) { + i_min = lc_t->getSize() - 1; + i_max = i_min + 1; + done = 1; + } + } + + if (!done) { + if (lc_t->getSize() <= 2) { + done = 1; + } + } + + while (!done) { + i = (i_min + i_max) / 2; + if ((*lc_t)[i] == val) { + done = 1; + found = 1; + retval = i; + } else if (val > (*lc_t)[i]) { + i_min = i; + } else { + i_max = i; + } + + if (i_max <= i_min + 1) { + done = 1; + } + } + + if (!found) { + lc_t->append(val); + + for (i = lc_t->getSize() - 1; i >= i_max + 1; i--) { + (*lc_t)[i] = (*lc_t)[i - 1]; + } + + (*lc_t)[i_max] = val; + } else if (err) { + printf("ERROR in routine get_index_sorted()\n"); + CORE_DUMP; + } + + if (insFlagPtr) { + *insFlagPtr = (found ? 0 : 1); + } + + return (retval); +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::sort ****/ +/**** INPUTS: compare -- comparator function (kind of 'less' ****/ +/**** OUTPUTS: ****/ +/**** Prints values of {list[0], list[1], ..., list[n-1]} ****/ +template +void ListClass::sort(const std::function &compare) +{ + std::sort(a, a + size, compare); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::printlist ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Prints values of {list[0], list[1], ..., list[n-1]} ****/ +template +void ListClass::printlist(int n, const char *elemSep, const char *grpSep, const char *endStr) +{ + int i; + + for (i = 0; i <= size - 1; i++) { + std::cout << a[i]; + if (i != size - 1) { + if (i % n == n - 1) { + if (grpSep) { + std::cout << grpSep; + } else { + std::cout << "\n "; + } + } else { + if (elemSep) { + std::cout << elemSep; + } else { + std::cout << " "; + } + } + } + } + if (endStr) { + std::cout << endStr; + } else { + std::cout << std::endl; + } + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::reverse ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Reverse the order of all values in the list. ****/ +template +void ListClass::reverse() +{ + int i; + T tmp; + + for (i = 0; i <= (size / 2) - 1; i++) { + tmp = a[i]; + a[i] = a[size - 1 - i]; + a[size - 1 - i] = tmp; + } + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::sort ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Numerical Recipes in C: Modification of sort.c to sort integer array. ****/ +/**** Modified to use zero-based vectors and translated to C++. ****/ +/******************************************************************************************/ +#define SWAP(a, b) \ + temp = (a); \ + (a) = (b); \ + (b) = temp; +#define M 7 +#define NSTACK 50 + +template +void sort(ListClass *lc_t) +{ + int i, j, k, l = 1; + int jstack = 0, *istack; + T vara, temp; + + int ir = lc_t->getSize(); + + istack = (int *)malloc(NSTACK * sizeof(int)); + for (;;) { + if (ir - l < M) { + for (j = l + 1; j <= ir; j++) { + vara = (*lc_t)[j - 1]; + for (i = j - 1; i >= 1; i--) { + if (!((*lc_t)[i - 1] > vara)) + break; + (*lc_t)[i] = (*lc_t)[i - 1]; + } + (*lc_t)[i] = vara; + } + if (jstack == 0) { + free(istack); + return; + } + ir = istack[jstack - 1]; + l = istack[jstack - 2]; + jstack -= 2; + } else { + k = (l + ir) >> 1; + SWAP((*lc_t)[k - 1], (*lc_t)[l]) + if ((*lc_t)[l] > (*lc_t)[ir - 1]) { + SWAP((*lc_t)[l], (*lc_t)[ir - 1]) + } + if ((*lc_t)[l - 1] > (*lc_t)[ir - 1]) { + SWAP((*lc_t)[l - 1], (*lc_t)[ir - 1]) + } + if ((*lc_t)[l] > (*lc_t)[l - 1]) { + SWAP((*lc_t)[l], (*lc_t)[l - 1]) + } + i = l + 1; + j = ir; + vara = (*lc_t)[l - 1]; + for (;;) { + do + i++; + while (vara > (*lc_t)[i - 1]); + do + j--; + while ((*lc_t)[j - 1] > vara); + if (j < i) + break; + SWAP((*lc_t)[i - 1], (*lc_t)[j - 1]); + } + (*lc_t)[l - 1] = (*lc_t)[j - 1]; + (*lc_t)[j - 1] = vara; + jstack += 2; + if (jstack > NSTACK) { + printf("NSTACK too small in isort."); + exit(1); + } + if (ir - i + 1 >= j - l) { + istack[jstack - 1] = ir; + istack[jstack - 2] = i; + ir = j - 1; + } else { + istack[jstack - 1] = j - 1; + istack[jstack - 2] = l; + l = i; + } + } + } +} +#undef M +#undef NSTACK +#undef SWAP +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::sort2 ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Numerical Recipes in C: Modification of sort2.c to sort integer array, while ****/ +/**** making corresponding rearrangements in another integer array. Overloaded to ****/ +/**** treat double, int ****/ +/**** Modified to use zero-based vectors and translated to C++. ****/ +/******************************************************************************************/ + +#define TSWAP(a, b) \ + tempa = (a); \ + (a) = (b); \ + (b) = tempa; +#define USWAP(a, b) \ + tempb = (a); \ + (a) = (b); \ + (b) = tempb; +#define M 7 +#define NSTACK 50 + +template +void sort2(ListClass *lc_t, ListClass *lc_u) +{ + int i, j, k, l = 1; + int *istack, jstack = 0; + T vara, tempa; + U varb, tempb; + + int ir = lc_t->getSize(); + + if (lc_u->getSize() != lc_t->getSize()) { + printf("ERROR in routine ListClass::sort2(), lists are of unequal length\n"); + CORE_DUMP; + } + + istack = (int *)malloc((NSTACK + 1) * sizeof(int)); + for (;;) { + if (ir - l < M) { + for (j = l + 1; j <= ir; j++) { + vara = (*lc_t)[j - 1]; + varb = (*lc_u)[j - 1]; + for (i = j - 1; i >= 1; i--) { + if (!((*lc_t)[i - 1] > vara)) + break; + (*lc_t)[i] = (*lc_t)[i - 1]; + (*lc_u)[i] = (*lc_u)[i - 1]; + } + (*lc_t)[i] = vara; + (*lc_u)[i] = varb; + } + if (!jstack) { + free(istack); + return; + } + ir = istack[jstack - 1]; + l = istack[jstack - 2]; + jstack -= 2; + } else { + k = (l + ir) >> 1; + TSWAP((*lc_t)[k - 1], (*lc_t)[l]) + USWAP((*lc_u)[k - 1], (*lc_u)[l]) + if ((*lc_t)[l] > (*lc_t)[ir - 1]) { + TSWAP((*lc_t)[l], (*lc_t)[ir - 1]) + USWAP((*lc_u)[l], (*lc_u)[ir - 1]) + } + if ((*lc_t)[l - 1] > (*lc_t)[ir - 1]) { + TSWAP((*lc_t)[l - 1], (*lc_t)[ir - 1]) + USWAP((*lc_u)[l - 1], (*lc_u)[ir - 1]) + } + if ((*lc_t)[l] > (*lc_t)[l - 1]) { + TSWAP((*lc_t)[l], (*lc_t)[l - 1]) + USWAP((*lc_u)[l], (*lc_u)[l - 1]) + } + i = l + 1; + j = ir; + vara = (*lc_t)[l - 1]; + varb = (*lc_u)[l - 1]; + for (;;) { + do + i++; + while (vara > (*lc_t)[i - 1]); + do + j--; + while ((*lc_t)[j - 1] > vara); + if (j < i) + break; + TSWAP((*lc_t)[i - 1], (*lc_t)[j - 1]) + USWAP((*lc_u)[i - 1], (*lc_u)[j - 1]) + } + (*lc_t)[l - 1] = (*lc_t)[j - 1]; + (*lc_t)[j - 1] = vara; + (*lc_u)[l - 1] = (*lc_u)[j - 1]; + (*lc_u)[j - 1] = varb; + jstack += 2; + if (jstack > NSTACK) { + printf("NSTACK too small in sort2."); + exit(1); + } + if (ir - i + 1 >= j - l) { + istack[jstack - 1] = ir; + istack[jstack - 2] = i; + ir = j - 1; + } else { + istack[jstack - 1] = j - 1; + istack[jstack - 2] = l; + l = i; + } + } + } +} + +#undef M +#undef NSTACK +#undef TSWAP +#undef USWAP +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::sort3 ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Numerical Recipes in C: Modification of sort2.c to sort integer array, while ****/ +/**** making corresponding rearrangements in another integer array. ****/ +/**** Modified to use zero-based vectors and translated to C++. ****/ +/******************************************************************************************/ + +#define TSWAP(a, b) \ + tempa = (a); \ + (a) = (b); \ + (b) = tempa; +#define USWAP(a, b) \ + tempb = (a); \ + (a) = (b); \ + (b) = tempb; +#define VSWAP(a, b) \ + tempc = (a); \ + (a) = (b); \ + (b) = tempc; +#define M 7 +#define NSTACK 50 + +template +void sort3(ListClass *lc_t, ListClass *lc_u, ListClass *lc_v) +{ + int i, j, k, l = 1; + int *istack, jstack = 0; + T vara, tempa; + U varb, tempb; + V varc, tempc; + + int ir = lc_t->getSize(); + + if ((lc_u->getSize() != ir) || (lc_v->getSize() != ir)) { + printf("ERROR in routine ListClass::sort3(), lists are of unequal length\n"); + CORE_DUMP; + } + + istack = (int *)malloc((NSTACK + 1) * sizeof(int)); + for (;;) { + if (ir - l < M) { + for (j = l + 1; j <= ir; j++) { + vara = (*lc_t)[j - 1]; + varb = (*lc_u)[j - 1]; + varc = (*lc_v)[j - 1]; + for (i = j - 1; i >= 1; i--) { + if (!((*lc_t)[i - 1] > vara)) + break; + (*lc_t)[i] = (*lc_t)[i - 1]; + (*lc_u)[i] = (*lc_u)[i - 1]; + (*lc_v)[i] = (*lc_v)[i - 1]; + } + (*lc_t)[i] = vara; + (*lc_u)[i] = varb; + (*lc_v)[i] = varc; + } + if (!jstack) { + free(istack); + return; + } + ir = istack[jstack - 1]; + l = istack[jstack - 2]; + jstack -= 2; + } else { + k = (l + ir) >> 1; + TSWAP((*lc_t)[k - 1], (*lc_t)[l]) + USWAP((*lc_u)[k - 1], (*lc_u)[l]) + VSWAP((*lc_v)[k - 1], (*lc_v)[l]) + if ((*lc_t)[l] > (*lc_t)[ir - 1]) { + TSWAP((*lc_t)[l], (*lc_t)[ir - 1]) + USWAP((*lc_u)[l], (*lc_u)[ir - 1]) + VSWAP((*lc_v)[l], (*lc_v)[ir - 1]) + } + if ((*lc_t)[l - 1] > (*lc_t)[ir - 1]) { + TSWAP((*lc_t)[l - 1], (*lc_t)[ir - 1]) + USWAP((*lc_u)[l - 1], (*lc_u)[ir - 1]) + VSWAP((*lc_v)[l - 1], (*lc_v)[ir - 1]) + } + if ((*lc_t)[l] > (*lc_t)[l - 1]) { + TSWAP((*lc_t)[l], (*lc_t)[l - 1]) + USWAP((*lc_u)[l], (*lc_u)[l - 1]) + VSWAP((*lc_v)[l], (*lc_v)[l - 1]) + } + i = l + 1; + j = ir; + vara = (*lc_t)[l - 1]; + varb = (*lc_u)[l - 1]; + varc = (*lc_v)[l - 1]; + for (;;) { + do + i++; + while (vara > (*lc_t)[i - 1]); + do + j--; + while ((*lc_t)[j - 1] > vara); + if (j < i) + break; + TSWAP((*lc_t)[i - 1], (*lc_t)[j - 1]) + USWAP((*lc_u)[i - 1], (*lc_u)[j - 1]) + VSWAP((*lc_v)[i - 1], (*lc_v)[j - 1]) + } + (*lc_t)[l - 1] = (*lc_t)[j - 1]; + (*lc_t)[j - 1] = vara; + (*lc_u)[l - 1] = (*lc_u)[j - 1]; + (*lc_u)[j - 1] = varb; + (*lc_v)[l - 1] = (*lc_v)[j - 1]; + (*lc_v)[j - 1] = varc; + jstack += 2; + if (jstack > NSTACK) { + printf("NSTACK too small in sort2."); + exit(1); + } + if (ir - i + 1 >= j - l) { + istack[jstack - 1] = ir; + istack[jstack - 2] = i; + ir = j - 1; + } else { + istack[jstack - 1] = j - 1; + istack[jstack - 2] = l; + l = i; + } + } + } +} + +#undef M +#undef NSTACK +#undef TSWAP +#undef USWAP +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::ins_pointer ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Same as ins_elem except values are dereferenced. ****/ +/******************************************************************************************/ +template +int ins_pointer(ListClass *lc_t, T &val, const int err, int *insFlagPtr) +{ + int i, found; + int insIdx = -1; + + if (insFlagPtr) { + *insFlagPtr = 0; + } + + found = 0; + for (i = lc_t->getSize() - 1; (i >= 0) && (!found); i--) { + if (*((*lc_t)[i]) == *val) { + found = 1; + insIdx = i; + } + } + + if (found) { + if (err) { + printf("ERROR in routine ListClass::ins_pointer()\n"); + CORE_DUMP; + } else { + delete val; + val = (*lc_t)[insIdx]; + } + } else { + lc_t->append(val); + insIdx = lc_t->getSize() - 1; + if (insFlagPtr) { + *insFlagPtr = 1; + } + } + + return (insIdx); +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::sort_pointer ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Same as sort except values are dereferenced when compared. ****/ +/******************************************************************************************/ +#define SWAP(a, b) \ + temp = (a); \ + (a) = (b); \ + (b) = temp; +#define M 7 +#define NSTACK 50 + +template +void sort_pointer(ListClass *lc_t) +{ + int i, ir = lc_t->getSize(), j, k, l = 1; + int jstack = 0, *istack; + T vara, temp; + + istack = (int *)malloc(NSTACK * sizeof(int)); + for (;;) { + if (ir - l < M) { + for (j = l + 1; j <= ir; j++) { + vara = (*lc_t)[j - 1]; + for (i = j - 1; i >= 1; i--) { + if (!(*((*lc_t)[i - 1]) > *vara)) + break; + (*lc_t)[i] = (*lc_t)[i - 1]; + } + (*lc_t)[i] = vara; + } + if (jstack == 0) { + free(istack); + return; + } + ir = istack[jstack - 1]; + l = istack[jstack - 2]; + jstack -= 2; + } else { + k = (l + ir) >> 1; + SWAP((*lc_t)[k - 1], (*lc_t)[l]) + if (*((*lc_t)[l]) > *((*lc_t)[ir - 1])) { + SWAP((*lc_t)[l], (*lc_t)[ir - 1]) + } + if (*((*lc_t)[l - 1]) > *((*lc_t)[ir - 1])) { + SWAP((*lc_t)[l - 1], (*lc_t)[ir - 1]) + } + if (*((*lc_t)[l]) > *((*lc_t)[l - 1])) { + SWAP((*lc_t)[l], (*lc_t)[l - 1]) + } + i = l + 1; + j = ir; + vara = (*lc_t)[l - 1]; + for (;;) { + do + i++; + while (*vara > *((*lc_t)[i - 1])); + do + j--; + while (*((*lc_t)[j - 1]) > *vara); + if (j < i) + break; + SWAP((*lc_t)[i - 1], (*lc_t)[j - 1]); + } + (*lc_t)[l - 1] = (*lc_t)[j - 1]; + (*lc_t)[j - 1] = vara; + jstack += 2; + if (jstack > NSTACK) { + printf("NSTACK too small in isort."); + exit(1); + } + if (ir - i + 1 >= j - l) { + istack[jstack - 1] = ir; + istack[jstack - 2] = i; + ir = j - 1; + } else { + istack[jstack - 1] = j - 1; + istack[jstack - 2] = l; + l = i; + } + } + } +} +#undef M +#undef NSTACK +#undef SWAP +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::contains_pointer ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Same as contains except values are dereferenced when compared. ****/ +/**** Given list {list[0], list[1], ..., list[n-1]} and value val, return 1 if val is ****/ +/**** contained in the list and 0 otherwise. ****/ +/******************************************************************************************/ +template +int contains_pointer(ListClass *lc_t, T &val) +{ + int i, found; + + found = 0; + for (i = lc_t->getSize() - 1; (i >= 0) && (!found); i--) { + if (*((*lc_t)[i]) == *val) { + found = 1; + } + } + + return (found); +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: readOneCol ****/ +/**** INPUTS: file "flname" ****/ +/**** OUTPUTS: List of values from the file. ****/ +/**** Reads one column tabular data from the file "flname" into the ListClass lc_t. ****/ +/**** Lines beginning with the character '#' are ignored to allow for comments. ****/ +/**** Elements of class T must have overator << defined to read from the file. ****/ + +template +void readOneCol(const char *flname, ListClass *lc_t) +{ +#if 1 + char *lnptr; + FILE *fp; + T val; + + char *line = (char *)malloc(500 * sizeof(char)); + + #define TMP_NEDELIM (lnptr[0] != ',') && (lnptr[0] != ' ') && (lnptr[0] != '\t') + #define TMP_EQDELIM (lnptr[0] == ',') || (lnptr[0] == ' ') || (lnptr[0] == '\t') + + if (strcmp(flname, "stdin") == 0) { + fp = stdin; + } else if (!(fp = fopen(flname, "rb"))) { + std::string errmsg = std::string("ERROR: Unable to read from file \"") + + std::string(flname) + std::string("\"\n"); + throw ::std::invalid_argument(errmsg); + return; + } + + while (fgetline(fp, line)) { + lnptr = line; + while ((lnptr[0] == ' ') || (lnptr[0] == '\t')) + lnptr++; + if ((lnptr[0] != '#') && (lnptr[0] != '\n')) { + lnptr += cvtStrToVal(lnptr, val); + lc_t->append(val); + } + } + if (fp != stdin) { + fclose(fp); + } + + free(line); + #undef TMP_NEDELIM + #undef TMP_EQDELIM + +#else + int position; + char ch; + void *cont; + T val; + + std::ifstream fp; + + fp.open(flname); // , std::ios::in|std::ios::binary); + + if (!fp) { + std::cerr << "Error reading from file " << flname << std::endl; + exit(1); + } + + lc_t->reset(); + + do { + position = fp.tellg(); + cont = fp.get(ch); + while (cont && ((ch == ' ') || (ch == '\t'))) { + cont = fp.get(ch); + } + if ((ch != '#') && (ch != '\n')) { + fp.seekg(-1, std::ios::cur); + if (fp >> val) { + lc_t->append(val); + } + } + fp.seekg(position, std::ios::beg); + cont = fp.get(ch); + while (cont && (ch != '\n')) { + cont = fp.get(ch); + // Using "cont = (fp >> ch)" ignores '\n' and doesn't work. Don't + // understand. + } + } while (cont); + + fp.close(); +#endif + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ListClass::crossList ****/ +/**** INPUTS: ****/ +/**** OUTPUTS: ****/ +/**** Given: list of T {list[0], list[1], ..., list[n-1]} ****/ +/**** list of int {ilist[0], ilist[1], ..., ilist[n-1]} ****/ +/**** AND int value ival. ****/ +/**** Return the value x, such that x = list[i] and ilist[i] = ival. ****/ +/******************************************************************************************/ +template +T crossList(const ListClass *lc_t, const U *ilist, U val, int err) +{ + int i, found; + T retval = (T)NULL; + int size = lc_t->getSize(); + + found = 0; + for (i = size - 1; (i >= 0) && (!found); i--) { + if (ilist[i] == val) { + found = 1; + retval = (*lc_t)[i]; + } + } + + if (!found) { + if (err) { + printf("ERROR in routine crossList()\n"); + CORE_DUMP; + } + } + + return (retval); +}; +/******************************************************************************************/ diff --git a/src/afc-engine/list.h b/src/afc-engine/list.h new file mode 100644 index 0000000..80e7c46 --- /dev/null +++ b/src/afc-engine/list.h @@ -0,0 +1,84 @@ +/******************************************************************************************/ +/**** FILE: list.h ****/ +/******************************************************************************************/ + +#ifndef LIST_H +#define LIST_H + +#include + +/******************************************************************************************/ +/**** Template Class: ListClass ****/ +/**** Based on template class example from "C++ Inside & Out", pg 528. ****/ +/******************************************************************************************/ +template +class ListClass +{ + public: + ListClass(int n, int incr = 10); + ~ListClass(); + T &operator[](int index) const; + int operator==(ListClass &val); + int getSize() const; + void append(T val); + void reset(); + void resize(int n = 0); + void reverse(); + void del_elem(T val, const int err = 1); + void del_elem_idx(int index, const int err = 1); + void toggle_elem(T val); + T pop(); // push not needed because append does the same thing. + + int ins_elem(T val, const int err = 1, int *insFlagPtr = (int *)0); + int get_index(T val, const int err = 1) const; + int contains(T val) const; + + void sort(const std::function &compare); + + void printlist(int n = 100, + const char *elemSep = (const char *)0, + const char *grpSep = (const char *)0, + const char *endStr = (const char *)0); + + private: + T *a; + int size; + int allocation_size; + int allocation_increment; +}; +/******************************************************************************************/ + +template +int get_index_sorted(ListClass *lc_t, T val, int err = 1); + +template +int contains_sorted(ListClass *lc_t, T val); + +template +int ins_elem_sorted(ListClass *lc_t, T val, const int err = 1, int *insFlagPtr = (int *)0); + +template +void sort(ListClass *lc_t); + +template +void sort2(ListClass *lc_t, ListClass *lc_u); + +template +void sort3(ListClass *lc_t, ListClass *lc_u, ListClass *lc_v); + +template +int ins_pointer(ListClass *lc_t, T &val, int err = 1, int *insFlagPtr = (int *)0); + +template +void sort_pointer(ListClass *lc_t); + +template +int contains_pointer(ListClass *lc_t, T &val); + +template +void readOneCol(const char *filename, ListClass *lc_t); + +template +T crossList(const ListClass *lc_t, const U *ilist, U val, int err = 1); + +#endif diff --git a/src/afc-engine/list_impl.cpp b/src/afc-engine/list_impl.cpp new file mode 100644 index 0000000..2d4a70a --- /dev/null +++ b/src/afc-engine/list_impl.cpp @@ -0,0 +1,24 @@ +#include "list.cpp" +#include "dbldbl.h" +#include "Vector3.h" + +class ULSClass; +class RLANClass; +class ChannelPairClass; +class LidarBlacklistEntryClass; +class BBClass; + +template class ListClass; +template class ListClass; +template class ListClass; +template class ListClass; +template class ListClass; +template class ListClass; +template class ListClass; +template class ListClass; +template class ListClass; + +template void sort(ListClass *); +template void sort(ListClass *); + +template void sort2(ListClass *, ListClass *); diff --git a/src/afc-engine/local_defines.h b/src/afc-engine/local_defines.h new file mode 100644 index 0000000..f7b33b7 --- /dev/null +++ b/src/afc-engine/local_defines.h @@ -0,0 +1,22 @@ +/******************************************************************************************/ +/**** FILE: local_defines.h ****/ +/******************************************************************************************/ + +#ifndef LOCAL_DEFINES_H +#define LOCAL_DEFINES_H + +#ifndef APPLICATION_RELEASE + #define APPLICATION_RELEASE "NON_RELEASE" /* APPLICATION_RELEASE version, ex "040531.1" */ +#endif + +#ifndef CDEBUG + #define CDEBUG 0 /* Whether or not to compile in debug mode */ +/* Note that DEBUG is used by Qt, so CDEBUG is */ +/* used to avoid conflict. */ +#endif + +#define FORCE_EQV_EXCEL 0 /* Force computation to be exactly same as excel */ + +#define INVALID_METRIC 0x3FFFFFFF + +#endif diff --git a/src/afc-engine/main.cpp b/src/afc-engine/main.cpp new file mode 100644 index 0000000..83455be --- /dev/null +++ b/src/afc-engine/main.cpp @@ -0,0 +1,133 @@ +#include + +#include "AfcManager.h" +#include "afclogging/QtStream.h" +#include "afclogging/LoggingConfig.h" + +namespace +{ +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "main") + +int showErrorMessage(const std::string &message) +{ + LOGGER_CRIT(logger) << "AFC Engine error: " << message; + // logging messages are all sent to stdout so when we want to display this + // error to the user we manually use stderr + std::cerr << "AFC Engine error: " << message << std::endl; + Logging::flush(); + + return 1; +} +} // end namespace + +int main(int argc, char **argv) +{ // Accepts input from command line + try { + // initialize logging + QtStream::installLogHandler(); + Logging::Config conf = Logging::Config(); + Logging::Filter filter = Logging::Filter(); + filter.setLevel("debug"); + conf.useStdOut = true; + conf.useStdErr = false; + conf.filter = filter; + Logging::initialize(conf); + + std::string inputFilePath, configFilePath, outputFilePath, tempDir, logLevel; + AfcManager afcManager = AfcManager(); + // Parse command line parameters + try { + afcManager.setCmdLineParams(inputFilePath, + configFilePath, + outputFilePath, + tempDir, + logLevel, + argc, + argv); + conf.filter.setLevel(logLevel); + Logging::initialize(conf); // reinitialize log level + } catch (std::exception &err) { + throw std::runtime_error(ErrStream() << "Failed to parse command line " + "arguments provided by GUI: " + << err.what()); + } + + /**************************************************************************************/ + /* Read in the input configuration and parameters */ + /**************************************************************************************/ + + // Set constant parameters + afcManager.setConstInputs(tempDir); + + // Import configuration from the GUI + LOGGER_DEBUG(logger) << "AFC Engine is importing configuration..."; + try { + afcManager.importConfigAFCjson(configFilePath, tempDir); + } catch (std::exception &err) { + throw std::runtime_error(ErrStream() + << "Failed to import configuration from GUI: " + << err.what()); + } + + // Import user inputs from the GUI + LOGGER_DEBUG(logger) << "AFC Engine is importing user inputs..."; + try { + afcManager.importGUIjson( + inputFilePath); // Reads the JSON file provided by the GUI + } catch (std::exception &err) { + throw std::runtime_error(ErrStream() + << "Failed to import user inputs from GUI: " + << err.what()); + } + + /**************************************************************************************/ + + // Prints user input files for debugging + afcManager.printUserInputs(); + + // Read in the databases' information + try { + LOGGER_DEBUG(logger) << "initializing databases"; + auto t1 = std::chrono::high_resolution_clock::now(); + afcManager.initializeDatabases(); + auto t2 = std::chrono::high_resolution_clock::now(); + LOGGER_INFO(logger) + << "Databases initialized in: " + << std::chrono::duration_cast(t2 - t1).count() + << " seconds"; + } catch (std::exception &err) { + throw std::runtime_error(ErrStream() << "Failed to initialize databases: " + << err.what()); + } + /**************************************************************************************/ + /* Perform AFC Engine Computations */ + /**************************************************************************************/ + auto t1 = std::chrono::high_resolution_clock::now(); + afcManager.compute(); + auto t2 = std::chrono::high_resolution_clock::now(); + LOGGER_INFO(logger) + << "Computations completed in: " + << std::chrono::duration_cast(t2 - t1).count() + << " seconds"; + /**************************************************************************************/ + +#if 0 + std::vector psdFreqRangeList; + afcManager.computeInquiredFreqRangesPSD(psdFreqRangeList); +#endif + + /**************************************************************************************/ + /* Write output files */ + /**************************************************************************************/ + QString outputPath = QString::fromStdString(outputFilePath); + afcManager.exportGUIjson(outputPath, tempDir); + + LOGGER_DEBUG(logger) << "AFC Engine has exported the data for the GUI..."; + /**************************************************************************************/ + + return 0; + } catch (std::exception &e) { + return showErrorMessage(e.what()); + } +} diff --git a/src/afc-engine/multiband_raster.cpp b/src/afc-engine/multiband_raster.cpp new file mode 100644 index 0000000..30115bb --- /dev/null +++ b/src/afc-engine/multiband_raster.cpp @@ -0,0 +1,65 @@ +#include "multiband_raster.h" +#include +#include +#include +#include + +namespace +{ +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "multiband_raster") + +} // end namespace + +const StrTypeClass MultibandRasterClass::strHeightResultList[] = { + {MultibandRasterClass::OUTSIDE_REGION, "OUTSIDE_REGION"}, + {MultibandRasterClass::NO_DATA, "NO_DATA"}, + {MultibandRasterClass::NO_BUILDING, "NO_BUILDING"}, + {MultibandRasterClass::BUILDING, "BUILDING"}, + + {-1, (char *)0} + +}; + +MultibandRasterClass::MultibandRasterClass(const std::string &rasterFile, + CConst::LidarFormatEnum formatVal) : + _format(formatVal), _cgLidar(rasterFile, "lidar", nullptr, 2) +{ + _cgLidar.setNoData(std::numeric_limits::quiet_NaN(), 1); + _cgLidar.setNoData(std::numeric_limits::quiet_NaN(), 2); +} + +bool MultibandRasterClass::contains(const double &lonDeg, const double &latDeg) +{ + return _cgLidar.covers(latDeg, lonDeg); +} + +void MultibandRasterClass::getHeight(const double &latDeg, + const double &lonDeg, + double &terrainHeight, + double &bldgHeight, + HeightResult &heightResult, + bool directGdalMode) const +{ + float terrainHeightF = std::numeric_limits::quiet_NaN(); + float bldgHeightF = std::numeric_limits::quiet_NaN(); + if (_cgLidar.covers(latDeg, lonDeg)) { + if (!_cgLidar.getValueAt(latDeg, lonDeg, &terrainHeightF, 1, directGdalMode)) { + heightResult = NO_DATA; + } else if (!_cgLidar.getValueAt(latDeg, lonDeg, &bldgHeightF, 2, directGdalMode)) { + heightResult = (_format == CConst::fromVectorLidarFormat) ? NO_BUILDING : + NO_DATA; + } else if ((_format == CConst::fromRasterLidarFormat) && + (bldgHeightF <= (terrainHeightF + 1))) { + heightResult = NO_BUILDING; + bldgHeightF = std::numeric_limits::quiet_NaN(); + } else { + heightResult = BUILDING; + } + } else { + heightResult = OUTSIDE_REGION; + } + terrainHeight = terrainHeightF; + bldgHeight = bldgHeightF; +} +/******************************************************************************************/ diff --git a/src/afc-engine/multiband_raster.h b/src/afc-engine/multiband_raster.h new file mode 100644 index 0000000..7efed1f --- /dev/null +++ b/src/afc-engine/multiband_raster.h @@ -0,0 +1,51 @@ +/******************************************************************************************/ +/**** FILE : multiband_raster.h ****/ +/******************************************************************************************/ + +#ifndef MULTIBAND_RASTER_H +#define MULTIBAND_RASTER_H + +#include "CachedGdal.h" +#include "cconst.h" +#include +#include "str_type.h" +#include + +/******************************************************************************************/ +/**** CLASS: MultibandRasterClass ****/ +/******************************************************************************************/ +class MultibandRasterClass : private boost::noncopyable +{ + public: + enum HeightResult { + OUTSIDE_REGION, // point outside region defined by rectangle "bounds" + NO_DATA, // point inside bounds that has no data. terrainHeight set to + // nodataBE, bldgHeight undefined + NO_BUILDING, // point where there is no building, terrainHeight set to valid + // value, bldgHeight set to nodataBldg + BUILDING // point where there is a building, terrainHeight and bldgHeight + // valid values + }; + + MultibandRasterClass(const std::string &rasterFile, + CConst::LidarFormatEnum formatVal); + + // Returns building height at a specified (lat/lon) point. If there are no buildings + // present at the given position then a quiet NaN value is returned + void getHeight(const double &latDeg, + const double &lonDeg, + double &terrainHeight, + double &bldgHeight, + HeightResult &heightResult, + bool directGdalMode = false) const; + bool contains(const double &latDeg, const double &lonDeg); + + static const StrTypeClass strHeightResultList[]; + + private: + CConst::LidarFormatEnum _format; + mutable CachedGdal _cgLidar; +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/nfa.cpp b/src/afc-engine/nfa.cpp new file mode 100644 index 0000000..55a00fc --- /dev/null +++ b/src/afc-engine/nfa.cpp @@ -0,0 +1,285 @@ +/******************************************************************************************/ +/**** FILE : nfa.cpp ****/ +/******************************************************************************************/ + +#include "nfa.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "AfcDefinitions.h" + +/******************************************************************************************/ +/**** CONSTRUCTOR: NFAClass::NFAClass() ****/ +/******************************************************************************************/ +NFAClass::NFAClass() +{ + tableFile = ""; + nfaTable = (double ***)NULL; + numxdb = -1; + numu = -1; + numeff = -1; + xdbStart = quietNaN; + uStart = quietNaN; + effStart = quietNaN; + xdbStep = quietNaN; + uStep = quietNaN; + effStep = quietNaN; +}; + +NFAClass::NFAClass(std::string tableFileVal) : tableFile(tableFileVal) +{ + readTable(); +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** DESTRUCTOR: NFAClass::~NFAClass() ****/ +/******************************************************************************************/ +NFAClass::~NFAClass() +{ + if (nfaTable) { + for (int xdbIdx = 0; xdbIdx < numxdb; ++xdbIdx) { + for (int uIdx = 0; uIdx < numu; ++uIdx) { + free(nfaTable[xdbIdx][uIdx]); + } + free(nfaTable[xdbIdx]); + } + free(nfaTable); + } +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: NFAClass::readTable() ****/ +/******************************************************************************************/ +void NFAClass::readTable() +{ + string line; + vector headerList; + vector> datastore; + // data structure + vector xdB_dB; + vector u_step_count; + vector efficiency; + vector Near_field_adjustment_dB; + std::ostringstream errStr; + + xdbStep = 1.0; + uStep = 0.05; + effStep = 0.05; + + double minxdb = quietNaN; + double maxxdb = quietNaN; + double minu = quietNaN; + double maxu = quietNaN; + double mineff = quietNaN; + double maxeff = quietNaN; + + ifstream file(tableFile); + if (!file.is_open()) { + errStr << std::string("ERROR: Unable to open Near Field Adjustment File \"") + + tableFile + std::string("\"\n"); + throw std::runtime_error(errStr.str()); + } + int linenum = 0; + + while (getline(file, line)) { + linenum++; + vector current_row; + + size_t last = 0; + size_t next = 0; + bool found_last_comma = false; + + while (!found_last_comma) { + if ((next = line.find(',', last)) == string::npos) { + found_last_comma = true; + } + + current_row.push_back(line.substr(last, next - last)); + last = next + 1; + } + if (current_row.size() != 4) { + errStr << std::string("ERROR: Near Field Adjustment File ") << tableFile + << ":" << linenum << " INVALID DATA\n"; + throw std::runtime_error(errStr.str()); + } + if (linenum == 1) { + } else { + vector doubleVector(current_row.size()); + transform(current_row.begin(), + current_row.end(), + doubleVector.begin(), + [](const std::string &val) { + return std::stod(val); + }); + datastore.push_back(doubleVector); + + double xdb = doubleVector[0]; + double u = doubleVector[1]; + double eff = doubleVector[2]; + + if (datastore.size() == 1) { + minxdb = xdb; + maxxdb = xdb; + minu = u; + maxu = u; + mineff = eff; + maxeff = eff; + } else { + if (xdb < minxdb) { + minxdb = xdb; + } else if (xdb > maxxdb) { + maxxdb = xdb; + } + if (u < minu) { + minu = u; + } else if (u > maxu) { + maxu = u; + } + if (eff < mineff) { + mineff = eff; + } else if (eff > maxeff) { + maxeff = eff; + } + } + } + } + numxdb = (int)floor((maxxdb - minxdb) / xdbStep + 0.5) + 1; + numu = (int)floor((maxu - minu) / uStep + 0.5) + 1; + numeff = (int)floor((maxeff - mineff) / effStep + 0.5) + 1; + + xdbStart = minxdb; + uStart = minu; + effStart = mineff; + + int xdbIdx, uIdx, effIdx; + + nfaTable = (double ***)malloc(numxdb * sizeof(double **)); + for (xdbIdx = 0; xdbIdx < numxdb; ++xdbIdx) { + nfaTable[xdbIdx] = (double **)malloc(numu * sizeof(double *)); + for (uIdx = 0; uIdx < numu; ++uIdx) { + nfaTable[xdbIdx][uIdx] = (double *)malloc(numeff * sizeof(double)); + for (effIdx = 0; effIdx < numeff; ++effIdx) { + nfaTable[xdbIdx][uIdx][effIdx] = quietNaN; + } + } + } + + double xdb, u, eff, nfa; + + // loads parse up table data/creates data strucutre each column into 4 vectors xdB, step + // count, efficiency, and NFA. Done for unit testing puposes to make sure all the data + // values are present. This entire for loop can be removed. + for (int row_ind = 0; row_ind < (int)datastore.size(); ++row_ind) { + xdb = datastore[row_ind][0]; + u = datastore[row_ind][1]; + eff = datastore[row_ind][2]; + nfa = datastore[row_ind][3]; + + xdbIdx = (int)floor(((xdb - xdbStart) / xdbStep) + 0.5); + uIdx = (int)floor(((u - uStart) / uStep) + 0.5); + effIdx = (int)floor(((eff - effStart) / effStep) + 0.5); + nfaTable[xdbIdx][uIdx][effIdx] = nfa; + } + + for (xdbIdx = 0; xdbIdx < numxdb; ++xdbIdx) { + for (effIdx = 0; effIdx < numeff; ++effIdx) { + bool foundDataStart = false; + for (uIdx = numu - 1; uIdx >= 0; --uIdx) { + if (std::isnan(nfaTable[xdbIdx][uIdx][effIdx])) { + if (!foundDataStart) { + nfaTable[xdbIdx][uIdx][effIdx] = 0.0; + } else { + xdb = xdbStart + xdbIdx * xdbStep; + u = uStart + uIdx * uStep; + eff = effStart + effIdx * effStep; + errStr << std::string("ERROR: Near Field " + "Adjustment File ") + << tableFile + << " does not contain data for xdb = " << xdb + << ", u = " << u << ", eff = " << eff + << std::endl; + throw std::runtime_error(errStr.str()); + } + } else { + foundDataStart = true; + } + } + } + } + + return; +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: NFAClass::computeNFA() ****/ +/******************************************************************************************/ +double NFAClass::computeNFA(double xdb, double u, double eff) +{ + double xdbIdxDbl = (xdb - xdbStart) / xdbStep; + double uIdxDbl = (u - uStart) / uStep; + double effIdxDbl = (eff - effStart) / effStep; + + if (xdbIdxDbl < 0.0) { + xdbIdxDbl = 0.0; + } else if (xdbIdxDbl > numxdb - 1) { + xdbIdxDbl = (double)numxdb - 1; + } + + if (uIdxDbl < 0.0) { + uIdxDbl = 0.0; + } else if (uIdxDbl > numu - 1) { + uIdxDbl = (double)numu - 1; + } + + if (effIdxDbl < 0.0) { + effIdxDbl = 0.0; + } else if (effIdxDbl > numeff - 1) { + effIdxDbl = (double)numeff - 1; + } + + int xdbIdx0 = (int)floor(xdbIdxDbl); + if (xdbIdx0 == numxdb - 1) { + xdbIdx0 = numxdb - 2; + } + + int uIdx0 = (int)floor(uIdxDbl); + if (uIdx0 == numu - 1) { + uIdx0 = numu - 2; + } + + int effIdx0 = (int)floor(effIdxDbl); + if (effIdx0 == numeff - 1) { + effIdx0 = numeff - 2; + } + + double F000 = nfaTable[xdbIdx0][uIdx0][effIdx0]; + double F001 = nfaTable[xdbIdx0][uIdx0][effIdx0 + 1]; + double F010 = nfaTable[xdbIdx0][uIdx0 + 1][effIdx0]; + double F011 = nfaTable[xdbIdx0][uIdx0 + 1][effIdx0 + 1]; + double F100 = nfaTable[xdbIdx0 + 1][uIdx0][effIdx0]; + double F101 = nfaTable[xdbIdx0 + 1][uIdx0][effIdx0 + 1]; + double F110 = nfaTable[xdbIdx0 + 1][uIdx0 + 1][effIdx0]; + double F111 = nfaTable[xdbIdx0 + 1][uIdx0 + 1][effIdx0 + 1]; + + double nfa = + F000 * (xdbIdx0 + 1 - xdbIdxDbl) * (uIdx0 + 1 - uIdxDbl) * + (effIdx0 + 1 - effIdxDbl) + + F001 * (xdbIdx0 + 1 - xdbIdxDbl) * (uIdx0 + 1 - uIdxDbl) * (effIdxDbl - effIdx0) + + F010 * (xdbIdx0 + 1 - xdbIdxDbl) * (uIdxDbl - uIdx0) * (effIdx0 + 1 - effIdxDbl) + + F011 * (xdbIdx0 + 1 - xdbIdxDbl) * (uIdxDbl - uIdx0) * (effIdxDbl - effIdx0) + + F100 * (xdbIdxDbl - xdbIdx0) * (uIdx0 + 1 - uIdxDbl) * (effIdx0 + 1 - effIdxDbl) + + F101 * (xdbIdxDbl - xdbIdx0) * (uIdx0 + 1 - uIdxDbl) * (effIdxDbl - effIdx0) + + F110 * (xdbIdxDbl - xdbIdx0) * (uIdxDbl - uIdx0) * (effIdx0 + 1 - effIdxDbl) + + F111 * (xdbIdxDbl - xdbIdx0) * (uIdxDbl - uIdx0) * (effIdxDbl - effIdx0); + + return (nfa); +} +/******************************************************************************************/ diff --git a/src/afc-engine/nfa.h b/src/afc-engine/nfa.h new file mode 100644 index 0000000..21fc6f4 --- /dev/null +++ b/src/afc-engine/nfa.h @@ -0,0 +1,35 @@ +/******************************************************************************************/ +/**** FILE : nfa.h ****/ +/******************************************************************************************/ + +#ifndef NFA_H +#define NFA_H + +#include + +using namespace std; + +/******************************************************************************************/ +/**** CLASS: NFAClass ****/ +/******************************************************************************************/ +class NFAClass +{ + public: + NFAClass(); + NFAClass(std::string tableFile); + ~NFAClass(); + + double computeNFA(double xdb, double u, double eff); + + private: + void readTable(); + + std::string tableFile; + double ***nfaTable; + int numxdb, numu, numeff; + double xdbStart, uStart, effStart; + double xdbStep, uStep, effStep; +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/polygon.cpp b/src/afc-engine/polygon.cpp new file mode 100644 index 0000000..b0f4455 --- /dev/null +++ b/src/afc-engine/polygon.cpp @@ -0,0 +1,1049 @@ +/******************************************************************************************/ +/**** PROGRAM: polygon.cpp ****/ +/******************************************************************************************/ +#include +#include +#include +#include +#include +#include + +#include "global_defines.h" +#include "global_fn.h" +#include "polygon.h" + +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::PolygonClass ****/ +/******************************************************************************************/ +PolygonClass::PolygonClass() +{ + num_segment = 0; + num_bdy_pt = (int *)NULL; + bdy_pt_x = (int **)NULL; + bdy_pt_y = (int **)NULL; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::PolygonClass ****/ +/******************************************************************************************/ +PolygonClass::PolygonClass(std::vector> *ii_list) +{ + int i; + + num_segment = 1; + num_bdy_pt = IVECTOR(num_segment); + num_bdy_pt[0] = ii_list->size(); + + bdy_pt_x = (int **)malloc(num_segment * sizeof(int *)); + bdy_pt_y = (int **)malloc(num_segment * sizeof(int *)); + + bdy_pt_x[0] = IVECTOR(num_bdy_pt[0]); + bdy_pt_y[0] = IVECTOR(num_bdy_pt[0]); + + for (i = 0; i <= num_bdy_pt[0] - 1; i++) { + std::tie(bdy_pt_x[0][i], bdy_pt_y[0][i]) = (*ii_list)[i]; + } +} +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::PolygonClass ****/ +/******************************************************************************************/ +PolygonClass::PolygonClass(std::string kmlFilename, double resolution) +{ + std::ostringstream errStr; + + /**************************************************************************************/ + /* Read entire contents of kml file into sval */ + /**************************************************************************************/ + std::ifstream fstr(kmlFilename); + std::stringstream istream; + istream << fstr.rdbuf(); + std::string sval = istream.str(); + /**************************************************************************************/ + + std::size_t found; + + /**************************************************************************************/ + /* Grab contents of ... */ + /**************************************************************************************/ + found = sval.find(""); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find "; + throw std::runtime_error(errStr.str()); + } + sval.replace(0, found + 11, ""); + + found = sval.find(""); + if (found != std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: multiple 's found"; + throw std::runtime_error(errStr.str()); + } + + found = sval.find(""); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find "; + throw std::runtime_error(errStr.str()); + } + sval.replace(found, sval.size() - found, ""); + /**************************************************************************************/ + + /**************************************************************************************/ + /* Extract name */ + /**************************************************************************************/ + found = sval.find(""); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find "; + throw std::runtime_error(errStr.str()); + } + std::size_t nameStart = found + 6; + + found = sval.find(""); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find "; + throw std::runtime_error(errStr.str()); + } + std::size_t nameEnd = found; + + int nameLength = nameEnd - nameStart; + + if (nameLength) { + name = sval.substr(nameStart, nameLength); + } else { + name.clear(); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Grab contents of ... */ + /**************************************************************************************/ + found = sval.find(""); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find "; + throw std::runtime_error(errStr.str()); + } + sval.replace(0, found + 9, ""); + + found = sval.find(""); + if (found != std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: File " << kmlFilename << " multiple 's found"; + throw std::runtime_error(errStr.str()); + } + + found = sval.find(""); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find "; + throw std::runtime_error(errStr.str()); + } + sval.replace(found, sval.size() - found, ""); + /**************************************************************************************/ + + /**************************************************************************************/ + /* Grab contents of ... */ + /**************************************************************************************/ + found = sval.find(""); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find "; + throw std::runtime_error(errStr.str()); + } + sval.replace(0, found + 17, ""); + + found = sval.find(""); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find "; + throw std::runtime_error(errStr.str()); + } + sval.replace(found, sval.size() - found, ""); + /**************************************************************************************/ + + /**************************************************************************************/ + /* Grab contents of ... */ + /**************************************************************************************/ + found = sval.find(""); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find "; + throw std::runtime_error(errStr.str()); + } + sval.replace(0, found + 13, ""); + + found = sval.find(""); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find "; + throw std::runtime_error(errStr.str()); + } + sval.replace(found, sval.size() - found, ""); + /**************************************************************************************/ + + /**************************************************************************************/ + /* Remove whitespace from beginning and end of sval */ + /**************************************************************************************/ + std::size_t start = sval.find_first_not_of(" \n\t"); + std::size_t end = sval.find_last_not_of(" \n\t"); + if (start == std::string::npos) { + sval.clear(); + } else { + sval = sval.substr(start, end - start + 1); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Replace consecutive whitespace characters with a single space */ + /**************************************************************************************/ + std::size_t posn = 0; + bool cont = true; + do { + std::size_t posnA = sval.find_first_of(" \n\t", posn); + if (posnA == std::string::npos) { + cont = false; + } else { + std::size_t posnB = sval.find_first_not_of(" \n\t", posnA); + if (posnB - posnA == 1) { + if (sval[posnA] != ' ') { + sval[posnA] = ' '; + } + } else { + sval.replace(posnA, posnB - posnA, " "); + } + posn = posnA + 1; + } + } while (cont); + /**************************************************************************************/ + + std::vector clist = split(sval, ' '); + + /**************************************************************************************/ + /* Remove duplicate endpoint (in kml polygons are closed with last pt = first pt) */ + /**************************************************************************************/ + if ((clist.size() > 1) && (clist[0] == clist[clist.size() - 1])) { + clist.pop_back(); + } + /**************************************************************************************/ + + num_segment = 1; + num_bdy_pt = IVECTOR(num_segment); + num_bdy_pt[0] = clist.size(); + + bdy_pt_x = (int **)malloc(num_segment * sizeof(int *)); + bdy_pt_y = (int **)malloc(num_segment * sizeof(int *)); + + bdy_pt_x[0] = IVECTOR(num_bdy_pt[0]); + bdy_pt_y[0] = IVECTOR(num_bdy_pt[0]); + + int ptIdx; + for (ptIdx = 0; ptIdx <= num_bdy_pt[0] - 1; ptIdx++) { + char *chptr; + + std::vector lonlatStrList = split(clist[ptIdx], ','); + double longitude = std::strtod(lonlatStrList[0].c_str(), &chptr); + double latitude = std::strtod(lonlatStrList[1].c_str(), &chptr); + + int xval = (int)floor(((longitude) / resolution) + 0.5); + int yval = (int)floor(((latitude) / resolution) + 0.5); + + bdy_pt_x[0][ptIdx] = xval; + bdy_pt_y[0][ptIdx] = yval; + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::~PolygonClass ****/ +/******************************************************************************************/ +PolygonClass::~PolygonClass() +{ + int segment_idx; + + for (segment_idx = 0; segment_idx <= num_segment - 1; segment_idx++) { + free(bdy_pt_x[segment_idx]); + free(bdy_pt_y[segment_idx]); + } + if (num_segment) { + free(bdy_pt_x); + free(bdy_pt_y); + free(num_bdy_pt); + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::readMultiGeometry() ****/ +/******************************************************************************************/ +std::vector PolygonClass::readMultiGeometry(std::string kmlFilename, + double resolution) +{ + std::ostringstream errStr; + std::string tag; + std::string polystr; + + /**************************************************************************************/ + /* Read entire contents of kml file into sval */ + /**************************************************************************************/ + std::ifstream fstr(kmlFilename); + std::stringstream istream; + istream << fstr.rdbuf(); + std::string sval = istream.str(); + /**************************************************************************************/ + + std::size_t found; + + /**************************************************************************************/ + /* Grab contents of ... */ + /**************************************************************************************/ + tag = ""; + found = sval.find(tag); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find " << tag << " while reading file " << kmlFilename; + throw std::runtime_error(errStr.str()); + } + sval.replace(0, found + tag.size(), ""); + + found = sval.find(tag); + if (found != std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: multiple " << tag << "'s found while reading file " + << kmlFilename; + throw std::runtime_error(errStr.str()); + } + + tag = ""; + found = sval.find(tag); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find " << tag << " while reading file " << kmlFilename; + throw std::runtime_error(errStr.str()); + } + sval.replace(found, sval.size() - found, ""); + /**************************************************************************************/ + + /**************************************************************************************/ + /* Extract name */ + /**************************************************************************************/ + tag = ""; + found = sval.find(tag); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find " << tag << " while reading file " << kmlFilename; + throw std::runtime_error(errStr.str()); + } + std::size_t nameStart = found + tag.size(); + + tag = ""; + found = sval.find(tag); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find " << tag << " while reading file " << kmlFilename; + throw std::runtime_error(errStr.str()); + } + std::size_t nameEnd = found; + + int nameLength = nameEnd - nameStart; + + std::string namePfx; + if (nameLength) { + namePfx = sval.substr(nameStart, nameLength); + } else { + namePfx = "P"; + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Grab contents of ... */ + /**************************************************************************************/ + tag = ""; + + found = sval.find(tag); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find " << tag << " while reading file " << kmlFilename; + throw std::runtime_error(errStr.str()); + } + sval.replace(0, found + tag.size(), ""); + + found = sval.find(tag); + if (found != std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: multiple " << tag << "'s found"; + throw std::runtime_error(errStr.str()); + } + + tag = ""; + found = sval.find(tag); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find " << tag << " while reading file " << kmlFilename; + throw std::runtime_error(errStr.str()); + } + sval.replace(found, sval.size() - found, ""); + /**************************************************************************************/ + + std::vector polygonList; + + bool cont; + + do { + /**************************************************************************************/ + /* Grab contents of ... */ + /**************************************************************************************/ + tag = ""; + found = sval.find(tag); + if (found == std::string::npos) { + cont = false; + } else { + cont = true; + } + sval.replace(0, found + tag.size(), ""); + + if (cont) { + tag = ""; + found = sval.find(tag); + if (found == std::string::npos) { + std::cout << "SVAL: " << sval << std::endl; + errStr << "ERROR: unable to find " << tag << " while reading file " + << kmlFilename; + throw std::runtime_error(errStr.str()); + } + polystr = sval.substr(0, found); + sval.replace(0, found + tag.size(), ""); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Grab contents of ... */ + /**************************************************************************************/ + if (cont) { + tag = ""; + found = polystr.find(tag); + if (found == std::string::npos) { + std::cout << "POLYSTR: " << polystr << std::endl; + errStr << "ERROR: unable to find " << tag << " while reading file " + << kmlFilename; + throw std::runtime_error(errStr.str()); + } + polystr.replace(0, found + tag.size(), ""); + + tag = ""; + found = polystr.find(tag); + if (found == std::string::npos) { + std::cout << "POLYSTR: " << polystr << std::endl; + errStr << "ERROR: unable to find " << tag << " while reading file " + << kmlFilename; + throw std::runtime_error(errStr.str()); + } + polystr.replace(found, sval.size() - found, ""); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Grab contents of ... */ + /**************************************************************************************/ + if (cont) { + tag = ""; + found = polystr.find(tag); + if (found == std::string::npos) { + std::cout << "POLYSTR: " << polystr << std::endl; + errStr << "ERROR: unable to find " << tag << " while reading file " + << kmlFilename; + throw std::runtime_error(errStr.str()); + } + polystr.replace(0, found + tag.size(), ""); + + tag = ""; + found = polystr.find(tag); + if (found == std::string::npos) { + std::cout << "POLYSTR: " << polystr << std::endl; + errStr << "ERROR: unable to find " << tag << " while reading file " + << kmlFilename; + throw std::runtime_error(errStr.str()); + } + polystr.replace(found, polystr.size() - found, ""); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Remove whitespace from beginning and end of polystr */ + /**************************************************************************************/ + if (cont) { + std::size_t start = polystr.find_first_not_of(" \n\t"); + std::size_t end = polystr.find_last_not_of(" \n\t"); + if (start == std::string::npos) { + polystr.clear(); + } else { + polystr = polystr.substr(start, end - start + 1); + } + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Replace consecutive whitespace characters with a single space */ + /**************************************************************************************/ + if (cont) { + std::size_t posn = 0; + bool foundWhitespace = true; + do { + std::size_t posnA = polystr.find_first_of(" \n\t", posn); + if (posnA == std::string::npos) { + foundWhitespace = false; + } else { + std::size_t posnB = polystr.find_first_not_of(" \n\t", + posnA); + if (posnB - posnA == 1) { + if (polystr[posnA] != ' ') { + polystr[posnA] = ' '; + } + } else { + polystr.replace(posnA, posnB - posnA, " "); + } + posn = posnA + 1; + } + } while (foundWhitespace); + } + /**************************************************************************************/ + + if (cont) { + std::vector clist = split(polystr, ' '); + + /**********************************************************************************/ + /* Remove duplicate endpoint (in kml polygons are closed with last pt = + * first pt) */ + /**********************************************************************************/ + if ((clist.size() > 1) && (clist[0] == clist[clist.size() - 1])) { + clist.pop_back(); + } + /**********************************************************************************/ + + PolygonClass *poly = new PolygonClass(); + + poly->name = namePfx + "_" + std::to_string(polygonList.size()); + + poly->num_segment = 1; + poly->num_bdy_pt = IVECTOR(poly->num_segment); + poly->num_bdy_pt[0] = clist.size(); + + poly->bdy_pt_x = (int **)malloc(poly->num_segment * sizeof(int *)); + poly->bdy_pt_y = (int **)malloc(poly->num_segment * sizeof(int *)); + + poly->bdy_pt_x[0] = IVECTOR(poly->num_bdy_pt[0]); + poly->bdy_pt_y[0] = IVECTOR(poly->num_bdy_pt[0]); + + int ptIdx; + for (ptIdx = 0; ptIdx <= poly->num_bdy_pt[0] - 1; ptIdx++) { + char *chptr; + + std::vector lonlatStrList = split(clist[ptIdx], ','); + double longitude = std::strtod(lonlatStrList[0].c_str(), &chptr); + double latitude = std::strtod(lonlatStrList[1].c_str(), &chptr); + + int xval = (int)floor(((longitude) / resolution) + 0.5); + int yval = (int)floor(((latitude) / resolution) + 0.5); + + poly->bdy_pt_x[0][ptIdx] = xval; + poly->bdy_pt_y[0][ptIdx] = yval; + } + polygonList.push_back(poly); + } + } while (cont); + + return (polygonList); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::comp_bdy_min_max ****/ +/**** Find minx, maxx, miny, maxy for a list of bdy points. ****/ +/******************************************************************************************/ +void PolygonClass::comp_bdy_min_max(int &minx, + int &maxx, + int &miny, + int &maxy, + const int segment_idx) +{ + int i; + int n = num_bdy_pt[segment_idx]; + int *x = bdy_pt_x[segment_idx]; + int *y = bdy_pt_y[segment_idx]; + + minx = x[0]; + maxx = x[0]; + miny = y[0]; + maxy = y[0]; + for (i = 1; i <= n - 1; i++) { + minx = (x[i] < minx ? x[i] : minx); + maxx = (x[i] > maxx ? x[i] : maxx); + miny = (y[i] < miny ? y[i] : miny); + maxy = (y[i] > maxy ? y[i] : maxy); + } + + return; +} +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::comp_bdy_min_max ****/ +/**** Find minx, maxx, miny, maxy for a list of bdy points. ****/ +/******************************************************************************************/ +void PolygonClass::comp_bdy_min_max(int &minx, int &maxx, int &miny, int &maxy) +{ + int segment_idx; + int i_minx, i_maxx, i_miny, i_maxy; + + comp_bdy_min_max(minx, maxx, miny, maxy, 0); + + for (segment_idx = 1; segment_idx <= num_segment - 1; segment_idx++) { + comp_bdy_min_max(i_minx, i_maxx, i_miny, i_maxy, segment_idx); + minx = (i_minx < minx ? i_minx : minx); + maxx = (i_maxx > maxx ? i_maxx : maxx); + miny = (i_miny < miny ? i_miny : miny); + maxy = (i_maxy > maxy ? i_maxy : maxy); + } + + return; +} +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::translate ****/ +/******************************************************************************************/ +void PolygonClass::translate(int x, int y) +{ + int i, segment_idx, n; + + for (segment_idx = 0; segment_idx <= num_segment - 1; segment_idx++) { + n = num_bdy_pt[segment_idx]; + for (i = 0; i <= n - 1; i++) { + bdy_pt_x[segment_idx][i] += x; + bdy_pt_y[segment_idx][i] += y; + } + } + + return; +} +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::reverse ****/ +/******************************************************************************************/ +void PolygonClass::reverse() +{ + int i, segment_idx, n, tmp_x, tmp_y; + + for (segment_idx = 0; segment_idx <= num_segment - 1; segment_idx++) { + n = num_bdy_pt[segment_idx]; + for (i = 0; i <= n / 2 - 1; i++) { + tmp_x = bdy_pt_x[segment_idx][i]; + tmp_y = bdy_pt_y[segment_idx][i]; + bdy_pt_x[segment_idx][i] = bdy_pt_x[segment_idx][n - 1 - i]; + bdy_pt_y[segment_idx][i] = bdy_pt_y[segment_idx][n - 1 - i]; + bdy_pt_x[segment_idx][n - 1 - i] = tmp_x; + bdy_pt_y[segment_idx][n - 1 - i] = tmp_y; + } + } + + return; +} +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::comp_bdy_area ****/ +/**** Compute area of a PolygonClass ****/ +/******************************************************************************************/ +double PolygonClass::comp_bdy_area() +{ + int segment_idx; + double area; + + area = 0.0; + + for (segment_idx = 0; segment_idx <= num_segment - 1; segment_idx++) { + area += comp_bdy_area(num_bdy_pt[segment_idx], + bdy_pt_x[segment_idx], + bdy_pt_y[segment_idx]); + } + + return (area); +} +/******************************************************************************************/ +/**** FUNCTION: in_bdy_area ****/ +/**** Determine whether or not a given point lies within the bounded area ****/ +/******************************************************************************************/ +bool PolygonClass::in_bdy_area(const int a, const int b, bool *edge) +{ + int segment_idx, n; + int is_edge; + + if (edge) { + *edge = false; + } + n = 0; + for (segment_idx = 0; segment_idx <= num_segment - 1; segment_idx++) { + n += in_bdy_area(a, + b, + num_bdy_pt[segment_idx], + bdy_pt_x[segment_idx], + bdy_pt_y[segment_idx], + &is_edge); + if (is_edge) { + if (edge) { + *edge = true; + } + return (0); + } + } + + return (n & 1 ? true : false); +} +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::duplicate ****/ +/******************************************************************************************/ +PolygonClass *PolygonClass::duplicate() +{ + PolygonClass *new_polygon = new PolygonClass(); + + new_polygon->num_segment = num_segment; + + new_polygon->num_bdy_pt = IVECTOR(num_segment); + new_polygon->bdy_pt_x = (int **)malloc(num_segment * sizeof(int *)); + new_polygon->bdy_pt_y = (int **)malloc(num_segment * sizeof(int *)); + + for (int segment_idx = 0; segment_idx <= num_segment - 1; segment_idx++) { + new_polygon->num_bdy_pt[segment_idx] = num_bdy_pt[segment_idx]; + new_polygon->bdy_pt_x[segment_idx] = IVECTOR(num_bdy_pt[segment_idx]); + new_polygon->bdy_pt_y[segment_idx] = IVECTOR(num_bdy_pt[segment_idx]); + for (int bdy_pt_idx = 0; bdy_pt_idx <= num_bdy_pt[segment_idx] - 1; bdy_pt_idx++) { + new_polygon->bdy_pt_x[segment_idx][bdy_pt_idx] = + bdy_pt_x[segment_idx][bdy_pt_idx]; + new_polygon->bdy_pt_y[segment_idx][bdy_pt_idx] = + bdy_pt_y[segment_idx][bdy_pt_idx]; + } + } + + return (new_polygon); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::comp_bdy_area ****/ +/**** Compute area from list of boundary points ****/ +/******************************************************************************************/ +double PolygonClass::comp_bdy_area(const int n, const int *x, const int *y) +{ + int i, x1, y1, x2, y2; + double area; + + area = 0.0; + + for (i = 1; i <= n - 2; i++) { + x1 = x[i] - x[0]; + y1 = y[i] - y[0]; + x2 = x[i + 1] - x[0]; + y2 = y[i + 1] - y[0]; + + area += ((double)x1) * y2 - ((double)x2) * y1; + } + + area /= 2.0; + + return (area); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::comp_bdy_area ****/ +/**** Compute area from list of boundary points ****/ +/******************************************************************************************/ +double PolygonClass::comp_bdy_area(std::vector> *ii_list) +{ + int i, x1, y1, x2, y2, n; + double area; + + area = 0.0; + + n = ii_list->size(); + for (i = 1; i <= n - 2; i++) { + x1 = std::get<0>((*ii_list)[i]) - std::get<0>((*ii_list)[0]); + y1 = std::get<1>((*ii_list)[i]) - std::get<1>((*ii_list)[0]); + x2 = std::get<0>((*ii_list)[i + 1]) - std::get<0>((*ii_list)[0]); + y2 = std::get<1>((*ii_list)[i + 1]) - std::get<1>((*ii_list)[0]); + + area += ((double)x1) * y2 - ((double)x2) * y1; + } + + area /= 2.0; + + return (area); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: in_bdy_area ****/ +/**** Determine whether or not a given point lies within the bounded area ****/ +/******************************************************************************************/ +int PolygonClass::in_bdy_area(const int a, + const int b, + const int n, + const int *x, + const int *y, + int *edge) +{ + int i, num_left, num_right, same_y, index; + int x1, y1, x2, y2, eps; + + index = -1; + do { + index++; + if (index == n) { + return (0); + } + x2 = x[index]; + y2 = y[index]; + } while (y2 == b); + + if (edge) { + *edge = 0; + } + same_y = 0; + num_left = 0; + num_right = 0; + for (i = 0; i <= n - 1; i++) { + if (index == n - 1) { + index = 0; + } else { + index++; + } + x1 = x2; + y1 = y2; + x2 = x[index]; + y2 = y[index]; + + if ((x2 == a) && (y2 == b)) { + if (edge) { + *edge = 1; + } + return (0); + } + + if (!same_y) { + if (((y1 < b) && (b < y2)) || ((y1 > b) && (b > y2))) { + if ((x1 > a) && (x2 > a)) { + num_right++; + } else if ((x1 < a) && (x2 < a)) { + num_left++; + } else { + eps = ((x2 - x1) * (b - y1) - (a - x1) * (y2 - y1)); + if (eps == 0) { + if (edge) { + *edge = 1; + } + return (0); + } + if (((y1 < y2) && (eps > 0)) || ((y1 > y2) && (eps < 0))) { + num_right++; + } else { + num_left++; + } + } + } else if (y2 == b) { + same_y = (y1 > b) ? 1 : -1; + } + } else { + if (y2 == b) { + if (((x1 <= a) && (a <= x2)) || ((x2 <= a) && (a <= x1))) { + if (edge) { + *edge = 1; + } + return (0); + } + } else { + if (((y2 < b) && (same_y == 1)) || ((y2 > b) && (same_y == -1))) { + if (x1 < a) { + num_left++; + } else { + num_right++; + } + } + same_y = 0; + } + } + } + + if ((num_left + num_right) & 1) { + printf("ERROR in routine in_bdy_area()\n"); + CORE_DUMP; + } + + return (num_left & 1); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: closestPoint ****/ +/**** Determine whether or not a given point lies within the bounded area ****/ +/******************************************************************************************/ +std::tuple PolygonClass::closestPoint(std::tuple point) +{ + std::tuple cPoint; + double cDistSq; + + bool initFlag = true; + int xval, yval; + std::tie(xval, yval) = point; + for (int segment_idx = 0; segment_idx <= num_segment - 1; segment_idx++) { + for (int bdy_pt_idx = 0; bdy_pt_idx <= num_bdy_pt[segment_idx] - 1; bdy_pt_idx++) { + int bdy_pt_idx2 = (bdy_pt_idx + 1) % num_bdy_pt[segment_idx]; + int x0 = bdy_pt_x[segment_idx][bdy_pt_idx]; + int y0 = bdy_pt_y[segment_idx][bdy_pt_idx]; + int x1 = bdy_pt_x[segment_idx][bdy_pt_idx2]; + int y1 = bdy_pt_y[segment_idx][bdy_pt_idx2]; + double Lsq = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0); + double alpha = ((xval - x0) * (x1 - x0) + (yval - y0) * (y1 - y0)) / Lsq; + double ptx, pty, dsq; + if (alpha <= 0.0) { + ptx = (double)x0; + pty = (double)y0; + } else if (alpha >= 1.0) { + ptx = (double)x1; + pty = (double)y1; + } else { + ptx = (1.0 - alpha) * x0 + alpha * x1; + pty = (1.0 - alpha) * y0 + alpha * y1; + } + dsq = (ptx - xval) * (ptx - xval) + (pty - yval) * (pty - yval); + + if (initFlag || (dsq < cDistSq)) { + cDistSq = dsq; + cPoint = std::tuple(ptx, pty); + initFlag = false; + } + } + } + + return (cPoint); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: calcHorizExtents ****/ +/******************************************************************************************/ +void PolygonClass::calcHorizExtents(double yVal, double &xA, double &xB, bool &flag) const +{ + flag = false; + for (int segment_idx = 0; segment_idx <= num_segment - 1; segment_idx++) { + for (int bdy_pt_idx = 0; bdy_pt_idx <= num_bdy_pt[segment_idx] - 1; bdy_pt_idx++) { + int bdy_pt_idx2 = (bdy_pt_idx + 1) % num_bdy_pt[segment_idx]; + int x0 = bdy_pt_x[segment_idx][bdy_pt_idx]; + int y0 = bdy_pt_y[segment_idx][bdy_pt_idx]; + int x1 = bdy_pt_x[segment_idx][bdy_pt_idx2]; + int y1 = bdy_pt_y[segment_idx][bdy_pt_idx2]; + + double xVal; + bool found = false; + if ((double)y0 == yVal) { + found = true; + xVal = (double)x0; + } else if ((((double)y0 < yVal) && ((double)y1 >= yVal)) || + (((double)y0 > yVal) && ((double)y1 <= yVal))) { + found = true; + xVal = (x0 * (y1 - yVal) + x1 * (yVal - y0)) / (y1 - y0); + } + if (found) { + if (!flag) { + flag = true; + xA = xVal; + xB = xVal; + } else if (xVal < xA) { + xA = xVal; + } else if (xVal > xB) { + xB = xVal; + } + } + } + } + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: calcHorizExtents ****/ +/******************************************************************************************/ +void PolygonClass::calcVertExtents(double xVal, double &yA, double &yB, bool &flag) const +{ + flag = false; + for (int segment_idx = 0; segment_idx <= num_segment - 1; segment_idx++) { + for (int bdy_pt_idx = 0; bdy_pt_idx <= num_bdy_pt[segment_idx] - 1; bdy_pt_idx++) { + int bdy_pt_idx2 = (bdy_pt_idx + 1) % num_bdy_pt[segment_idx]; + int x0 = bdy_pt_x[segment_idx][bdy_pt_idx]; + int y0 = bdy_pt_y[segment_idx][bdy_pt_idx]; + int x1 = bdy_pt_x[segment_idx][bdy_pt_idx2]; + int y1 = bdy_pt_y[segment_idx][bdy_pt_idx2]; + + double yVal; + bool found = false; + if ((double)x0 == xVal) { + found = true; + yVal = (double)y0; + } else if ((((double)x0 < xVal) && ((double)x1 >= xVal)) || + (((double)x0 > xVal) && ((double)x1 <= xVal))) { + found = true; + yVal = (y0 * (x1 - xVal) + y1 * (xVal - x0)) / (x1 - x0); + } + if (found) { + if (!flag) { + flag = true; + yA = yVal; + yB = yVal; + } else if (yVal < yA) { + yA = yVal; + } else if (yVal > yB) { + yB = yVal; + } + } + } + } + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PolygonClass::combinePolygons ****/ +/******************************************************************************************/ +PolygonClass *PolygonClass::combinePolygons(std::vector polyList) +{ + PolygonClass *combinedPoly = new PolygonClass(); + + int totalNumSegment = 0; + for (int polyIdx = 0; polyIdx < polyList.size(); ++polyIdx) { + totalNumSegment += polyList[polyIdx]->num_segment; + } + + combinedPoly->num_segment = totalNumSegment; + + combinedPoly->num_bdy_pt = (int *)malloc(totalNumSegment * sizeof(int)); + combinedPoly->bdy_pt_x = (int **)malloc(totalNumSegment * sizeof(int *)); + combinedPoly->bdy_pt_y = (int **)malloc(totalNumSegment * sizeof(int *)); + + int segIdx = 0; + for (int polyIdx = 0; polyIdx < polyList.size(); ++polyIdx) { + PolygonClass *poly = polyList[polyIdx]; + for (int segment_idx = 0; segment_idx <= poly->num_segment - 1; segment_idx++) { + int numPt = poly->num_bdy_pt[segment_idx]; + combinedPoly->num_bdy_pt[segIdx] = numPt; + combinedPoly->bdy_pt_x[segIdx] = (int *)malloc(numPt * sizeof(int)); + combinedPoly->bdy_pt_y[segIdx] = (int *)malloc(numPt * sizeof(int)); + for (int bdy_pt_idx = 0; bdy_pt_idx < numPt; bdy_pt_idx++) { + combinedPoly->bdy_pt_x[segIdx][bdy_pt_idx] = + poly->bdy_pt_x[segment_idx][bdy_pt_idx]; + combinedPoly->bdy_pt_y[segIdx][bdy_pt_idx] = + poly->bdy_pt_y[segment_idx][bdy_pt_idx]; + } + segIdx++; + } + } + + return combinedPoly; +} +/******************************************************************************************/ diff --git a/src/afc-engine/polygon.h b/src/afc-engine/polygon.h new file mode 100644 index 0000000..7b31415 --- /dev/null +++ b/src/afc-engine/polygon.h @@ -0,0 +1,59 @@ +/******************************************************************************************/ +/**** FILE: polygon.h ****/ +/******************************************************************************************/ + +#ifndef POLYGON_H +#define POLYGON_H + +#include +#include + +/******************************************************************************************/ +/**** CLASS: PolygonClass ****/ +/******************************************************************************************/ +class PolygonClass +{ + public: + PolygonClass(); + PolygonClass(std::vector> *ii_list); + PolygonClass(std::string kmlFilename, double resolution); + ~PolygonClass(); + + static std::vector readMultiGeometry(std::string kmlFilename, + double resolution); + bool in_bdy_area(const int a, const int b, bool *edge = (bool *)NULL); + double comp_bdy_area(); + void comp_bdy_min_max(int &minx, int &maxx, int &miny, int &maxy); + void remove_duplicate_points(int segment_idx); + void translate(int x, int y); + void reverse(); + PolygonClass *duplicate(); + std::tuple closestPoint(std::tuple point); + void calcHorizExtents(double yVal, double &xA, double &xB, bool &flag) const; + void calcVertExtents(double xVal, double &yA, double &yB, bool &flag) const; + + static PolygonClass *combinePolygons(std::vector polyList); + static double comp_bdy_area(const int n, const int *x, const int *y); + static double comp_bdy_area(std::vector> *ii_list); + static int in_bdy_area(const int a, + const int b, + const int n, + const int *x, + const int *y, + int *edge = (int *)NULL); + + std::string name; + int num_segment; + int *num_bdy_pt; + int **bdy_pt_x, **bdy_pt_y; + + private: + void comp_bdy_min_max(int &minx, + int &maxx, + int &miny, + int &maxy, + const int segment_idx); +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/pop_grid.cpp b/src/afc-engine/pop_grid.cpp new file mode 100644 index 0000000..c0c5490 --- /dev/null +++ b/src/afc-engine/pop_grid.cpp @@ -0,0 +1,1653 @@ +/******************************************************************************************/ +/**** FILE : pop_grid.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gdal_priv.h" +#include "ogr_spatialref.h" + +#include "cconst.h" +#include "pop_grid.h" +#include "global_defines.h" +#include "local_defines.h" +#include "global_fn.h" +#include "spline.h" +#include "polygon.h" +#include "list.h" +#include "uls.h" +#include "EcefModel.h" + +#include "afclogging/Logging.h" +#include "afclogging/ErrStream.h" +#include "PopulationDatabase.h" +#include "AfcDefinitions.h" + +namespace +{ +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "pop_grid") + +} // end namespace + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::PopGridClass() ****/ +/******************************************************************************************/ +PopGridClass::PopGridClass(double densityThrUrbanVal, + double densityThrSuburbanVal, + double densityThrRuralVal) : + densityThrUrban(densityThrUrbanVal), + densityThrSuburban(densityThrSuburbanVal), + densityThrRural(densityThrRuralVal) +{ + minLonDeg = quietNaN; + minLatDeg = quietNaN; + deltaLonDeg = quietNaN; + deltaLatDeg = quietNaN; + + numLon = 0; + numLat = 0; + + pop = (double **)NULL; + propEnv = (char **)NULL; + region = (int **)NULL; + numRegion = (int)regionNameList.size(); + + isCumulative = false; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: PopGridClass::PopGridClass() ****/ +/******************************************************************************************/ +PopGridClass::PopGridClass(std::string worldPopulationFile, + const std::vector ®ionPolygonList, + double regionPolygonResolution, + double densityThrUrbanVal, + double densityThrSuburbanVal, + double densityThrRuralVal, + double minLat, + double minLon, + double maxLat, + double maxLon) : + densityThrUrban(densityThrUrbanVal), + densityThrSuburban(densityThrSuburbanVal), + densityThrRural(densityThrRuralVal) +{ + // NOTE: Each point in the PopGridClass is taken to be in the first region in which the + // point is contained. So, for regions contained in another region, specify interior region + // first. For example, if regions are defined for india and asia, specifying india first + // effectively defines two regions as india and "asia except india". + + std::ostringstream errStr; + if (worldPopulationFile.empty()) { + throw std::invalid_argument("worldPopulationFile is empty"); + } + + int regionIdx; + +#if DEBUG_AFC + FILE *fchk = (FILE *)NULL; + + #if 0 + if ( !(fchk = fopen("/tmp/test_pop.csv", "wb")) ) { + throw std::runtime_error("ERROR"); + } + #endif + if (fchk) { + fprintf(fchk, "Latitude (deg),Longitude (deg),Density (people/sq-km)\n"); + } +#endif + + numRegion = regionPolygonList.size(); + + std::cout << "Loading world polulation file " << worldPopulationFile << std::endl; + + GDALDataset *gdalDataset = static_cast( + GDALOpen(worldPopulationFile.c_str(), GA_ReadOnly)); + + int nXSize = gdalDataset->GetRasterXSize(); + int nYSize = gdalDataset->GetRasterYSize(); + int numRasterBand = gdalDataset->GetRasterCount(); + + double adfGeoTransform[6]; + printf("Size is %dx%dx%d\n", nXSize, nYSize, numRasterBand); + CPLErr readError = gdalDataset->GetGeoTransform(adfGeoTransform); + if (readError != CPLErr::CE_None) { + errStr << "ERROR: getting GEO Transform" << worldPopulationFile + << ", throwing CPLErr = " << readError; + throw std::runtime_error(errStr.str()); + } + + // const char *pszProjection = nullptr; + printf("Origin = (%.6f,%.6f)\n", adfGeoTransform[0], adfGeoTransform[3]); + printf("Pixel Size = (%.6f,%.6f)\n", adfGeoTransform[1], adfGeoTransform[5]); + // pszProjection = GDALGetProjectionRef(gdalDataset); + + double pixelSize = adfGeoTransform[1]; + if (fabs(pixelSize + adfGeoTransform[5]) > 1.0e-8) { + throw std::runtime_error("ERROR: X / Y pixel sizes not properly set"); + } + + double ULX = adfGeoTransform[0] + adfGeoTransform[1] * 0 + adfGeoTransform[2] * 0; + double ULY = adfGeoTransform[3] + adfGeoTransform[4] * 0 + adfGeoTransform[5] * 0; + + double LLX = adfGeoTransform[0] + adfGeoTransform[1] * 0 + adfGeoTransform[2] * nYSize; + double LLY = adfGeoTransform[3] + adfGeoTransform[4] * 0 + adfGeoTransform[5] * nYSize; + + double URX = adfGeoTransform[0] + adfGeoTransform[1] * nXSize + adfGeoTransform[2] * 0; + double URY = adfGeoTransform[3] + adfGeoTransform[4] * nXSize + adfGeoTransform[5] * 0; + + double LRX = adfGeoTransform[0] + adfGeoTransform[1] * nXSize + adfGeoTransform[2] * nYSize; + double LRY = adfGeoTransform[3] + adfGeoTransform[4] * nXSize + adfGeoTransform[5] * nYSize; + + if ((ULX != LLX) || (URX != LRX) || (LLY != LRY) || (ULY != URY)) { + errStr << "ERROR: Inconsistent bounding box in world population file: " + << worldPopulationFile; + throw std::runtime_error(errStr.str()); + } + + double worldMinLon = LLX; + double worldMinLat = LLY; + double worldMaxLon = URX; + double worldMaxLat = URY; + + if ((fabs(worldMinLon + 180.0) > 1.0e-8) || (fabs(worldMaxLon - 180.0) > 1.0e-8) || + (fabs(worldMinLat + 90.0) > 1.0e-8) || (fabs(worldMaxLat - 90.0) > 1.0e-8)) { + errStr << "ERROR: world population file: " << worldPopulationFile + << " does not cover region LON: -180,180 LAT: -90,90"; + throw std::runtime_error(errStr.str()); + } + + double resLon = (worldMaxLon - worldMinLon) / nXSize; + double resLat = (worldMaxLat - worldMinLat) / nYSize; + double resLonLat = std::min(resLon, resLat); + + std::cout << "UL LONLAT: " << ULX << " " << ULY << std::endl; + std::cout << "LL LONLAT: " << LLX << " " << LLY << std::endl; + std::cout << "UR LONLAT: " << URX << " " << URY << std::endl; + std::cout << "LR LONLAT: " << LRX << " " << LRY << std::endl; + + std::cout << "RES_LON = " << resLon << std::endl; + std::cout << "RES_LAT = " << resLat << std::endl; + std::cout << "RES_LONLAT = " << resLonLat << std::endl; + + std::cout << "NUMBER RASTER BANDS: " << numRasterBand << std::endl; + + if (numRasterBand != 1) { + throw std::runtime_error("ERROR numRasterBand must be 1"); + } + + int nBlockXSize, nBlockYSize; + GDALRasterBand *rasterBand = gdalDataset->GetRasterBand(1); + char **rasterMetadata = rasterBand->GetMetadata(); + + if (rasterMetadata) { + std::cout << "RASTER METADATA: " << std::endl; + char **chptr = rasterMetadata; + while (*chptr) { + std::cout << " " << *chptr << std::endl; + chptr++; + } + } else { + std::cout << "NO RASTER METADATA: " << std::endl; + } + + rasterBand->GetBlockSize(&nBlockXSize, &nBlockYSize); + printf("Block=%dx%d Type=%s, ColorInterp=%s\n", + nBlockXSize, + nBlockYSize, + GDALGetDataTypeName(rasterBand->GetRasterDataType()), + GDALGetColorInterpretationName(rasterBand->GetColorInterpretation())); + + int bGotMin, bGotMax; + double adfMinMax[2]; + adfMinMax[0] = rasterBand->GetMinimum(&bGotMin); + adfMinMax[1] = rasterBand->GetMaximum(&bGotMax); + if (!(bGotMin && bGotMax)) { + std::cout << "calling GDALComputeRasterMinMax()" << std::endl; + GDALComputeRasterMinMax((GDALRasterBandH)rasterBand, TRUE, adfMinMax); + } + + printf("Min=%.3f\nMax=%.3f\n", adfMinMax[0], adfMinMax[1]); + if (rasterBand->GetOverviewCount() > 0) { + printf("Band has %d overviews.\n", rasterBand->GetOverviewCount()); + } + if (rasterBand->GetColorTable() != NULL) { + printf("Band has a color table with %d entries.\n", + rasterBand->GetColorTable()->GetColorEntryCount()); + } + + int hasNoData; + double origNodataValue = rasterBand->GetNoDataValue(&hasNoData); + float origNodataValueFloat = (float)origNodataValue; + if (hasNoData) { + std::cout << "ORIG NODATA: " << origNodataValue << std::endl; + std::cout << "ORIG NODATA (float): " << origNodataValueFloat << std::endl; + } else { + std::cout << "ORIG NODATA undefined" << std::endl; + } + + int minx = (int)floor(minLon / regionPolygonResolution); + int maxx = (int)floor(maxLon / regionPolygonResolution) + 1; + int miny = (int)floor(minLat / regionPolygonResolution); + int maxy = (int)floor(maxLat / regionPolygonResolution) + 1; + + minLonDeg = minx * regionPolygonResolution; + minLatDeg = miny * regionPolygonResolution; + double maxLonDeg = maxx * regionPolygonResolution; + double maxLatDeg = maxy * regionPolygonResolution; + + deltaLonDeg = resLon; + deltaLatDeg = resLat; + + int minLonIdx = (int)floor((minLonDeg - worldMinLon) / resLon + 0.5); + int maxLonIdx = (int)floor((maxLonDeg - worldMinLon) / resLon + 0.5); + int minLatIdx = (int)floor((minLatDeg - worldMinLat) / resLat + 0.5); + int maxLatIdx = (int)floor((maxLatDeg - worldMinLat) / resLat + 0.5); + + std::cout << "REGION MIN LON DEG: " << minLonDeg << std::endl; + std::cout << "REGION MIN LAT DEG: " << minLatDeg << std::endl; + std::cout << "REGION MAX LON DEG: " << maxLonDeg << std::endl; + std::cout << "REGION MAX LAT DEG: " << maxLatDeg << std::endl; + + std::cout << std::endl; + + std::cout << "REGION MIN LON IDX: " << minLonIdx << std::endl; + std::cout << "REGION MIN LAT IDX: " << minLatIdx << std::endl; + std::cout << "REGION MAX LON IDX: " << maxLonIdx << std::endl; + std::cout << "REGION MAX LAT IDX: " << maxLatIdx << std::endl; + + std::cout << std::endl; + + bool wrapLonFlag; + if (maxLonIdx > nXSize - 1) { + wrapLonFlag = true; + } else { + wrapLonFlag = false; + } + + std::cout << "Analysis region wraps around LON discontinuity at +/- 180 deg: " + << (wrapLonFlag ? "YES" : "NO") << std::endl; + + /**************************************************************************************/ + /**** Set parameters ****/ + /**************************************************************************************/ + numLon = maxLonIdx - minLonIdx; + minLonDeg = worldMinLon + minLonIdx * resLon; + + numLat = maxLatIdx - minLatIdx; + minLatDeg = worldMinLat + minLatIdx * resLat; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Allocate matrix and initialize to zeros ****/ + /**************************************************************************************/ + pop = (double **)malloc(numLon * sizeof(double *)); + propEnv = (char **)malloc(numLon * sizeof(char *)); + region = (int **)malloc(numLon * sizeof(int *)); + for (int lonIdx = 0; lonIdx < numLon; lonIdx++) { + pop[lonIdx] = (double *)malloc(numLat * sizeof(double)); + propEnv[lonIdx] = (char *)malloc(numLat * sizeof(char)); + region[lonIdx] = (int *)malloc(numLat * sizeof(int)); + for (int latIdx = 0; latIdx < numLat; latIdx++) { + pop[lonIdx][latIdx] = 0.0; + propEnv[lonIdx][latIdx] = 'X'; + region[lonIdx][latIdx] = -1; + } + } + isCumulative = false; + /**************************************************************************************/ + + urbanPop.resize(numRegion); + suburbanPop.resize(numRegion); + ruralPop.resize(numRegion); + barrenPop.resize(numRegion); + + std::vector urbanArea(numRegion); + std::vector suburbanArea(numRegion); + std::vector ruralArea(numRegion); + std::vector barrenArea(numRegion); + std::vector zeroArea(numRegion); + + for (regionIdx = 0; regionIdx < numRegion; regionIdx++) { + urbanPop[regionIdx] = 0.0; + suburbanPop[regionIdx] = 0.0; + ruralPop[regionIdx] = 0.0; + barrenPop[regionIdx] = 0.0; + + urbanArea[regionIdx] = 0.0; + suburbanArea[regionIdx] = 0.0; + ruralArea[regionIdx] = 0.0; + barrenArea[regionIdx] = 0.0; + } + + double totalArea = 0.0; + double totalPop = 0.0; + + int lonIdx, latIdx; + std::cout << "numLon: " << numLon << std::endl; + std::cout << "numLat: " << numLat << std::endl; + // std::cout << "GDALGetDataTypeSizeBytes(GDT_Float32) = " << + // GDALGetDataTypeSizeBytes(GDT_Float32) << std::endl; + std::cout << "sizeof(GDT_Float32) = " << sizeof(GDT_Float32) << std::endl; + std::cout << "sizeof(GDT_Float64) = " << sizeof(GDT_Float64) << std::endl; + std::cout << "sizeof(float) = " << sizeof(float) << std::endl; + float *pafScanline = (float *)CPLMalloc(numLon * GDALGetDataTypeSize(GDT_Float32)); + + double areaGridEquator = CConst::earthRadius * CConst::earthRadius * + (deltaLonDeg * M_PI / 180.0) * (deltaLatDeg * M_PI / 180.0); + + for (latIdx = 0; latIdx < numLat; latIdx++) { + if (wrapLonFlag) { + rasterBand->RasterIO(GF_Read, + minLonIdx, + nYSize - 1 - minLatIdx - latIdx, + nXSize - minLonIdx, + 1, + pafScanline, + nXSize - minLonIdx, + 1, + GDT_Float32, + 0, + 0); + rasterBand->RasterIO(GF_Read, + 0, + nYSize - 1 - minLatIdx - latIdx, + numLon - (nXSize - minLonIdx), + 1, + pafScanline + nXSize - minLonIdx, + numLon - (nXSize - minLonIdx), + 1, + GDT_Float32, + 0, + 0); + } else { + rasterBand->RasterIO(GF_Read, + minLonIdx, + nYSize - 1 - minLatIdx - latIdx, + numLon, + 1, + pafScanline, + numLon, + 1, + GDT_Float32, + 0, + 0); + } + double latitudeDeg = (maxLatDeg * (2 * latIdx + 1) + + minLatDeg * (2 * numLat - 2 * latIdx - 1)) / + (2 * numLat); + int polygonY = (int)floor(latitudeDeg / regionPolygonResolution + 0.5); + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + // if ((lonIdx == 1382) && (latIdx == 1425) ) { + // std::cout << "CHECK POINT" << std::endl; + // } + + if (pafScanline[lonIdx] != origNodataValueFloat) { + double longitudeDeg = (maxLonDeg * (2 * lonIdx + 1) + + minLonDeg * (2 * numLon - 2 * lonIdx - 1)) / + (2 * numLon); + // std::cout << longitudeDeg << " " << latitudeDeg << std::endl; + int polygonX = (int)floor(longitudeDeg / regionPolygonResolution + + 0.5); + bool foundRegion = false; + for (regionIdx = 0; + (regionIdx < (int)regionPolygonList.size()) && (!foundRegion); + ++regionIdx) { + PolygonClass *regionPolygon = regionPolygonList[regionIdx]; + if (regionPolygon->in_bdy_area(polygonX, polygonY)) { + foundRegion = true; + + double density = + pafScanline[lonIdx] * + 1.0e-6; // convert from people/sq-km to + // people/sqm; + double area = areaGridEquator * + cos(latitudeDeg * M_PI / 180.0); + + pop[lonIdx][latIdx] = density * area; + region[lonIdx][latIdx] = regionIdx; + + if (density == 0.0) { + zeroArea[regionIdx] += area; + } + + if (density >= densityThrUrban) { + urbanPop[regionIdx] += density * area; + if (density != 0.0) { + urbanArea[regionIdx] += area; + } + propEnv[lonIdx][latIdx] = 'U'; + } else if (density >= densityThrSuburban) { + suburbanPop[regionIdx] += density * area; + if (density != 0.0) { + suburbanArea[regionIdx] += area; + } + propEnv[lonIdx][latIdx] = 'S'; + } else if (density >= densityThrRural) { + ruralPop[regionIdx] += density * area; + if (density != 0.0) { + ruralArea[regionIdx] += area; + } + propEnv[lonIdx][latIdx] = 'R'; + } else { + barrenPop[regionIdx] += density * area; + if (density != 0.0) { + barrenArea[regionIdx] += area; + } + propEnv[lonIdx][latIdx] = 'B'; + } + + totalArea += area; + totalPop += pop[lonIdx][latIdx]; + +#if DEBUG_AFC + if (fchk) { + fprintf(fchk, + "%.4f,%.4f,%.6f\n", + latitudeDeg, + longitudeDeg, + density * 1.0e6); + } +#endif + } + } + } + } + } +#if DEBUG_AFC + if (fchk) { + fclose(fchk); + } +#endif + + std::cout << "TOTAL INTEGRATED POPULATION: " << totalPop << std::endl; + std::cout << "TOTAL INTEGRATED AREA: " << totalArea << std::endl; + std::cout << std::endl; + if ((totalPop > 0.0) && (totalArea > 0.0)) { + for (regionIdx = 0; regionIdx < numRegion; regionIdx++) { + PolygonClass *regionPolygon = regionPolygonList[regionIdx]; + double regionPop = urbanPop[regionIdx] + suburbanPop[regionIdx] + + ruralPop[regionIdx] + barrenPop[regionIdx]; + std::cout << "REGION " << regionPolygon->name + << " URBAN POPULATION: " << urbanPop[regionIdx] << " " + << 100 * urbanPop[regionIdx] / totalPop << " % (total)" + << " " << 100 * urbanPop[regionIdx] / regionPop << " % (region)" + << std::endl; + std::cout << "REGION " << regionPolygon->name + << " SUBURBAN POPULATION: " << suburbanPop[regionIdx] << " " + << 100 * suburbanPop[regionIdx] / totalPop << " % (total)" + << " " << 100 * suburbanPop[regionIdx] / regionPop + << " % (region)" << std::endl; + std::cout << "REGION " << regionPolygon->name + << " RURAL POPULATION: " << ruralPop[regionIdx] << " " + << 100 * ruralPop[regionIdx] / totalPop << " % (total)" + << " " << 100 * ruralPop[regionIdx] / regionPop << " % (region)" + << std::endl; + std::cout << "REGION " << regionPolygon->name + << " BARREN POPULATION: " << barrenPop[regionIdx] << " " + << 100 * barrenPop[regionIdx] / totalPop << " % (total)" + << " " << 100 * barrenPop[regionIdx] / regionPop << " % (region)" + << std::endl; + std::cout << std::endl; + + double regionArea = urbanArea[regionIdx] + suburbanArea[regionIdx] + + ruralArea[regionIdx] + barrenArea[regionIdx] + + zeroArea[regionIdx]; + std::cout << "REGION " << regionPolygon->name + << " URBAN_NZ AREA: " << urbanArea[regionIdx] << " " + << 100 * urbanArea[regionIdx] / totalArea << " % (total)" + << " " << 100 * urbanArea[regionIdx] / regionArea << " % (region)" + << std::endl; + std::cout << "REGION " << regionPolygon->name + << " SUBURBAN_NZ AREA: " << suburbanArea[regionIdx] << " " + << 100 * suburbanArea[regionIdx] / totalArea << " % (total)" + << " " << 100 * suburbanArea[regionIdx] / regionArea + << " % (region)" << std::endl; + std::cout << "REGION " << regionPolygon->name + << " RURAL_NZ AREA: " << ruralArea[regionIdx] << " " + << 100 * ruralArea[regionIdx] / totalArea << " % (total)" + << " " << 100 * ruralArea[regionIdx] / regionArea << " % (region)" + << std::endl; + std::cout << "REGION " << regionPolygon->name + << " BARREN_NZ AREA: " << barrenArea[regionIdx] << " " + << 100 * barrenArea[regionIdx] / totalArea << " % (total)" + << " " << 100 * barrenArea[regionIdx] / regionArea + << " % (region)" << std::endl; + std::cout << "REGION " << regionPolygon->name + << " ZERO-POP AREA: " << zeroArea[regionIdx] << " " + << 100 * zeroArea[regionIdx] / totalArea << " % (total)" + << " " << 100 * zeroArea[regionIdx] / regionArea << " % (region)" + << std::endl; + std::cout << std::endl; + } + } + std::cout << std::flush; + /**************************************************************************************/ + + CPLFree(pafScanline); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** COPY CONSTRUCTOR: PopGridClass::PopGridClass(const PopGridClass &obj) ****/ +/******************************************************************************************/ +PopGridClass::PopGridClass(const PopGridClass &obj) : regionNameList(obj.regionNameList) +{ + densityThrUrban = obj.densityThrUrban; + densityThrSuburban = obj.densityThrSuburban; + densityThrRural = obj.densityThrRural; + minLonDeg = obj.minLonDeg; + minLatDeg = obj.minLatDeg; + deltaLonDeg = obj.deltaLonDeg; + deltaLatDeg = obj.deltaLatDeg; + isCumulative = obj.isCumulative; + numLon = obj.numLon; + numLat = obj.numLat; + numRegion = obj.numRegion; + + /**************************************************************************************/ + /**** Allocate matrix and copy values ****/ + /**************************************************************************************/ + int lonIdx, latIdx; + pop = (double **)malloc(numLon * sizeof(double *)); + propEnv = (char **)malloc(numLon * sizeof(char *)); + region = (int **)malloc(numLon * sizeof(int *)); + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + pop[lonIdx] = (double *)malloc(numLat * sizeof(double)); + propEnv[lonIdx] = (char *)malloc(numLat * sizeof(char)); + region[lonIdx] = (int *)malloc(numLat * sizeof(int)); + for (latIdx = 0; latIdx < numLat; latIdx++) { + pop[lonIdx][latIdx] = obj.pop[lonIdx][latIdx]; + propEnv[lonIdx][latIdx] = obj.propEnv[lonIdx][latIdx]; + region[lonIdx][latIdx] = obj.region[lonIdx][latIdx]; + } + } + /**************************************************************************************/ + + urbanPop = obj.urbanPop; + suburbanPop = obj.suburbanPop; + ruralPop = obj.ruralPop; + barrenPop = obj.barrenPop; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::~PopGridClass() ****/ +/******************************************************************************************/ +PopGridClass::~PopGridClass() +{ + int lonIdx; + + if (pop) { + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + free(pop[lonIdx]); + } + free(pop); + } + + if (propEnv) { + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + free(propEnv[lonIdx]); + } + free(propEnv); + } + + if (region) { + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + free(region[lonIdx]); + } + free(region); + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** GET/SET functions ****/ +/******************************************************************************************/ +int PopGridClass::getNumLon() +{ + return (numLon); +} +int PopGridClass::getNumLat() +{ + return (numLat); +} +double PopGridClass::getDensityThrUrban() +{ + return (densityThrUrban); +} +double PopGridClass::getDensityThrSuburban() +{ + return (densityThrSuburban); +} +double PopGridClass::getDensityThrRural() +{ + return (densityThrRural); +} + +void PopGridClass::setPop(int lonIdx, int latIdx, double popVal) +{ + pop[lonIdx][latIdx] = popVal; + return; +} +void PopGridClass::setPropEnv(int lonIdx, int latIdx, char propEnvVal) +{ + propEnv[lonIdx][latIdx] = propEnvVal; + return; +} + +double PopGridClass::getPropEnvPop(CConst::PropEnvEnum propEnvVal, int regionIdx) const +{ + double popVal; + switch (propEnvVal) { + case CConst::urbanPropEnv: + popVal = urbanPop[regionIdx]; + break; + case CConst::suburbanPropEnv: + popVal = suburbanPop[regionIdx]; + break; + case CConst::ruralPropEnv: + popVal = ruralPop[regionIdx]; + break; + case CConst::barrenPropEnv: + popVal = barrenPop[regionIdx]; + break; + default: + throw std::runtime_error( + ErrStream() << "ERROR in PopGridClass::getPropEnvPop: propEnvVal = " + << propEnvVal << " INVALID value"); + break; + } + + return (popVal); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::getPropEnv() ****/ +/******************************************************************************************/ +char PopGridClass::getPropEnv(int lonIdx, int latIdx) const +{ + return propEnv[lonIdx][latIdx]; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::getLonLatDeg() ****/ +/******************************************************************************************/ +void PopGridClass::getLonLatDeg(int lonIdx, int latIdx, double &longitudeDeg, double &latitudeDeg) +{ + longitudeDeg = minLonDeg + (lonIdx + 0.5) * deltaLonDeg; + latitudeDeg = minLatDeg + (latIdx + 0.5) * deltaLatDeg; + + if (longitudeDeg > 180.0) { + longitudeDeg -= 360.0; + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::getPop() ****/ +/******************************************************************************************/ +double PopGridClass::getPop(int lonIdx, int latIdx) const +{ + if (isCumulative) { + throw("ERROR in PopGridClass::getPop(), pop grid is cumulative\n"); + } + + return pop[lonIdx][latIdx]; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::getPopFromCDF() ****/ +/******************************************************************************************/ +double PopGridClass::getPopFromCDF(int lonIdx, int latIdx) const +{ + double population; + + if (!isCumulative) { + throw("ERROR in PopGridClass::getPopFromCDF(), pop grid not cumulative\n"); + } + + if ((lonIdx == 0) && (latIdx == 0)) { + population = pop[0][0]; + } else if (latIdx == 0) { + population = pop[lonIdx][latIdx] - pop[lonIdx - 1][numLat - 1]; + } else { + population = pop[lonIdx][latIdx] - pop[lonIdx][latIdx - 1]; + } + + return population; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::getProbFromCDF() ****/ +/******************************************************************************************/ +double PopGridClass::getProbFromCDF(int lonIdx, int latIdx) const +{ + double population = getPopFromCDF(lonIdx, latIdx); + + double prob = population / pop[numLon - 1][numLat - 1]; + + return prob; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::readData() ****/ +/******************************************************************************************/ +void PopGridClass::readData(std::string filename, + const std::vector ®ionNameListVal, + const std::vector ®ionIDListVal, + int numLonVal, + double deltaLonDegVal, + double minLonDegVal, + int numLatVal, + double deltaLatDegVal, + double minLatDegVal) +{ + LOGGER_INFO(logger) << "Reading population density file: " << filename << " ..."; + + int lonIdx, latIdx; + + if (regionIDListVal.size() != regionNameListVal.size()) { + throw("ERROR creating PopGridClass, inconsistent region name and ID lists\n"); + } + + regionNameList = regionNameListVal; + + numRegion = regionIDListVal.size(); + + /**************************************************************************************/ + /**** Set parameters ****/ + /**************************************************************************************/ + numLon = numLonVal; + deltaLonDeg = deltaLonDegVal; + minLonDeg = minLonDegVal; + + numLat = numLatVal; + deltaLatDeg = deltaLatDegVal; + minLatDeg = minLatDegVal; + + double maxLonDeg = minLonDeg + numLon * deltaLonDeg; + double maxLatDeg = minLatDeg + numLat * deltaLatDeg; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Allocate matrix and initialize to zeros ****/ + /**************************************************************************************/ + pop = (double **)malloc(numLon * sizeof(double *)); + propEnv = (char **)malloc(numLon * sizeof(char *)); + region = (int **)malloc(numLon * sizeof(int *)); + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + pop[lonIdx] = (double *)malloc(numLat * sizeof(double)); + propEnv[lonIdx] = (char *)malloc(numLat * sizeof(char)); + region[lonIdx] = (int *)malloc(numLat * sizeof(int)); + for (latIdx = 0; latIdx < numLat; latIdx++) { + pop[lonIdx][latIdx] = 0.0; // default to no population + propEnv[lonIdx][latIdx] = 'B'; // Default to barren outside defined area + region[lonIdx][latIdx] = 0; // default to first region + } + } + /**************************************************************************************/ + + // variables updated for each row in database + double longitudeDeg, latitudeDeg; + double density; + int regionIdx, regionVal; + double area; + double lonIdxDbl; + double latIdxDbl; + + urbanPop.resize(numRegion); + suburbanPop.resize(numRegion); + ruralPop.resize(numRegion); + barrenPop.resize(numRegion); + + std::vector urbanArea(numRegion); + std::vector suburbanArea(numRegion); + std::vector ruralArea(numRegion); + std::vector barrenArea(numRegion); + + for (regionIdx = 0; regionIdx < numRegion; regionIdx++) { + urbanPop[regionIdx] = 0.0; + suburbanPop[regionIdx] = 0.0; + ruralPop[regionIdx] = 0.0; + barrenPop[regionIdx] = 0.0; + + urbanArea[regionIdx] = 0.0; + suburbanArea[regionIdx] = 0.0; + ruralArea[regionIdx] = 0.0; + barrenArea[regionIdx] = 0.0; + } + + double totalArea = 0.0; + double totalPop = 0.0; + // bool foundLabelLine = false; + // bool hasRegion = false; + + std::vector rows; + PopulationDatabase::loadPopulationData(QString::fromStdString(filename), + rows, + minLatDeg, + maxLatDeg, + minLonDeg, + maxLonDeg); // add buffer *1.5 + + // iterate through returned rows in population db and add them to members + for (int r = 0; r < (int)rows.size(); r++) { + // Grabs the longitude, latitude, and density from a row + longitudeDeg = rows.at(r).longitude; + latitudeDeg = rows.at(r).latitude; + density = rows.at(r).density * 1.0e-6; // convert from + + regionVal = 0; // only support 1 region + + if (longitudeDeg < minLonDeg) { + longitudeDeg += 360.0; + } + + lonIdxDbl = (longitudeDeg - minLonDeg) / deltaLonDeg; + latIdxDbl = (latitudeDeg - minLatDeg) / deltaLatDeg; + + lonIdx = (int)floor(lonIdxDbl); + latIdx = (int)floor(latIdxDbl); + + // When we store in db we only check that lat/lon values are valid coordinates, but + // not that they are on grid + if (fabs(lonIdxDbl - lonIdx - 0.5) > 0.05) { + throw std::runtime_error(ErrStream() + << "ERROR: Invalid population density data file \"" + << filename << "(" << r + << ")\" longitude value not on grid, lonIdxDbl = " + << lonIdxDbl); + } + + if (fabs(latIdxDbl - latIdx - 0.5) > 0.05) { + throw std::runtime_error(ErrStream() + << "ERROR: Invalid population density data file \"" + << filename << "(" << r + << ")\" latitude value not on grid, latIdxDbl = " + << latIdxDbl); + } + + // area of population tile + area = CConst::earthRadius * CConst::earthRadius * cos(latitudeDeg * M_PI / 180.0) * + deltaLonDeg * deltaLatDeg * (M_PI / 180.0) * (M_PI / 180.0); + + // assign data value to class members + double populationVal = density * area; + pop[lonIdx][latIdx] = populationVal; + + region[lonIdx][latIdx] = regionVal; + + if (density >= + densityThrUrban) { // Check density against different thresholds to determine + // what the population environment is (urban, suburban, etc.) + urbanPop[regionVal] += populationVal; + urbanArea[regionVal] += area; + propEnv[lonIdx][latIdx] = 'U'; // Urban + } else if (density >= densityThrSuburban) { + suburbanPop[regionVal] += populationVal; + suburbanArea[regionVal] += area; + propEnv[lonIdx][latIdx] = 'S'; // Suburban + } else if (density >= densityThrRural) { + ruralPop[regionVal] += populationVal; + ruralArea[regionVal] += area; + propEnv[lonIdx][latIdx] = 'R'; // Ruran + } else { + barrenPop[regionVal] += populationVal; + barrenArea[regionVal] += area; + propEnv[lonIdx][latIdx] = 'B'; // Barren + } // Char is used since working with big files, so 8 bit char is better than 32 bit + // int + + // add to totals + totalArea += area; + totalPop += pop[lonIdx][latIdx]; + } + + LOGGER_INFO(logger) << "Lines processed: " << rows.size(); + LOGGER_INFO(logger) << "TOTAL INTEGRATED POPULATION: " << totalPop; + LOGGER_INFO(logger) << "TOTAL INTEGRATED AREA: " << totalArea; + for (regionIdx = 0; regionIdx < numRegion; regionIdx++) { + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " URBAN POPULATION: " << urbanPop[regionIdx] << " " + << 100 * urbanPop[regionIdx] / totalPop << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " SUBURBAN POPULATION: " << suburbanPop[regionIdx] << " " + << 100 * suburbanPop[regionIdx] / totalPop << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " RURAL POPULATION: " << ruralPop[regionIdx] << " " + << 100 * ruralPop[regionIdx] / totalPop << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " BARREN POPULATION: " << barrenPop[regionIdx] << " " + << 100 * barrenPop[regionIdx] / totalPop << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " URBAN AREA: " << urbanArea[regionIdx] << " " + << 100 * urbanArea[regionIdx] / totalArea << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " SUBURBAN AREA: " << suburbanArea[regionIdx] << " " + << 100 * suburbanArea[regionIdx] / totalArea << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " RURAL AREA: " << ruralArea[regionIdx] << " " + << 100 * ruralArea[regionIdx] / totalArea << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " BARREN AREA: " << barrenArea[regionIdx] << " " + << 100 * barrenArea[regionIdx] / totalArea << " %"; + } + /**************************************************************************************/ + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::setDimensions() ****/ +/******************************************************************************************/ +void PopGridClass::setDimensions(int numLonVal, + double deltaLonDegVal, + double minLonDegVal, + int numLatVal, + double deltaLatDegVal, + double minLatDegVal) +{ + int lonIdx, latIdx; + + /**************************************************************************************/ + /**** Set parameters ****/ + /**************************************************************************************/ + numLon = numLonVal; + deltaLonDeg = deltaLonDegVal; + minLonDeg = minLonDegVal; + + numLat = numLatVal; + deltaLatDeg = deltaLatDegVal; + minLatDeg = minLatDegVal; + + isCumulative = false; + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Allocate matrix and initialize to zeros ****/ + /**************************************************************************************/ + pop = (double **)malloc(numLon * sizeof(double *)); + propEnv = (char **)malloc(numLon * sizeof(char *)); + region = (int **)malloc(numLon * sizeof(int *)); + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + pop[lonIdx] = (double *)malloc(numLat * sizeof(double)); + propEnv[lonIdx] = (char *)malloc(numLat * sizeof(char)); + region[lonIdx] = (int *)malloc(numLat * sizeof(int)); + for (latIdx = 0; latIdx < numLat; latIdx++) { + pop[lonIdx][latIdx] = 0.0; + propEnv[lonIdx][latIdx] = 'X'; + region[lonIdx][latIdx] = -1; + } + } + /**************************************************************************************/ + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::scale() ****/ +/******************************************************************************************/ +void PopGridClass::scale(std::vector urbanPopVal, + std::vector suburbanPopVal, + std::vector ruralPopVal, + std::vector barrenPopVal) +{ + int regionIdx; + + if (isCumulative) { + throw("ERROR in PopGridClass::scale(), pop grid cumulative\n"); + } + + std::vector scaleUrban(numRegion); + std::vector scaleSuburban(numRegion); + std::vector scaleRural(numRegion); + std::vector scaleBarren(numRegion); + + for (regionIdx = 0; regionIdx < numRegion; regionIdx++) { + scaleUrban[regionIdx] = urbanPopVal[regionIdx] / urbanPop[regionIdx]; + scaleSuburban[regionIdx] = suburbanPopVal[regionIdx] / suburbanPop[regionIdx]; + scaleRural[regionIdx] = ruralPopVal[regionIdx] / ruralPop[regionIdx]; + scaleBarren[regionIdx] = ((barrenPopVal[regionIdx] == 0.0) ? + 0.0 : + barrenPopVal[regionIdx] / barrenPop[regionIdx]); + + urbanPop[regionIdx] = 0.0; + suburbanPop[regionIdx] = 0.0; + ruralPop[regionIdx] = 0.0; + barrenPop[regionIdx] = 0.0; + } + + int lonIdx, latIdx; + + double totalPop = 0.0; + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + for (latIdx = 0; latIdx < numLat; latIdx++) { + regionIdx = region[lonIdx][latIdx]; + switch (propEnv[lonIdx][latIdx]) { + case 'U': + pop[lonIdx][latIdx] *= scaleUrban[regionIdx]; + urbanPop[regionIdx] += pop[lonIdx][latIdx]; + break; + case 'S': + pop[lonIdx][latIdx] *= scaleSuburban[regionIdx]; + suburbanPop[regionIdx] += pop[lonIdx][latIdx]; + break; + case 'R': + pop[lonIdx][latIdx] *= scaleRural[regionIdx]; + ruralPop[regionIdx] += pop[lonIdx][latIdx]; + break; + case 'B': + pop[lonIdx][latIdx] *= scaleBarren[regionIdx]; + barrenPop[regionIdx] += pop[lonIdx][latIdx]; + break; + } + totalPop += pop[lonIdx][latIdx]; + } + } + + int totalScaledPopulation = 0; + for (regionIdx = 0; regionIdx < numRegion; regionIdx++) { + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " RLAN DEVICE URBAN POPULATION: " << urbanPop[regionIdx] + << " " << 100 * urbanPop[regionIdx] / totalPop << " %"; + LOGGER_INFO(logger) + << "REGION " << regionNameList[regionIdx] + << " RLAN DEVICE SUBURBAN POPULATION: " << suburbanPop[regionIdx] << " " + << 100 * suburbanPop[regionIdx] / totalPop << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " RLAN DEVICE RURAL POPULATION: " << ruralPop[regionIdx] + << " " << 100 * ruralPop[regionIdx] / totalPop << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " RLAN DEVICE BARREN POPULATION: " << barrenPop[regionIdx] + << " " << 100 * barrenPop[regionIdx] / totalPop << " %"; + totalScaledPopulation += urbanPop[regionIdx] + suburbanPop[regionIdx] + + ruralPop[regionIdx] + barrenPop[regionIdx]; + } + LOGGER_INFO(logger) << "TOTAL_RLAN_DEVICE_POPULATION: " << totalScaledPopulation; + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::writeDensity() ****/ +/******************************************************************************************/ +void PopGridClass::writeDensity(std::string filename, bool dumpPopGrid) +{ + FILE *fp; + std::ostringstream errStr; + + if (isCumulative) { + throw("ERROR in PopGridClass::writeDensity(), pop grid cumulative\n"); + } + + if (!(fp = fopen(filename.c_str(), "wb"))) { + throw std::runtime_error(ErrStream() << "ERROR: Unable to write to file \"" + << filename << "\""); + } + + int lonIdx, latIdx; + + if (dumpPopGrid) { + double popSum = 0.0; + fprintf(fp, "lonIdx,latIdx,pop,popSum\n"); + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + for (latIdx = 0; latIdx < numLat; latIdx++) { + popSum += pop[lonIdx][latIdx]; + fprintf(fp, + "%d,%d,%.5f,%.5f\n", + lonIdx, + latIdx, + pop[lonIdx][latIdx], + popSum); + } + } + } else { + fprintf(fp, + "Longitude (deg),Latitude (deg),Device density (#/sqkm),Propagation " + "Environment\n"); + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + double longitudeDeg = minLonDeg + (lonIdx + 0.5) * deltaLonDeg; + for (latIdx = 0; latIdx < numLat; latIdx++) { + if ((propEnv[lonIdx][latIdx] != 'X') && + (propEnv[lonIdx][latIdx] != 'B')) { + double latitudeDeg = minLatDeg + + (latIdx + 0.5) * deltaLatDeg; + double area = computeArea(lonIdx, latIdx); + + fprintf(fp, + "%.5f,%.5f,%.3f,%c\n", + longitudeDeg, + latitudeDeg, + (pop[lonIdx][latIdx] / area) * 1.0e6, + propEnv[lonIdx][latIdx]); + } + } + } + } + + fclose(fp); + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::adjustRegion ****/ +/******************************************************************************************/ +int PopGridClass::adjustRegion(double centerLongitudeDeg, double centerLatitudeDeg, double radius) +{ + int regionIdx; + + if (isCumulative) { + throw("ERROR in PopGridClass::adjustRegion(), pop grid cumulative\n"); + } + + Vector3 centerPosition = EcefModel::geodeticToEcef(centerLatitudeDeg, + centerLongitudeDeg, + 0.0); + + // Solve: radius = 2*EarthRadius*cos(centerLatitude)*sin(maxLonOffset/2); + double maxLonOffset = 2 * asin(radius / (2 * CConst::earthRadius * + cos(centerLatitudeDeg * M_PI / 180.0))); + + // Solve: radius = 2*EarthRadius*sin(maxLatOffset/2); + double maxLatOffset = 2 * asin(radius / (2 * CConst::earthRadius)) * 180.0 / M_PI; + + double newMinLon = centerLongitudeDeg - maxLonOffset; + double newMaxLon = centerLongitudeDeg + maxLonOffset; + double newMinLat = centerLatitudeDeg - maxLatOffset; + double newMaxLat = centerLatitudeDeg + maxLatOffset; + + int minLonIdx = (int)floor((newMinLon - minLonDeg) / deltaLonDeg); + int maxLonIdx = (int)floor((newMaxLon - minLonDeg) / deltaLonDeg) + 1; + int minLatIdx = (int)floor((newMinLat - minLatDeg) / deltaLatDeg); + int maxLatIdx = (int)floor((newMaxLat - minLatDeg) / deltaLatDeg) + 1; + + if (minLonIdx < 0) { + minLonIdx = 0; + } + if (minLatIdx < 0) { + minLatIdx = 0; + } + if (maxLonIdx > numLon - 1) { + maxLonIdx = numLon - 1; + } + if (maxLatIdx > numLat - 1) { + maxLatIdx = numLat - 1; + } + + int newNumLon = maxLonIdx - minLonIdx + 1; + int newNumLat = maxLatIdx - minLatIdx + 1; + + newMinLon = minLonDeg + minLonIdx * deltaLonDeg; + newMinLat = minLatDeg + minLatIdx * deltaLatDeg; + + for (regionIdx = 0; regionIdx < numRegion; regionIdx++) { + urbanPop[regionIdx] = 0.0; + suburbanPop[regionIdx] = 0.0; + ruralPop[regionIdx] = 0.0; + barrenPop[regionIdx] = 0.0; + } + + int totalPop = 0; + int lonIdx, latIdx; + /**************************************************************************************/ + /**** Allocate matrix ****/ + /**************************************************************************************/ + double **newPop = (double **)malloc(newNumLon * sizeof(double *)); + char **newPropEnv = (char **)malloc(newNumLon * sizeof(char *)); + int **newRegion = (int **)malloc(newNumLon * sizeof(int *)); + for (lonIdx = 0; lonIdx < newNumLon; lonIdx++) { + newPop[lonIdx] = (double *)malloc(newNumLat * sizeof(double)); + newPropEnv[lonIdx] = (char *)malloc(newNumLat * sizeof(char)); + newRegion[lonIdx] = (int *)malloc(newNumLat * sizeof(int)); + double lonDeg = (newMinLon + lonIdx * deltaLonDeg); + for (latIdx = 0; latIdx < newNumLat; latIdx++) { + double latDeg = (newMinLat + latIdx * deltaLatDeg); + Vector3 posn = EcefModel::geodeticToEcef(latDeg, lonDeg, 0.0); + if ((posn - centerPosition).len() * 1000.0 <= radius) { + newPop[lonIdx][latIdx] = + pop[minLonIdx + lonIdx][minLatIdx + latIdx]; + newPropEnv[lonIdx][latIdx] = + propEnv[minLonIdx + lonIdx][minLatIdx + latIdx]; + newRegion[lonIdx][latIdx] = + region[minLonIdx + lonIdx][minLatIdx + latIdx]; + } else { + newPop[lonIdx][latIdx] = 0.0; + newPropEnv[lonIdx][latIdx] = 'X'; + newRegion[lonIdx][latIdx] = -1; + } + regionIdx = newRegion[lonIdx][latIdx]; + switch (newPropEnv[lonIdx][latIdx]) { + case 'U': + urbanPop[regionIdx] += newPop[lonIdx][latIdx]; + break; + case 'S': + suburbanPop[regionIdx] += newPop[lonIdx][latIdx]; + break; + case 'R': + ruralPop[regionIdx] += newPop[lonIdx][latIdx]; + break; + case 'B': + barrenPop[regionIdx] += newPop[lonIdx][latIdx]; + break; + } + totalPop += newPop[lonIdx][latIdx]; + } + } + /**************************************************************************************/ + + if (pop) { + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + free(pop[lonIdx]); + } + free(pop); + } + + if (propEnv) { + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + free(propEnv[lonIdx]); + } + free(propEnv); + } + if (region) { + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + free(region[lonIdx]); + } + free(region); + } + + pop = newPop; + propEnv = newPropEnv; + region = newRegion; + minLonDeg = newMinLon; + minLatDeg = newMinLat; + numLon = newNumLon; + numLat = newNumLat; + + return (totalPop); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::adjustRegion ****/ +/******************************************************************************************/ +double PopGridClass::adjustRegion(ListClass *ulsList, double maxRadius) +{ + int regionIdx; + time_t t1; + char *tstr; + + t1 = time(NULL); + tstr = strdup(ctime(&t1)); + strtok(tstr, "\n"); + std::cout << tstr << " : Beginning ADJUSTING SIMULATION REGION." << std::endl; + free(tstr); + std::cout << std::flush; + + double maxDistGridCenterToEdge = CConst::earthRadius * + sqrt((deltaLonDeg * deltaLonDeg + + deltaLatDeg * deltaLatDeg) * + (M_PI / 180.0) * (M_PI / 180.0)) / + 2; + double maxDistKMSQ = (maxRadius + maxDistGridCenterToEdge) * + (maxRadius + maxDistGridCenterToEdge) * 1.0e-6; + + int lonIdx, latIdx; + int **possible = (int **)malloc(numLon * sizeof(int *)); + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + possible[lonIdx] = (int *)malloc(numLat * sizeof(int)); + for (latIdx = 0; latIdx < numLat; latIdx++) { + possible[lonIdx][latIdx] = 0; + } + } + + double minDeltaY = CConst::earthRadius * deltaLatDeg * (M_PI / 180.0); + double cosa = cos(minLatDeg * M_PI / 180.0); + double cosb = cos((minLatDeg + (numLat - 1) * deltaLatDeg) * (M_PI / 180.0)); + double mincos = std::min(cosa, cosb); + double minDeltaX = CConst::earthRadius * deltaLonDeg * mincos * (M_PI / 180.0); + int offsetLonIdx = ceil((maxRadius + maxDistGridCenterToEdge) / minDeltaX) + 1; + int offsetLatIdx = ceil((maxRadius + maxDistGridCenterToEdge) / minDeltaY) + 1; + + for (int ulsIdx = 0; ulsIdx < ulsList->getSize(); ulsIdx++) { + ULSClass *uls = (*ulsList)[ulsIdx]; + int ulsLonIdx = (int)floor((uls->getRxLongitudeDeg() - minLonDeg) / deltaLonDeg); + int ulsLatIdx = (int)floor((uls->getRxLatitudeDeg() - minLatDeg) / deltaLatDeg); + int lonIdxStart = ulsLonIdx - offsetLonIdx; + if (lonIdxStart < 0) { + lonIdxStart = 0; + } + int lonIdxStop = ulsLonIdx + offsetLonIdx; + if (lonIdxStop > numLon - 1) { + lonIdxStop = numLon - 1; + } + + int latIdxStart = ulsLatIdx - offsetLatIdx; + if (latIdxStart < 0) { + latIdxStart = 0; + } + int latIdxStop = ulsLatIdx + offsetLatIdx; + if (latIdxStop > numLat - 1) { + latIdxStop = numLat - 1; + } + + for (lonIdx = lonIdxStart; lonIdx <= lonIdxStop; lonIdx++) { + for (latIdx = latIdxStart; latIdx <= latIdxStop; latIdx++) { + possible[lonIdx][latIdx] = 1; + } + } + } + + bool initFlag = true; + int minLonIdx = -1; + int maxLonIdx = -1; + int minLatIdx = -1; + int maxLatIdx = -1; + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + double longitudeDeg = minLonDeg + lonIdx * deltaLonDeg; + for (latIdx = 0; latIdx < numLat; latIdx++) { + double latitudeDeg = minLatDeg + latIdx * deltaLatDeg; + + bool useFlag = false; + + if (possible[lonIdx][latIdx]) { + Vector3 gridPosition = EcefModel::geodeticToEcef(latitudeDeg, + longitudeDeg, + 0.0); + + for (int ulsIdx = 0; (ulsIdx < ulsList->getSize()) && (!useFlag); + ulsIdx++) { + ULSClass *uls = (*ulsList)[ulsIdx]; + Vector3 losPath = uls->getRxPosition() - gridPosition; + double pathDistKMSQ = losPath.dot(losPath); + if (pathDistKMSQ < maxDistKMSQ) { + useFlag = true; + } + } + } + + if (useFlag) { + if (initFlag || (lonIdx < minLonIdx)) { + minLonIdx = lonIdx; + } + if (initFlag || (lonIdx > maxLonIdx)) { + maxLonIdx = lonIdx; + } + if (initFlag || (latIdx < minLatIdx)) { + minLatIdx = latIdx; + } + if (initFlag || (latIdx > maxLatIdx)) { + maxLatIdx = latIdx; + } + initFlag = false; + } else { + pop[lonIdx][latIdx] = 0.0; + propEnv[lonIdx][latIdx] = 'X'; + region[lonIdx][latIdx] = -1; + } + } + + if ((lonIdx % 100) == 99) { + std::cout << "ADJUSTED " << (double)(lonIdx + 1.0) * 100 / numLon << " %" + << std::endl; + std::cout << std::flush; + } + } + + int newNumLon = maxLonIdx - minLonIdx + 1; + int newNumLat = maxLatIdx - minLatIdx + 1; + + double newMinLon = minLonDeg + minLonIdx * deltaLonDeg; + double newMinLat = minLatDeg + minLatIdx * deltaLatDeg; + + std::vector urbanArea(numRegion); + std::vector suburbanArea(numRegion); + std::vector ruralArea(numRegion); + std::vector barrenArea(numRegion); + + for (regionIdx = 0; regionIdx < numRegion; regionIdx++) { + urbanPop[regionIdx] = 0.0; + suburbanPop[regionIdx] = 0.0; + ruralPop[regionIdx] = 0.0; + barrenPop[regionIdx] = 0.0; + + urbanArea[regionIdx] = 0.0; + suburbanArea[regionIdx] = 0.0; + ruralArea[regionIdx] = 0.0; + barrenArea[regionIdx] = 0.0; + } + + double totalArea = 0.0; + double totalPop = 0.0; + /**************************************************************************************/ + /**** Allocate matrix ****/ + /**************************************************************************************/ + double **newPop = (double **)malloc(newNumLon * sizeof(double *)); + char **newPropEnv = (char **)malloc(newNumLon * sizeof(char *)); + int **newRegion = (int **)malloc(newNumLon * sizeof(int *)); + for (lonIdx = 0; lonIdx < newNumLon; lonIdx++) { + newPop[lonIdx] = (double *)malloc(newNumLat * sizeof(double)); + newPropEnv[lonIdx] = (char *)malloc(newNumLat * sizeof(char)); + newRegion[lonIdx] = (int *)malloc(newNumLat * sizeof(int)); + for (latIdx = 0; latIdx < newNumLat; latIdx++) { + double latitudeDeg = minLatDeg + latIdx * deltaLatDeg; + double area = CConst::earthRadius * CConst::earthRadius * + cos(latitudeDeg * M_PI / 180.0) * deltaLonDeg * deltaLatDeg * + (M_PI / 180.0) * (M_PI / 180.0); + + newPop[lonIdx][latIdx] = pop[minLonIdx + lonIdx][minLatIdx + latIdx]; + newPropEnv[lonIdx][latIdx] = + propEnv[minLonIdx + lonIdx][minLatIdx + latIdx]; + newRegion[lonIdx][latIdx] = region[minLonIdx + lonIdx][minLatIdx + latIdx]; + + regionIdx = newRegion[lonIdx][latIdx]; + switch (newPropEnv[lonIdx][latIdx]) { + case 'U': + urbanPop[regionIdx] += newPop[lonIdx][latIdx]; + urbanArea[regionIdx] += area; + totalArea += area; + break; + case 'S': + suburbanPop[regionIdx] += newPop[lonIdx][latIdx]; + suburbanArea[regionIdx] += area; + totalArea += area; + break; + case 'R': + ruralPop[regionIdx] += newPop[lonIdx][latIdx]; + ruralArea[regionIdx] += area; + totalArea += area; + break; + case 'B': + barrenPop[regionIdx] += newPop[lonIdx][latIdx]; + barrenArea[regionIdx] += area; + totalArea += area; + break; + } + totalPop += newPop[lonIdx][latIdx]; + } + } + /**************************************************************************************/ + + if (pop) { + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + free(pop[lonIdx]); + } + free(pop); + } + + if (propEnv) { + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + free(propEnv[lonIdx]); + } + free(propEnv); + } + if (region) { + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + free(region[lonIdx]); + } + free(region); + } + + if (possible) { + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + free(possible[lonIdx]); + } + free(possible); + } + + pop = newPop; + propEnv = newPropEnv; + region = newRegion; + minLonDeg = newMinLon; + minLatDeg = newMinLat; + numLon = newNumLon; + numLat = newNumLat; + + for (regionIdx = 0; regionIdx < numRegion; regionIdx++) { + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " ADJUSTED URBAN POPULATION: " << urbanPop[regionIdx] + << " " << 100 * urbanPop[regionIdx] / totalPop << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " ADJUSTED SUBURBAN POPULATION: " << suburbanPop[regionIdx] + << " " << 100 * suburbanPop[regionIdx] / totalPop << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " ADJUSTED RURAL POPULATION: " << ruralPop[regionIdx] + << " " << 100 * ruralPop[regionIdx] / totalPop << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " ADJUSTED BARREN POPULATION: " << barrenPop[regionIdx] + << " " << 100 * barrenPop[regionIdx] / totalPop << " %"; + + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " ADJUSTED URBAN AREA: " << urbanArea[regionIdx] << " " + << 100 * urbanArea[regionIdx] / totalArea << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " ADJUSTED SUBURBAN AREA: " << suburbanArea[regionIdx] << " " + << 100 * suburbanArea[regionIdx] / totalArea << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " ADJUSTED RURAL AREA: " << ruralArea[regionIdx] << " " + << 100 * ruralArea[regionIdx] / totalArea << " %"; + LOGGER_INFO(logger) << "REGION " << regionNameList[regionIdx] + << " ADJUSTED BARREN AREA: " << barrenArea[regionIdx] << " " + << 100 * barrenArea[regionIdx] / totalArea << " %"; + } + LOGGER_INFO(logger) << "TOTAL_ADJUSTED_POPULATION: " << totalPop; + LOGGER_INFO(logger) << "TOTAL_ADJUSTED_AREA: " << totalArea; + + t1 = time(NULL); + tstr = strdup(ctime(&t1)); + strtok(tstr, "\n"); + std::cout << tstr << " : DONE ADJUSTING SIMULATION REGION." << std::endl; + free(tstr); + std::cout << std::flush; + + return (totalPop); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::makeCDF ****/ +/******************************************************************************************/ +void PopGridClass::makeCDF() +{ + int lonIdx, latIdx; + + if (isCumulative) { + throw("ERROR in PopGridClass::makeCDF(), pop grid already cumulative\n"); + } + + double sum = 0.0; + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + for (latIdx = 0; latIdx < numLat; latIdx++) { + sum += pop[lonIdx][latIdx]; + pop[lonIdx][latIdx] = sum; + } + } + + isCumulative = true; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::check ****/ +/******************************************************************************************/ +void PopGridClass::check(std::string s) +{ + int lonIdx, latIdx; + + for (lonIdx = 0; lonIdx < numLon; lonIdx++) { + for (latIdx = 0; latIdx < numLat; latIdx++) { + if ((propEnv[lonIdx][latIdx] == 'X') && (pop[lonIdx][latIdx] != 0.0)) { + std::cout << "CHECK GRID: " << s << " " << lonIdx << " " << latIdx + << " POP = " << pop[lonIdx][latIdx] << std::endl; + } + } + } + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::findDeg() ****/ +/**** Find given longitude/latitude coordinates in PopGridClass, if coordinates are ****/ +/**** outside the grid, set lonIdx = -1, latIdx = -1, provEnv = (char) NULL ****/ +/******************************************************************************************/ +void PopGridClass::findDeg(double longitudeDeg, + double latitudeDeg, + int &lonIdx, + int &latIdx, + char &propEnvVal, + int ®ionIdx) const +{ + if (longitudeDeg < minLonDeg) { + longitudeDeg += 360.0; + } + lonIdx = (int)floor((longitudeDeg - minLonDeg) / deltaLonDeg); + latIdx = (int)floor((latitudeDeg - minLatDeg) / deltaLatDeg); + + if ((lonIdx < 0) || (lonIdx > numLon - 1) || (latIdx < 0) || (latIdx > numLat - 1)) { + lonIdx = -1; + latIdx = -1; + propEnvVal = 0; + regionIdx = -1; + } else { + propEnvVal = propEnv[lonIdx][latIdx]; + regionIdx = region[lonIdx][latIdx]; + } + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PopGridClass::computeArea() ****/ +/**** Compute area of deltaLon x deltaLat rectangle. ****/ +/******************************************************************************************/ +double PopGridClass::computeArea(int /* lonIdx */, int latIdx) const +{ + double latitudeDeg = minLatDeg + (latIdx + 0.5) * deltaLatDeg; + double area = CConst::earthRadius * CConst::earthRadius * cos(latitudeDeg * M_PI / 180.0) * + deltaLonDeg * deltaLatDeg * (M_PI / 180.0) * (M_PI / 180.0); + + return (area); +} +/******************************************************************************************/ diff --git a/src/afc-engine/pop_grid.h b/src/afc-engine/pop_grid.h new file mode 100644 index 0000000..782c411 --- /dev/null +++ b/src/afc-engine/pop_grid.h @@ -0,0 +1,141 @@ +/******************************************************************************************/ +/**** FILE : pop_grid.h ****/ +/******************************************************************************************/ + +#ifndef POP_GRID_H +#define POP_GRID_H + +#include +#include "list.h" +#include "cconst.h" + +class ULSClass; +class PolygonClass; + +/******************************************************************************************/ +/**** CLASS: PopGridClass ****/ +/**** Population Grid Class. Grid of LON/LAT coordinates, where LON vals are equally ****/ +/**** spaced in increments of deltaLon, and LAT values equally spaced in increments of ****/ +/**** deltaLat. For each LON/LAT coord in the grid, the matrix stores the population ****/ +/**** over the region defined by LON +/- deltaLon/2, LAT +/- deltaLat/2. The area of ****/ +/**** this region is taken to be: ****/ +/**** R*cos(LAT)*deltaLon*deltaLat ****/ +/**** Note that regions at different latitudes have different areas. ****/ +/**** The population over the region is the polulation density times the area. ****/ +/**** An effort is made to be explicit with angle units by using Rad or Deg in ****/ +/**** variable names. ****/ +/**** Non-rectangular regions can be converted to rectangular regions by padding with ****/ +/**** zeros. ****/ +/******************************************************************************************/ +class PopGridClass +{ + public: + PopGridClass(double densityThrUrbanVal, + double densityThrSuburbanVal, + double densityThrRuralVal); + PopGridClass(std::string worldPopulationFile, + const std::vector ®ionPolygonList, + double regionPolygonResolution, + double densityThrUrbanVal, + double densityThrSuburbanVal, + double densityThrRuralVal, + double minLat, + double minLon, + double maxLat, + double maxLon); + PopGridClass(const PopGridClass &obj); + ~PopGridClass(); + void readData(std::string populationDensityFile, + const std::vector ®ionNameList, + const std::vector ®ionIDList, + int numLonVal, + double deltaLonDeg, + double minLonDeg, + int numLatVal, + double deltaLatDeg, + double minLatDeg); + void setDimensions(int numLonVal, + double deltaLonRad, + double minLonRad, + int numLatVal, + double deltaLatRad, + double minLatRad); + void scale(std::vector urbanPopVal, + std::vector suburbanPopVal, + std::vector ruralPopVal, + std::vector barrenPopVal); + int adjustRegion(double centerLongitudeDeg, + double centerLatitudeDeg, + double radius); + double adjustRegion(ListClass *ulsList, double maxRadius); + void makeCDF(); + void check(std::string s); + void writeDensity(std::string filename, bool dumpPopGrid = false); + void findDeg(double longitudeDeg, + double latitudeDeg, + int &lonIdx, + int &latIdx, + char &propEnvVal, + int ®ionIdx) const; + double computeArea(int lonIdx, int latIdx) const; + void setPop(int lonIdx, int latIdx, double popVal); + void setPropEnv(int lonIdx, int latIdx, char propEnvVal); + char getPropEnv(int lonIdx, int latIdx) const; + double getPropEnvPop(CConst::PropEnvEnum propEnv, int regionIdx) const; + double getPop(int lonIdx, int latIdx) const; + double getPopFromCDF(int lonIdx, int latIdx) const; + double getProbFromCDF(int lonIdx, int latIdx) const; + void getLonLatDeg(int lonIdx, + int latIdx, + double &longitudeDeg, + double &latitudeDeg); + int getNumLon(); + int getNumLat(); + double getDensityThrUrban(); + double getDensityThrSuburban(); + double getDensityThrRural(); + std::string getRegionName(int regionIdx) + { + return (regionNameList[regionIdx]); + } + double getMinLonDeg() + { + return (minLonDeg); + } + double getMinLatDeg() + { + return (minLatDeg); + } + double getMaxLonDeg() + { + return (minLonDeg + numLon * deltaLonDeg); + } + double getMaxLatDeg() + { + return (minLatDeg + numLat * deltaLatDeg); + } + + private: + int numRegion; + std::vector regionNameList; + // std::vector regionIDList; + double densityThrUrban, densityThrSuburban, densityThrRural; + + double minLonDeg; + double minLatDeg; + double deltaLonDeg; + double deltaLatDeg; + + int numLon, numLat; + double **pop; + char **propEnv; + int **region; + std::vector urbanPop; + std::vector suburbanPop; + std::vector ruralPop; + std::vector barrenPop; + bool isCumulative; +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/prtable.cpp b/src/afc-engine/prtable.cpp new file mode 100644 index 0000000..6fa337b --- /dev/null +++ b/src/afc-engine/prtable.cpp @@ -0,0 +1,271 @@ +/******************************************************************************************/ +/**** FILE : prtable.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "prtable.h" +#include "global_fn.h" + +/******************************************************************************************/ +/**** CONSTRUCTOR: PRTABLEClass::PRTABLEClass() ****/ +/******************************************************************************************/ +PRTABLEClass::PRTABLEClass() +{ + tableFile = ""; + prTable = (double **)NULL; + numQ = -1; + numOneOverKs = -1; + oneOverKsValList = (double *)NULL; + QValList = (double *)NULL; +}; + +PRTABLEClass::PRTABLEClass(std::string tableFileVal) : tableFile(tableFileVal) +{ + readTable(); +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** DESTRUCTOR: PRTABLEClass::~PRTABLEClass() ****/ +/******************************************************************************************/ +PRTABLEClass::~PRTABLEClass() +{ + free(QValList); + free(oneOverKsValList); + for (int qIdx = 0; qIdx < numQ; ++qIdx) { + free(prTable[qIdx]); + } + free(prTable); +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PRTABLEClass::readTable() ****/ +/******************************************************************************************/ +void PRTABLEClass::readTable() +{ + string line; + vector headerList; + vector> datastore; + // data structure + std::ostringstream errStr; + + ifstream file(tableFile); + if (!file.is_open()) { + errStr << std::string("ERROR: Unable to open Passive Repeater Table File \"") + + tableFile + std::string("\"\n"); + throw std::runtime_error(errStr.str()); + } + int linenum = 0; + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + bool foundLabelLine = false; + int kIdx = -1; + int qIdx, fieldIdx; + + while (getline(file, line)) { + linenum++; + std::vector fieldList = splitCSV(line); + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + int fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if ((lineType == unknownLineType) && (!foundLabelLine)) { + lineType = labelLineType; + foundLabelLine = 1; + } + if ((lineType == unknownLineType) && (foundLabelLine)) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + std::string field; + switch (lineType) { + case labelLineType: { + std::vector sizeStrList = split(fieldList[0], ':'); + if (sizeStrList.size() != 2) { + errStr << std::string("ERROR: Passive Repeater Table File ") + << tableFile << ":" << linenum + << " Ivalid table size " << fieldList[0] + << std::endl; + throw std::runtime_error(errStr.str()); + } + numQ = std::stoi(sizeStrList[0]); + numOneOverKs = std::stoi(sizeStrList[1]); + } + QValList = (double *)malloc(numQ * sizeof(double)); + oneOverKsValList = (double *)malloc(numOneOverKs * sizeof(double)); + prTable = (double **)malloc(numQ * sizeof(double *)); + for (qIdx = 0; qIdx < numQ; ++qIdx) { + prTable[qIdx] = (double *)malloc(numOneOverKs * + sizeof(double)); + } + if ((int)fieldList.size() != numQ + 1) { + errStr << std::string("ERROR: Passive Repeater Table File ") + << tableFile << ":" << linenum << " INVALID DATA\n"; + throw std::runtime_error(errStr.str()); + } + + for (fieldIdx = 1; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + + QValList[fieldIdx - 1] = std::stod(field); + + // std::cout << "FIELD: \"" << field << "\"" << std::endl; + } + kIdx = 0; + + break; + case dataLineType: + if (kIdx > numOneOverKs - 1) { + errStr << std::string("ERROR: Passive Repeater Table File ") + << tableFile << ":" << linenum << " INVALID DATA\n"; + throw std::runtime_error(errStr.str()); + } + + oneOverKsValList[kIdx] = std::stod(fieldList[0]); + + if (((int)fieldList.size()) != numQ + 1) { + errStr << std::string("ERROR: Passive Repeater Table File ") + << tableFile << ":" << linenum << " INVALID DATA\n"; + throw std::runtime_error(errStr.str()); + } + + for (fieldIdx = 1; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + + prTable[fieldIdx - 1][kIdx] = std::stod(field); + + // std::cout << "FIELD: \"" << field << "\"" << std::endl; + } + kIdx++; + break; + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + CORE_DUMP; + break; + } + } + if (kIdx != numOneOverKs) { + errStr << std::string("ERROR: Passive Repeater Table File ") << tableFile << ":" + << " Read " << kIdx << " lines of data, expecting " << numOneOverKs + << std::endl; + throw std::runtime_error(errStr.str()); + } + + return; +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PRTABLEClass::computePRTABLE() ****/ +/******************************************************************************************/ +double PRTABLEClass::computePRTABLE(double Q, double oneOverKs) +{ + double qIdxDbl = getIdx(Q, QValList, numQ); + double kIdxDbl = getIdx(oneOverKs, oneOverKsValList, numOneOverKs); + + if (qIdxDbl < 0.0) { + qIdxDbl = 0.0; + } else if (qIdxDbl > numQ - 1) { + qIdxDbl = (double)numQ - 1; + } + + if (kIdxDbl < 0.0) { + kIdxDbl = 0.0; + } else if (kIdxDbl > numOneOverKs - 1) { + kIdxDbl = (double)numOneOverKs - 1; + } + + int qIdx0 = (int)floor(qIdxDbl); + if (qIdx0 == numQ - 1) { + qIdx0 = numQ - 2; + } + + int kIdx0 = (int)floor(kIdxDbl); + if (kIdx0 == numOneOverKs - 1) { + kIdx0 = numOneOverKs - 2; + } + + double F00 = prTable[qIdx0][kIdx0]; + double F01 = prTable[qIdx0][kIdx0 + 1]; + double F10 = prTable[qIdx0 + 1][kIdx0]; + double F11 = prTable[qIdx0 + 1][kIdx0 + 1]; + + double tableVal = F00 * (qIdx0 + 1 - qIdxDbl) * (kIdx0 + 1 - kIdxDbl) + + F01 * (qIdx0 + 1 - qIdxDbl) * (kIdxDbl - kIdx0) + + F10 * (qIdxDbl - qIdx0) * (kIdx0 + 1 - kIdxDbl) + + F11 * (qIdxDbl - qIdx0) * (kIdxDbl - kIdx0); + + return (tableVal); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PRTABLEClass::getIdx() ****/ +/******************************************************************************************/ +double PRTABLEClass::getIdx(double val, double *valList, int numVal) +{ + int i0 = 0; + int i1 = numVal - 1; + + double v0 = valList[i0]; + double v1 = valList[i1]; + + if (val <= v0) { + return (-1.0); + } + if (val >= v1) { + return ((double)numVal - 1.0); + } + + while (i1 > i0 + 1) { + int im = (i0 + i1) / 2; + double vm = valList[im]; + if (val >= vm) { + i0 = im; + v0 = vm; + } else { + i1 = im; + v1 = vm; + } + } + + double idxDbl = i0 + (val - v0) / (v1 - v0); + + return (idxDbl); +} +/******************************************************************************************/ diff --git a/src/afc-engine/prtable.h b/src/afc-engine/prtable.h new file mode 100644 index 0000000..34a1aa1 --- /dev/null +++ b/src/afc-engine/prtable.h @@ -0,0 +1,36 @@ +/******************************************************************************************/ +/**** FILE : prtable.h ****/ +/******************************************************************************************/ + +#ifndef PRTABLE_H +#define PRTABLE_H + +#include + +using namespace std; + +/******************************************************************************************/ +/**** CLASS: PRTABLEClass ****/ +/******************************************************************************************/ +class PRTABLEClass +{ + public: + PRTABLEClass(); + PRTABLEClass(std::string tableFile); + ~PRTABLEClass(); + + double computePRTABLE(double Q, double oneOverKs); + double getIdx(double val, double *valList, int numVal); + + private: + void readTable(); + + std::string tableFile; + double **prTable; + int numOneOverKs, numQ; + double *oneOverKsValList; + double *QValList; +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/readITUFiles.cpp b/src/afc-engine/readITUFiles.cpp new file mode 100644 index 0000000..c7257d9 --- /dev/null +++ b/src/afc-engine/readITUFiles.cpp @@ -0,0 +1,154 @@ +#include "readITUFiles.hpp" + +#include +#include +#include +#include +#include +#include + +ITUDataClass::ITUDataClass(std::string t_radioClimatePath, std::string t_surfRefracPath) +{ + readRCFile(t_radioClimatePath); + readSRFile(t_surfRefracPath); +} + +ITUDataClass::~ITUDataClass() +{ + int latIdx; + + for (latIdx = 0; latIdx < RCNumLat; ++latIdx) { + free(RCData[latIdx]); + } + free(RCData); + + for (latIdx = 0; latIdx < SRNumLat; ++latIdx) { + free(SRData[latIdx]); + } + free(SRData); +} + +void ITUDataClass::readRCFile(std::string RCFile) +{ + std::ifstream file(RCFile); + + int latIdx, lonIdx; + + RCData = (int **)malloc(RCNumLat * sizeof(int *)); + for (latIdx = 0; latIdx < RCNumLat; ++latIdx) { + RCData[latIdx] = (int *)malloc(RCNumLon * sizeof(int)); + } + std::string line; + + latIdx = 0; + while (std::getline(file, line)) { + std::istringstream lineBuffer(line); + for (lonIdx = 0; lonIdx < RCNumLon; ++lonIdx) + lineBuffer >> RCData[latIdx][lonIdx]; + + latIdx++; + } + if (latIdx != RCNumLat) { + throw std::length_error("ERROR: Incorrect number of rows in " + RCFile); + } + + return; +} + +void ITUDataClass::readSRFile(std::string SRFile) +{ + std::ifstream file(SRFile); + + int latIdx, lonIdx; + + SRData = (double **)malloc(SRNumLat * sizeof(double *)); + for (latIdx = 0; latIdx < SRNumLat; ++latIdx) { + SRData[latIdx] = (double *)malloc(SRNumLon * sizeof(double)); + } + std::string line; + + latIdx = 0; + while (std::getline(file, line)) { + std::istringstream lineBuffer(line); + for (lonIdx = 0; lonIdx < SRNumLon; lonIdx++) + lineBuffer >> SRData[latIdx][lonIdx]; + + latIdx++; + } + if (latIdx != SRNumLat) { + throw std::length_error("ERROR: Incorrect number of rows in " + SRFile); + } + + return; +} + +int ITUDataClass::getRadioClimateValue(double latDeg, double lonDeg) +{ + if (latDeg < -90 || latDeg > 90) { + throw std::range_error("Latitude outside [-90.0,90.0]!"); + } else if (latDeg < -89.75) { + latDeg = -89.75; + } + + if (lonDeg < -180 || lonDeg > 360) { + throw std::range_error("Longitude outside [-180.0,360.0]!"); + } + + int latIdx = std::floor((90.0 - latDeg) * 2.0); + + int lonIdx = std::floor((lonDeg + 180.0) * 2.0); + + if (lonIdx >= 720) { + lonIdx -= 720; + } + + int radio_climate_value = RCData[latIdx][lonIdx]; + + return radio_climate_value; +} + +double ITUDataClass::getSurfaceRefractivityValue(double latDeg, double lonDeg) +{ + if (latDeg < -90 || latDeg > 90) { + throw std::range_error("Latitude outside [-90.0,90.0]!"); + } + + if (lonDeg < -180 || lonDeg > 360) { + throw std::range_error("Longitude outside [-180.0,360.0]!"); + } + + if (lonDeg < 0.0) { + lonDeg += 360.0; + } + + double latIdxDbl = (90.0 - latDeg) / 1.5; + + int latIdx0 = (int)std::floor(latIdxDbl); + + if (latIdx0 == 120) { + latIdx0 = 119; + } + + double lonIdxDbl = (lonDeg) / 1.5; + + int lonIdx0 = (int)std::floor(lonIdxDbl); + + if (lonIdx0 == 240) { + lonIdx0 = 239; + } + + int latIdx1 = latIdx0 + 1; + int lonIdx1 = lonIdx0 + 1; + + double val00 = SRData[latIdx0][lonIdx0]; + double val01 = SRData[latIdx0][lonIdx1]; + double val10 = SRData[latIdx1][lonIdx0]; + double val11 = SRData[latIdx1][lonIdx1]; + + double surf_refract = (val00 * (latIdx1 - latIdxDbl) * (lonIdx1 - lonIdxDbl) + + val01 * (latIdx1 - latIdxDbl) * (lonIdxDbl - lonIdx0) + + val10 * (latIdxDbl - latIdx0) * (lonIdx1 - lonIdxDbl) + + val11 * (latIdxDbl - latIdx0) * (lonIdxDbl - lonIdx0)); + + return surf_refract; +} diff --git a/src/afc-engine/readITUFiles.hpp b/src/afc-engine/readITUFiles.hpp new file mode 100644 index 0000000..74b60fe --- /dev/null +++ b/src/afc-engine/readITUFiles.hpp @@ -0,0 +1,29 @@ +#ifndef INCLUDE_IPDR_UTIL_READITUFILES_HPP_ +#define INCLUDE_IPDR_UTIL_READITUFILES_HPP_ + +#include +#include +#include + +class ITUDataClass +{ + public: + ITUDataClass(std::string t_radioClimatePath, std::string t_surfRefracPath); + ~ITUDataClass(); + + int getRadioClimateValue(double latDeg, double lonDeg); + double getSurfaceRefractivityValue(double latDeg, double lonDeg); + + private: + const int RCNumLat = 360; + const int RCNumLon = 720; + void readRCFile(std::string RCFile); + int **RCData; + + const int SRNumLat = 121; + const int SRNumLon = 241; + void readSRFile(std::string SRFile); + double **SRData; +}; + +#endif // INCLUDE_IPDR_UTIL_READITUFILES_HPP_ diff --git a/src/afc-engine/spline.cpp b/src/afc-engine/spline.cpp new file mode 100644 index 0000000..88bf5ec --- /dev/null +++ b/src/afc-engine/spline.cpp @@ -0,0 +1,393 @@ +/******************************************************************************************/ +/**** PROGRAM: spline.cpp ****/ +/******************************************************************************************/ +#include +#include +#include +#include +#include + +#include "global_defines.h" +#include "list.h" +#include "spline.h" + +/******************************************************************************************/ +/**** CONSTRUCTOR: SplineClass::SplineClass ****/ +/******************************************************************************************/ +SplineClass::SplineClass(ListClass *dataList) +{ + int ptIdx; + int n = dataList->getSize(); + + double *origDataX = DVECTOR(n); + double *origDataY = DVECTOR(n); + + for (ptIdx = 0; ptIdx <= dataList->getSize() - 1; ptIdx++) { + origDataX[ptIdx] = (*dataList)[ptIdx].x(); + origDataY[ptIdx] = (*dataList)[ptIdx].y(); + } + + a = DVECTOR(n + 2); + b = DVECTOR(n + 2); + c = DVECTOR(n + 2); + d = DVECTOR(n + 2); + x = DVECTOR(n + 2); + + makesplinecoeffs(1, n, origDataX - 1, origDataY - 1); + + free(origDataX); + free(origDataY); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: SplineClass::SplineClass ****/ +/******************************************************************************************/ +SplineClass::SplineClass(std::vector dataList) +{ + int ptIdx; + int n = (int)dataList.size(); + + double *origDataX = DVECTOR(n); + double *origDataY = DVECTOR(n); + + for (ptIdx = 0; ptIdx < n; ptIdx++) { + origDataX[ptIdx] = dataList.at(ptIdx).x(); + origDataY[ptIdx] = dataList.at(ptIdx).y(); + } + + a = DVECTOR(n + 2); + b = DVECTOR(n + 2); + c = DVECTOR(n + 2); + d = DVECTOR(n + 2); + x = DVECTOR(n + 2); + + makesplinecoeffs(1, n, origDataX - 1, origDataY - 1); + + free(origDataX); + free(origDataY); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: SplineClass::~SplineClass ****/ +/******************************************************************************************/ +SplineClass::~SplineClass() +{ + free(a); + free(b); + free(c); + free(d); + free(x); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: SplineClass::makesplinecoeffs ****/ +/**** Return value: 1 if successfull, 0 if not successful ****/ +/******************************************************************************************/ +int SplineClass::makesplinecoeffs(int p_n1, int p_n2, double *xs, double *ys) +{ + int i, m1, m2, cont; + double *y; + double e, f, f2, g, h, p, s; + std::ostringstream errStr; + + /* the dimensioning of the arrays is from n1-1 to n2+1 */ + double *r, *r1, *r2, *t, *t1, *u, *v, *dy; + + n1 = p_n1; + n2 = p_n2; + + if (n1 <= 0) { + errStr << std::string("ERROR in routine makesplinecoeffs()") << std::endl + << "first data point index must be > 0" << std::endl + << "n1 = " << n1 << std::endl; + throw std::runtime_error(errStr.str()); + } + + if ((n1 >= n2)) { + errStr << std::string("ERROR in routine makesplinecoeffs()") << std::endl + << "n1 >= n2" << std::endl + << "n1 = " << n1 << ", n2 = " << n2 << std::endl; + throw std::runtime_error(errStr.str()); + } + + for (i = n1; i <= n2; i++) { + x[i] = xs[i]; + if ((i > n1) && (x[i] <= x[i - 1])) { + errStr << std::string("ERROR in routine makesplinecoeffs()") << std::endl + << "x var not strictly increasing between indicies = " << i - 1 + << "," << i << " (" << x[i - 1] << ", " << x[i] << std::endl; + throw std::runtime_error(errStr.str()); + } + } + + y = DVECTOR(n2 + 1); + r = DVECTOR(n2 + 2); + r1 = DVECTOR(n2 + 2); + r2 = DVECTOR(n2 + 2); + t = DVECTOR(n2 + 2); + t1 = DVECTOR(n2 + 2); + u = DVECTOR(n2 + 2); + v = DVECTOR(n2 + 2); + dy = DVECTOR(n2 + 2); + + for (i = n1; i <= n2; i++) { + y[i] = ys[i]; + } + + s = 0.0; + + for (i = n1 - 1; i <= n2 + 1; i++) { + dy[i] = 1.0; + } + + m1 = n1 - 1; + m2 = n2 + 1; + r[m1] = 0.0; + r[n1] = 0.0; + r1[n2] = 0.0; + r2[n2] = 0.0; + r2[m2] = 0.0; + u[m1] = 0.0; + u[n1] = 0.0; + u[n2] = 0.0; + u[m2] = 0.0; + p = 0.0; + m1 = n1 + 1; + m2 = n2 - 1; + h = x[m1] - x[n1]; + f = (y[m1] - y[n1]) / h; + + g = h; + for (i = m1; i <= m2; i++) { + g = h; + h = x[i + 1] - x[i]; + e = f; + f = (y[i + 1] - y[i]) / h; + a[i] = f - e; + t[i] = 2.0 * (g + h) / 3.0; + t1[i] = h / 3.0; + r2[i] = dy[i - 1] / g; + r[i] = dy[i + 1] / h; + r1[i] = -dy[i] / g - dy[i] / h; + } + + for (i = m1; i <= m2; i++) { + b[i] = r[i] * r[i] + r1[i] * r1[i] + r2[i] * r2[i]; + c[i] = r[i] * r1[i + 1] + r1[i] * r2[i + 1]; + d[i] = r[i] * r2[i + 2]; + } + + f2 = -s; + + cont = 1; + do { + for (i = m1; i <= m2; i++) { + r1[i - 1] = f * r[i - 1]; + r2[i - 2] = g * r[i - 2]; + r[i] = 1.0 / (p * b[i] + t[i] - f * r1[i - 1] - g * r2[i - 2]); + u[i] = a[i] - r1[i - 1] * u[i - 1] - r2[i - 2] * u[i - 2]; + f = p * c[i] + t1[i] - h * r1[i - 1]; + g = h; + h = d[i] * p; + } + + for (i = m2; i >= m1; i--) { + u[i] = r[i] * u[i] - r1[i] * u[i + 1] - r2[i] * u[i + 2]; + } + + e = 0.0; + h = 0.0; + for (i = n1; i <= m2; i++) { + g = h; + h = (u[i + 1] - u[i]) / (x[i + 1] - x[i]); + v[i] = (h - g) * dy[i] * dy[i]; + e = e + v[i] * (h - g); + } + + g = -h * dy[n2] * dy[n2]; + v[n2] = g; + e = e - g * h; + g = f2; + f2 = e * p * p; + if ((f2 >= s) || (f2 <= g)) { + cont = 0; + } else { + f = 0.0; + h = (v[m1] - v[n1]) / (x[m1] - x[n1]); + for (i = m1; i <= m2; i++) { + g = h; + h = (v[i + 1] - v[i]) / (x[i + 1] - x[i]); + g = h - g - r1[i - 1] * r[i - 1] - r2[i - 2] * r[i - 2]; + f = f + g * r[i] * g; + r[i] = g; + } + + h = e - p * f; + + if (h > 0.0) { + p = p + (s - f2) / ((sqrt(s / e) + p) * h); + } else { + cont = 0; + } + } + } while (cont); + + for (i = n1; i <= n2; i++) { + a[i] = y[i] - p * v[i]; + c[i] = u[i]; + } + + for (i = n1; i <= m2; i++) { + h = x[i + 1] - x[i]; + d[i] = (c[i + 1] - c[i]) / (3.0 * h); + b[i] = (a[i + 1] - a[i]) / h - (h * d[i] + c[i]) * h; + } + + free(y); + free(r); + free(r1); + free(r2); + free(t); + free(t1); + free(u); + free(v); + free(dy); + + return (1); +} +/******************************************************************************************/ +/**** FUNCTION: SplineClass::splineval ****/ +/**** this evaluates the cubic spline as defined by the last call to makesplinecoeffs. ****/ +/**** ****/ +/**** xpoint = desired function argument ****/ +/**** ****/ +/******************************************************************************************/ +double SplineClass::splineval(double xpoint) const +{ + int s = -1; + double h, z; + + if ((xpoint >= x[n1]) && (xpoint <= x[n2])) { + s = spline_getintindex(xpoint); + } else if (xpoint > x[n2]) { + s = n2 - 1; + } else if (xpoint < x[n1]) { + s = n1; + } else { + fprintf(stdout, "ERROR in routine splineval()\n"); + fprintf(stdout, "input x out of range in spline evaluation routine.\n"); + fprintf(stdout, "input x = %5.3f\n", xpoint); + fprintf(stdout, "first, last in array = %5.3f, %5.3f\n", x[n1], x[n2]); + fprintf(stdout, "\n"); + CORE_DUMP; + } + + h = xpoint - x[s]; + z = ((d[s] * h + c[s]) * h + b[s]) * h + a[s]; + + return (z); +} +/******************************************************************************************/ +/**** FUNCTION: SplineClass::splineDerivativeVal ****/ +/**** Evaluated the first derivative of the spline. ****/ +/**** ****/ +/**** xpoint = desired function argument ****/ +/**** ****/ +/******************************************************************************************/ +double SplineClass::splineDerivativeVal(double xpoint) const +{ + int s = -1; + double h, z; + + if ((xpoint >= x[n1]) && (xpoint <= x[n2])) { + s = spline_getintindex(xpoint); + } else if (xpoint > x[n2]) { + s = n2 - 1; + } else if (xpoint < x[n1]) { + s = n1; + } else { + fprintf(stdout, "ERROR in routine splineval()\n"); + fprintf(stdout, "input x out of range in spline evaluation routine.\n"); + fprintf(stdout, "input x = %5.3f\n", xpoint); + fprintf(stdout, "first, last in array = %5.3f, %5.3f\n", x[n1], x[n2]); + fprintf(stdout, "\n"); + CORE_DUMP; + } + + h = xpoint - x[s]; + z = ((3 * d[s] * h + 2 * c[s]) * h + b[s]); + + return (z); +} +/******************************************************************************************/ +/**** FUNCTION: SplineClass::splineDerivative2Val ****/ +/**** Evaluate the second derivative of the spline. ****/ +/**** ****/ +/**** xpoint = desired function argument ****/ +/**** ****/ +/******************************************************************************************/ +double SplineClass::splineDerivative2Val(double xpoint) const +{ + int s = -1; + double h, z; + + if ((xpoint >= x[n1]) && (xpoint <= x[n2])) { + s = spline_getintindex(xpoint); + } else if (xpoint > x[n2]) { + s = n2 - 1; + } else if (xpoint < x[n1]) { + s = n1; + } else { + fprintf(stdout, "ERROR in routine splineval()\n"); + fprintf(stdout, "input x out of range in spline evaluation routine.\n"); + fprintf(stdout, "input x = %5.3f\n", xpoint); + fprintf(stdout, "first, last in array = %5.3f, %5.3f\n", x[n1], x[n2]); + fprintf(stdout, "\n"); + CORE_DUMP; + } + + h = xpoint - x[s]; + z = 6 * d[s] * h + 2 * c[s]; + + return (z); +} +/******************************************************************************************/ +/**** FUNCTION: SplineClass::spline_getintindex ****/ +/******************************************************************************************/ +int SplineClass::spline_getintindex(double xtest) const +{ + int lowind, upind, testind, index; + + if ((xtest < x[n1]) || (xtest > x[n2])) { + fprintf(stdout, "ERROR in routine getintindex()\n"); + fprintf(stdout, "input x out of range.\n"); + fprintf(stdout, "x =%5.3f\n", xtest); + fprintf(stdout, "range = %5.3f, %5.3f\n", x[n1], x[n2]); + fprintf(stdout, "\n"); + CORE_DUMP; + } + + lowind = n1; + upind = n2; + + do { + testind = (lowind + upind) / 2; + if (xtest > x[testind]) { + lowind = testind; + } else { + upind = testind; + } + } while (upind - lowind > 1); + + if (xtest > x[upind]) { + index = upind; + } else { + index = lowind; + } + + return (index); +} +/******************************************************************************************/ diff --git a/src/afc-engine/spline.h b/src/afc-engine/spline.h new file mode 100644 index 0000000..2f8e595 --- /dev/null +++ b/src/afc-engine/spline.h @@ -0,0 +1,35 @@ +/******************************************************************************************/ +/**** FILE: spline.h ****/ +/******************************************************************************************/ + +#ifndef SPLINE_H +#define SPLINE_H + +#include +#include "dbldbl.h" + +template +class ListClass; + +/******************************************************************************************/ +/**** CLASS: SplineClass ****/ +/******************************************************************************************/ +class SplineClass +{ + public: + SplineClass(ListClass *dataList); + SplineClass(std::vector dataList); + ~SplineClass(); + double splineval(double) const; + double splineDerivativeVal(double xpoint) const; + double splineDerivative2Val(double xpoint) const; + + private: + double *a, *b, *c, *d, *x; + int n1, n2; + int spline_getintindex(double) const; + int makesplinecoeffs(int, int, double *, double *); +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/str_type.cpp b/src/afc-engine/str_type.cpp new file mode 100644 index 0000000..4d165cf --- /dev/null +++ b/src/afc-engine/str_type.cpp @@ -0,0 +1,121 @@ +/******************************************************************************************/ +/**** PROGRAM: str_type.cpp ****/ +/******************************************************************************************/ +#include +#include + +#include "global_defines.h" + +#include "str_type.h" + +/******************************************************************************************/ +/**** FUNCTION: StrTypeClass::str_to_type ****/ +/******************************************************************************************/ +int StrTypeClass::str_to_type(const char *typestr, int &validFlag, int err) const +{ + int type = -1, i; + + i = 0; + validFlag = 0; + + if (typestr) { + while ((this[i].type_str) && (!validFlag)) { + if (strcmp(typestr, this[i].type_str) == 0) { + type = this[i].type_num; + validFlag = 1; + } else { + i++; + } + } + } + + if (!validFlag) { + type = -1; + if (err) { + CORE_DUMP; + } + } + + return (type); +} + +int StrTypeClass::str_to_type(const std::string &typestr, int &validFlag, int err) const +{ + int type = -1, i; + + i = 0; + validFlag = 0; + + if (!typestr.empty()) { + while ((this[i].type_str) && (!validFlag)) { + if (strcmp(typestr.c_str(), this[i].type_str) == 0) { + type = this[i].type_num; + validFlag = 1; + } else { + i++; + } + } + } + + if (!validFlag) { + type = -1; + if (err) { + CORE_DUMP; + } + } + + return (type); +} +/******************************************************************************************/ +/**** FUNCTION: StrTypeClass::type_to_str ****/ +/******************************************************************************************/ +const char *StrTypeClass::type_to_str(int type) const +{ + int i, found; + const char *typestr = (const char *)NULL; + + i = 0; + found = 0; + + while ((this[i].type_str) && (!found)) { + if (this[i].type_num == type) { + typestr = this[i].type_str; + found = 1; + } else { + i++; + } + } + + if (!found) { + CORE_DUMP; + } + + return (typestr); +} +/******************************************************************************************/ +/**** FUNCTION: StrTypeClass::valid ****/ +/******************************************************************************************/ +int StrTypeClass::valid(int type, int *idxPtr) const +{ + int i, found; + + i = 0; + found = 0; + if (idxPtr) { + *idxPtr = -1; + } + + while ((this[i].type_str) && (!found)) { + if (this[i].type_num == type) { + found = 1; + if (idxPtr) { + *idxPtr = i; + } + } else { + i++; + } + } + + return (found); +} +/******************************************************************************************/ diff --git a/src/afc-engine/str_type.h b/src/afc-engine/str_type.h new file mode 100644 index 0000000..bb06ab9 --- /dev/null +++ b/src/afc-engine/str_type.h @@ -0,0 +1,22 @@ +/******************************************************************************************/ +/**** FILE: str_type.h ****/ +/******************************************************************************************/ + +#ifndef STR_TYPE_H +#define STR_TYPE_H + +#include + +class StrTypeClass +{ + public: + int type_num; + const char *type_str; + + int str_to_type(const char *typestr, int &validFlag, int err = 0) const; + int str_to_type(const std::string &typestr, int &validFlag, int err = 0) const; + const char *type_to_str(int type) const; + int valid(int type, int *idxPtr = (int *)0) const; +}; + +#endif diff --git a/src/afc-engine/terrain.cpp b/src/afc-engine/terrain.cpp new file mode 100644 index 0000000..06dbf5b --- /dev/null +++ b/src/afc-engine/terrain.cpp @@ -0,0 +1,675 @@ +/******************************************************************************************/ +/**** FILE : terrain.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include +#include +#include + +#include "global_fn.h" +#include "terrain.h" +#include "cconst.h" +#include "AfcDefinitions.h" + +#include "afclogging/ErrStream.h" +#include "afclogging/Logging.h" +#include "afclogging/LoggingConfig.h" + +std::atomic_llong TerrainClass::numLidar; +std::atomic_llong TerrainClass::numCDSM; +std::atomic_llong TerrainClass::numSRTM; +std::atomic_llong TerrainClass::numGlobal; +std::atomic_llong TerrainClass::numDEP; +std::atomic_llong TerrainClass::numITM; + +namespace +{ +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "terrain") +} + +/******************************************************************************************/ +/**** FUNCTION: TerrainClass::TerrainClass() ****/ +/******************************************************************************************/ +TerrainClass::TerrainClass(std::string lidarDir, + std::string cdsmDir, + std::string srtmDir, + std::string depDir, + std::string globeDir, + double terrainMinLat, + double terrainMinLon, + double terrainMaxLat, + double terrainMaxLon, + double terrainMinLatBldg, + double terrainMinLonBldg, + double terrainMaxLatBldg, + double terrainMaxLonBldg, + int maxLidarRegionLoadVal) : + maxLidarRegionLoad(maxLidarRegionLoadVal), gdalDirectMode(false) +{ + if (!lidarDir.empty()) { + LOGGER_INFO(logger) << "Loading building+terrain data from " << lidarDir; + readLidarInfo(lidarDir); + readLidarData(terrainMinLatBldg, + terrainMinLonBldg, + terrainMaxLatBldg, + terrainMaxLonBldg); + minLidarLongitude = terrainMinLonBldg; + maxLidarLongitude = terrainMaxLonBldg; + minLidarLatitude = terrainMinLatBldg; + maxLidarLatitude = terrainMaxLatBldg; + } else { + minLidarLongitude = 0.0; + maxLidarLongitude = -1.0; + minLidarLatitude = 0.0; + maxLidarLatitude = -1.0; + } + + if (!cdsmDir.empty()) { + cgCdsm.reset(new CachedGdal( + cdsmDir, + "cdsm", + GdalNameMapperPattern::make_unique("{latHem:ns}{latDegCeil:02}{lonHem:ew}{" + "lonDegFloor:03}.tif", + cdsmDir))); + cgCdsm->setTransformationModifier([](GdalTransform *t) { + t->roundPpdToMultipleOf(1.); + t->setMarginsOutsideDeg(1.); + }); + } + + if (!depDir.empty()) { + cgDep.reset(new CachedGdal( + depDir, + "dep", + GdalNameMapperPattern::make_unique("USGS_1_{latHem:ns}{latDegCeil:02}{" + "lonHem:ew}{lonDegFloor:03}.tif", + depDir))); + cgDep->setTransformationModifier([](GdalTransform *t) { + t->roundPpdToMultipleOf(1.); + t->setMarginsOutsideDeg(1.); + }); + } + + // STRM data is always loaded as fallback + cgSrtm.reset(new CachedGdal(srtmDir, + "srtm", + GdalNameMapperPattern::make_unique( + "{latHem:NS}{latDegFloor:02}{lonHem:EW}{" + "lonDegFloor:03}.hgt"))); + cgSrtm->setTransformationModifier([](GdalTransform *t) { + t->roundPpdToMultipleOf(0.5); + t->setMarginsOutsideDeg(1.); + }); + + // GLOBE data is always loaded as final fallback + cgGlobe.reset( + new CachedGdal(globeDir, + "globe", + GdalNameMapperDirect::make_unique("*.bil", globeDir))); + cgGlobe->setNoData(0); + + numLidar = (long long)0; + numCDSM = (long long)0; + numSRTM = (long long)0; + numGlobal = (long long)0; + numITM = (long long)0; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: TerrainClass::~TerrainClass() ****/ +/******************************************************************************************/ +TerrainClass::~TerrainClass() +{ + while (activeLidarRegionList.size()) { + int deleteLidarRegionIdx = activeLidarRegionList.back(); + activeLidarRegionList.pop_back(); + delete lidarRegionList[deleteLidarRegionIdx].multibandRaster; + lidarRegionList[deleteLidarRegionIdx].multibandRaster = (MultibandRasterClass *) + NULL; + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: TerrainClass::getNumLidarRegion() ****/ +/******************************************************************************************/ +int TerrainClass::getNumLidarRegion() +{ + return ((int)lidarRegionList.size()); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: TerrainClass::getLidarRegion() ****/ +/******************************************************************************************/ +LidarRegionStruct &TerrainClass::getLidarRegion(int lidarRegionIdx) +{ + if (!lidarRegionList[lidarRegionIdx].multibandRaster) { + std::string file = lidarRegionList[lidarRegionIdx].topPath + "/" + + lidarRegionList[lidarRegionIdx].multibandFile; + lidarRegionList[lidarRegionIdx].multibandRaster = + new MultibandRasterClass(file, lidarRegionList[lidarRegionIdx].format); + } + return (lidarRegionList[lidarRegionIdx]); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: TerrainClass::getTerrainHeight() ****/ +/******************************************************************************************/ +void TerrainClass::getTerrainHeight(double longitudeDeg, + double latitudeDeg, + double &terrainHeight, + double &bldgHeight, + MultibandRasterClass::HeightResult &lidarHeightResult, + CConst::HeightSourceEnum &heightSource, + bool cdsmFlag) const +{ + int lidarRegionIdx = -1; + heightSource = CConst::unknownHeightSource; + + if (cdsmFlag && cgCdsm.get()) { + float ht; + if (cgCdsm->getValueAt(latitudeDeg, longitudeDeg, &ht, 1, gdalDirectMode)) { + heightSource = CConst::cdsmHeightSource; + terrainHeight = (double)ht; + numCDSM++; + } + } else if ((longitudeDeg >= minLidarLongitude) && (longitudeDeg <= maxLidarLongitude) && + (latitudeDeg >= minLidarLatitude) && (latitudeDeg <= maxLidarLatitude)) { + lidarRegionIdx = getLidarRegion(longitudeDeg, latitudeDeg); + } + + if (lidarRegionIdx != -1) { + // loadLidarRegion(lidarRegionIdx); + lidarRegionList[lidarRegionIdx].multibandRaster->getHeight(latitudeDeg, + longitudeDeg, + terrainHeight, + bldgHeight, + lidarHeightResult, + gdalDirectMode); + + switch (lidarHeightResult) { + case MultibandRasterClass::OUTSIDE_REGION: + // point outside region defined by rectangle "bounds" + // This should be impossible because lidarRegion has already been + // checked. + throw std::logic_error( + "point outside region defined by rectangle 'bounds' for " + "lat: " + + std::to_string(latitudeDeg) + + ", lon: " + std::to_string(longitudeDeg) + + " in lidarRegionIdx: " + std::to_string(lidarRegionIdx)); + break; + case MultibandRasterClass::NO_DATA: + // point inside bounds that has no data. terrainHeight set to + // nodataBE, bldgHeight undefined + heightSource = CConst::unknownHeightSource; + break; + case MultibandRasterClass::NO_BUILDING: + // point where there is no building, terrainHeight set to valid + // value, bldgHeight set to nodataBldg + case MultibandRasterClass::BUILDING: + // point where there is a building, terrainHeight and bldgHeight + // valid values + heightSource = CConst::lidarHeightSource; + numLidar++; + break; + } + } else { + lidarHeightResult = MultibandRasterClass::OUTSIDE_REGION; + bldgHeight = quietNaN; + } + + if (heightSource == CConst::unknownHeightSource && cgDep.get()) { + float ht; + if (cgDep->getValueAt(latitudeDeg, longitudeDeg, &ht, 1, gdalDirectMode)) { + heightSource = CConst::depHeightSource; + terrainHeight = (double)ht; + numDEP++; + } + } + if (heightSource == CConst::unknownHeightSource) { + qint16 ht; + if (cgSrtm->getValueAt(latitudeDeg, longitudeDeg, &ht, 1, gdalDirectMode)) { + heightSource = CConst::srtmHeightSource; + terrainHeight = (double)ht; + numSRTM++; + } + } + + if (heightSource == CConst::unknownHeightSource) { + terrainHeight = (double)cgGlobe->valueAt(latitudeDeg, + longitudeDeg, + 1, + gdalDirectMode); + heightSource = CConst::globalHeightSource; + numGlobal++; + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: TerrainClass::getGdalDirectMode() ****/ +/******************************************************************************************/ +bool TerrainClass::getGdalDirectMode() const +{ + return gdalDirectMode; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: TerrainClass::setGdalDirectMode() ****/ +/******************************************************************************************/ +bool TerrainClass::setGdalDirectMode(bool newGdalDirectMode) +{ + bool ret = gdalDirectMode; + gdalDirectMode = newGdalDirectMode; + return ret; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: TerrainClass::loadLidarRegion() ****/ +/******************************************************************************************/ +void TerrainClass::loadLidarRegion(int lidarRegionIdx) +{ + std::string command; + + if (lidarRegionList[lidarRegionIdx].multibandRaster) { + // region already loaded. + return; + } + + if (((int)activeLidarRegionList.size()) == maxLidarRegionLoad) { + // Close Lidar region before opening new one. + int deleteLidarRegionIdx = activeLidarRegionList.back(); + activeLidarRegionList.pop_back(); + delete lidarRegionList[deleteLidarRegionIdx].multibandRaster; + lidarRegionList[deleteLidarRegionIdx].multibandRaster = (MultibandRasterClass *) + NULL; + + LOGGER_WARN(logger) << "REMOVING LIDAR REGION: " << deleteLidarRegionIdx; + } + + LOGGER_DEBUG(logger) << "LOADING LIDAR REGION: " << lidarRegionIdx; + std::string file = lidarRegionList[lidarRegionIdx].topPath + "/" + + lidarRegionList[lidarRegionIdx].multibandFile; + lidarRegionList[lidarRegionIdx].multibandRaster = + new MultibandRasterClass(file, lidarRegionList[lidarRegionIdx].format); + + activeLidarRegionList.insert(activeLidarRegionList.begin(), lidarRegionIdx); + + LOGGER_DEBUG(logger) << "NUM LIDAR REGIONS LOADED = " << activeLidarRegionList.size() + << " MAX = " << maxLidarRegionLoad; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** TerrainClass::getLidarRegion() ****/ +/******************************************************************************************/ +int TerrainClass::getLidarRegion(double lonDeg, double latDeg) const +{ + int lidarRegionIdx, retval; + bool found = false; + + for (lidarRegionIdx = 0; (lidarRegionIdx < ((int)lidarRegionList.size())) && (!found); + ++lidarRegionIdx) { + const LidarRegionStruct &lidarRegion = lidarRegionList[lidarRegionIdx]; + + if (lidarRegion.multibandRaster) { + if (lidarRegion.multibandRaster->contains(lonDeg, latDeg)) { + found = true; + retval = lidarRegionIdx; + } + } + // else { + // if ( (lonDeg >= lidarRegion.minLonDeg) && (lonDeg <= + // lidarRegion.maxLonDeg) + // && (latDeg >= lidarRegion.minLatDeg) && (latDeg <= + // lidarRegion.maxLatDeg) ) { + // found = true; + // retval = lidarRegionIdx; + // } + // } + } + + if (!found) { + retval = -1; + } + + return (retval); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** TerrainClass::readLidarData() ****/ +/******************************************************************************************/ +void TerrainClass::readLidarData(double terrainMinLat, + double terrainMinLon, + double terrainMaxLat, + double terrainMaxLon) +{ + int lidarRegionIdx; + std::ostringstream errStr; + + int numRegionWithOverlap = 0; + for (lidarRegionIdx = 0; (lidarRegionIdx < ((int)lidarRegionList.size())); + ++lidarRegionIdx) { + const LidarRegionStruct &lidarRegion = lidarRegionList[lidarRegionIdx]; + + if (!((terrainMaxLon < lidarRegion.minLonDeg) || + (terrainMinLon > lidarRegion.maxLonDeg) || + (terrainMaxLat < lidarRegion.minLatDeg) || + (terrainMinLat > lidarRegion.maxLatDeg))) { + numRegionWithOverlap++; + } + } + + if (numRegionWithOverlap > maxLidarRegionLoad) { + errStr << "ERROR: Terrain region specified requires " << numRegionWithOverlap + << " LIDAR tiles which exceeds maxLidarRegionLoad = " << maxLidarRegionLoad + << std::endl; + throw std::runtime_error(errStr.str()); + } + + for (lidarRegionIdx = 0; (lidarRegionIdx < ((int)lidarRegionList.size())); + ++lidarRegionIdx) { + const LidarRegionStruct &lidarRegion = lidarRegionList[lidarRegionIdx]; + + if (!((terrainMaxLon < lidarRegion.minLonDeg) || + (terrainMinLon > lidarRegion.maxLonDeg) || + (terrainMaxLat < lidarRegion.minLatDeg) || + (terrainMinLat > lidarRegion.maxLatDeg))) { + loadLidarRegion(lidarRegionIdx); + } + } + +#if 0 + if (numRegionWithOverlap == 0) + throw std::runtime_error("Building data was requested, but none was found within the analysis area."); +#endif + LOGGER_INFO(logger) << numRegionWithOverlap << " LiDAR tiles loaded"; + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** TerrainClass::readLidarInfo() ****/ +/******************************************************************************************/ +void TerrainClass::readLidarInfo(std::string lidarDir) +{ + QDir lDir(QString::fromStdString(lidarDir)); + auto lidarCityNames = lDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (int i = 0; i < lidarCityNames.size(); i++) { + auto file = lDir.filePath(lidarCityNames[i]); + lidarCityNames[i] = file; + } + + int cityIdx; + + for (cityIdx = 0; cityIdx < lidarCityNames.size(); ++cityIdx) { + std::string topPath = lidarCityNames[cityIdx].toStdString(); + std::string infoFile = lidarCityNames[cityIdx].toStdString() + "_info.csv"; + std::string cityName; + + std::size_t posn = topPath.find_last_of("/\\"); + if (posn == std::string::npos) { + cityName = topPath; + } else { + cityName = topPath.substr(posn + 1); + } + + int linenum, fIdx; + std::string line, strval; + char *chptr; + FILE *fp = (FILE *)NULL; + std::string str; + std::string reasonIgnored; + std::ostringstream errStr; + + int multibandFieldIdx = -1; + int minLonFieldIdx = -1; + int maxLonFieldIdx = -1; + int minLatFieldIdx = -1; + int maxLatFieldIdx = -1; + int formatFieldIdx = -1; + + std::vector fieldIdxList; + std::vector fieldLabelList; + fieldIdxList.push_back(&multibandFieldIdx); + fieldLabelList.push_back("FILE"); + fieldIdxList.push_back(&minLonFieldIdx); + fieldLabelList.push_back("MIN_LON_DEG"); + fieldIdxList.push_back(&maxLonFieldIdx); + fieldLabelList.push_back("MAX_LON_DEG"); + fieldIdxList.push_back(&minLatFieldIdx); + fieldLabelList.push_back("MIN_LAT_DEG"); + fieldIdxList.push_back(&maxLatFieldIdx); + fieldLabelList.push_back("MAX_LAT_DEG"); + fieldIdxList.push_back(&formatFieldIdx); + fieldLabelList.push_back("FORMAT"); + + int fieldIdx; + + if (infoFile.empty()) { + str = std::string("ERROR: Lidar Info file specified\n"); + throw std::runtime_error(str); + } + + if (!(fp = fopen(infoFile.c_str(), "rb"))) { + str = std::string("ERROR: Unable to open Lidar Info file \"") + infoFile + + std::string("\"\n"); + throw std::runtime_error(str); + } + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + linenum = 0; + bool foundLabelLine = false; + while (fgetline(fp, line, false)) { + linenum++; + std::vector fieldList = splitCSV(line); + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if ((lineType == unknownLineType) && (!foundLabelLine)) { + lineType = labelLineType; + foundLabelLine = 1; + } + if ((lineType == unknownLineType) && (foundLabelLine)) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + bool found; + std::string field; + LidarRegionStruct lidarRegion; + lidarRegion.topPath = topPath; + lidarRegion.cityName = cityName; + lidarRegion.multibandRaster = (MultibandRasterClass *)NULL; + switch (lineType) { + case labelLineType: + for (fieldIdx = 0; fieldIdx < (int)fieldList.size(); + fieldIdx++) { + field = fieldList.at(fieldIdx); + + // std::cout << "FIELD: \"" << field << "\"" << + // std::endl; + + found = false; + for (fIdx = 0; + (fIdx < (int)fieldLabelList.size()) && + (!found); + fIdx++) { + if (field == fieldLabelList.at(fIdx)) { + *fieldIdxList.at(fIdx) = fieldIdx; + found = true; + } + } + } + + for (fIdx = 0; fIdx < (int)fieldIdxList.size(); fIdx++) { + if (*fieldIdxList.at(fIdx) == -1) { + if (fieldLabelList.at(fIdx) == "FORMAT") { + } else { + errStr << "ERROR: Invalid Lidar " + "Info file \"" + << infoFile + << "\" label line missing \"" + << fieldLabelList.at(fIdx) + << "\"" << std::endl; + throw std::runtime_error( + errStr.str()); + } + } + } + + break; + case dataLineType: + lidarRegion.multibandFile = fieldList.at(multibandFieldIdx); + + strval = fieldList.at(minLonFieldIdx); + lidarRegion.minLonDeg = std::strtod(strval.c_str(), &chptr); + + strval = fieldList.at(maxLonFieldIdx); + lidarRegion.maxLonDeg = std::strtod(strval.c_str(), &chptr); + + strval = fieldList.at(minLatFieldIdx); + lidarRegion.minLatDeg = std::strtod(strval.c_str(), &chptr); + + strval = fieldList.at(maxLatFieldIdx); + lidarRegion.maxLatDeg = std::strtod(strval.c_str(), &chptr); + + if (formatFieldIdx == -1) { + strval = "from_vector"; + } else { + strval = fieldList.at(formatFieldIdx); + } + + if (strval == "from_vector") { + lidarRegion.format = CConst::fromVectorLidarFormat; + } else if (strval == "from_raster") { + lidarRegion.format = CConst::fromRasterLidarFormat; + } else { + errStr << "lidarRegion.format not a valid value. " + "Got " + << strval << std::endl; + throw std::logic_error(errStr.str()); + } + + lidarRegionList.push_back(lidarRegion); + +#if 0 + LOGGER_DEBUG(logger) << "TOP_PATH = " << lidarRegion.topPath + << " MULTIBAND_FILE = " << lidarRegion.multibandFile + << " MIN_LON (deg) = " << lidarRegion.minLonDeg + << " MAX_LON (deg) = " << lidarRegion.maxLonDeg + << " MIN_LAT (deg) = " << lidarRegion.minLatDeg + << " MAX_LAT (deg) = " << lidarRegion.maxLatDeg; +#endif + break; + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + throw std::runtime_error("Impossible case statement " + "reached in terrain.cpp"); + break; + } + } + + if (fp) { + fclose(fp); + } + } + + return; +} +/******************************************************************************************/ + +std::vector TerrainClass::getBounds() const +{ + std::vector bounds = std::vector(); + for (LidarRegionStruct m : this->lidarRegionList) { + if (!m.multibandRaster) + continue; + QPointF topLeft(m.maxLonDeg, m.minLatDeg); + QPointF bottomRight(m.minLonDeg, m.maxLatDeg); + QRectF b(topLeft, bottomRight); + bounds.push_back(b); + } + return bounds; +} + +/** + * Register a label with a height source value + */ +void TerrainClass::setSourceName(const CConst::HeightSourceEnum &sourceVal, + const std::string &sourceName) +{ + sourceNames[sourceVal] = sourceName; +} + +/** + * Convert a HeightSourceEnum to correct string representation for the Terrain model + */ +const std::string &TerrainClass::getSourceName(const CConst::HeightSourceEnum &sourceVal) const +{ + return sourceNames.at(sourceVal); +} + +/******************************************************************************************/ +/**** TerrainClass::printStats() ****/ +/******************************************************************************************/ +void TerrainClass::printStats() +{ + long long totalNumTerrain = numLidar + numCDSM + numSRTM + numDEP + numGlobal; + + LOGGER_INFO(logger) << "TOTAL_NUM_TERRAIN = " << totalNumTerrain; + LOGGER_INFO(logger) << "NUM_LIDAR = " << numLidar << " (" + << (double)(totalNumTerrain ? numLidar * 100.0 / totalNumTerrain : 0.0) + << " %)"; + LOGGER_INFO(logger) << "NUM_CDSM = " << numCDSM << " (" + << (double)(totalNumTerrain ? numCDSM * 100.0 / totalNumTerrain : 0.0) + << " %)"; + LOGGER_INFO(logger) << "NUM_DEP = " << numDEP << " (" + << (double)(totalNumTerrain ? numDEP * 100.0 / totalNumTerrain : 0.0) + << " %)"; + LOGGER_INFO(logger) << "NUM_SRTM = " << numSRTM << " (" + << (double)(totalNumTerrain ? numSRTM * 100.0 / totalNumTerrain : 0.0) + << " %)"; + LOGGER_INFO(logger) << "NUM_GLOBAL = " << numGlobal << " (" + << (double)(totalNumTerrain ? numGlobal * 100.0 / totalNumTerrain : 0.0) + << " %)"; + LOGGER_INFO(logger) << "NUM_ITM = " << numITM; +} +/******************************************************************************************/ diff --git a/src/afc-engine/terrain.h b/src/afc-engine/terrain.h new file mode 100644 index 0000000..d6c4163 --- /dev/null +++ b/src/afc-engine/terrain.h @@ -0,0 +1,131 @@ +/******************************************************************************************/ +/**** FILE : terrain.h ****/ +/******************************************************************************************/ + +#ifndef TERRAIN_H +#define TERRAIN_H + +#include +#include "cconst.h" +#include "ratcommon/SearchPaths.h" +#include "multiband_raster.h" +#include +#include +#include +#include +#include +// Loggers +#include "afclogging/ErrStream.h" +#include "afclogging/Logging.h" +#include "afclogging/LoggingConfig.h" +#include "CachedGdal.h" + +// Use lidar files that have been pre-processed to have: +// bare-earth terrain height in band 1 +// building height in band 2 + +class MultibandRasterClass; +struct LidarRegionStruct { + std::string topPath; + CConst::LidarFormatEnum format; + std::string multibandFile; + std::string cityName; + double minLonDeg; + double maxLonDeg; + double minLatDeg; + double maxLatDeg; + MultibandRasterClass *multibandRaster; +}; + +/******************************************************************************************/ +/**** CLASS: TerrainClass ****/ +/******************************************************************************************/ +class TerrainClass +{ + public: + TerrainClass(std::string lidarDir, + std::string cdsmDir, + std::string srtmDir, + std::string depDir, + std::string globeDir, + double terrainMinLat, + double terrainMinLon, + double terrainMaxLat, + double terrainMaxLon, + double terrainMinLatBldg, + double terrainMinLonBldg, + double terrainMaxLatBldg, + double terrainMaxLonBldg, + int maxLidarRegionLoadVal); + ~TerrainClass(); + + void readLidarInfo(std::string lidarDir); + void readLidarData(double terrainMinLat, + double terrainMinLon, + double terrainMaxLat, + double terrainMaxLon); + int getLidarRegion(double lonDeg, double latDeg) const; + void loadLidarRegion(int lidarRegionIdx); + + void printStats(); + + void getTerrainHeight(double longitudeDeg, + double latitudeDeg, + double &terrainHeight, + double &bldgHeight, + MultibandRasterClass::HeightResult &lidarHeightResult, + CConst::HeightSourceEnum &heightSource, + bool cdsmFlag = false) const; + + void writeTerrainProfile(std::string filename, + double startLongitudeDeg, + double startLatitudeDeg, + double startHeightAboveTerrain, + double stopLongitudeDeg, + double stopLatitudeDeg, + double stopHeightAboveTerrain); + int getNumLidarRegion(); + LidarRegionStruct &getLidarRegion(int lidarRegionIdx); + + void setSourceName(const CConst::HeightSourceEnum &sourceVal, + const std::string &sourceName); + const std::string &getSourceName(const CConst::HeightSourceEnum &sourceVal) const; + + std::vector getBounds() const; + + bool getGdalDirectMode() const; + bool setGdalDirectMode(bool newGdalDirectMode); + + static std::atomic_llong numITM; + + private: + /**************************************************************************************/ + /**** Data ****/ + /**************************************************************************************/ + std::vector lidarRegionList; + std::vector activeLidarRegionList; + /**************************************************************************************/ + + double minLidarLongitude, maxLidarLongitude; + double minLidarLatitude, maxLidarLatitude; + int maxLidarRegionLoad; + + std::shared_ptr> cgCdsm; + std::shared_ptr> cgSrtm; + std::shared_ptr> cgDep; + std::shared_ptr> cgGlobe; + bool gdalDirectMode; + + std::map sourceNames = {}; + + static std::atomic_llong numLidar; + static std::atomic_llong numCDSM; + static std::atomic_llong numSRTM; + static std::atomic_llong numDEP; + static std::atomic_llong numGlobal; + + std::string lidarWorkingDir; +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-engine/uls.cpp b/src/afc-engine/uls.cpp new file mode 100644 index 0000000..8bccc4c --- /dev/null +++ b/src/afc-engine/uls.cpp @@ -0,0 +1,1204 @@ +/******************************************************************************************/ +/**** FILE : uls.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#include + +#include "uls.h" +#include "antenna.h" +#include "calcitu1245.h" +#include "calcitu699.h" +#include "calcitu1336_4.h" +#include "AfcManager.h" +#include "global_defines.h" +#include "local_defines.h" +#include "spline.h" +#include "list.h" +#include "UlsMeasurementAnalysis.h" +#include "EcefModel.h" + +namespace +{ +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "ULSClass") +} + +/******************************************************************************************/ +/**** FUNCTION: ULSClass::ULSClass() ****/ +/******************************************************************************************/ +ULSClass::ULSClass(AfcManager *dataSetVal, + int idVal, + int dbIdxVal, + int numPRVal, + std::string regionVal) : + dataSet(dataSetVal), id(idVal), dbIdx(dbIdxVal), numPR(numPRVal), region(regionVal) +{ + prList = new PRClass[numPR]; + + location = (char *)NULL; + ITMHeightProfile = (double *)NULL; + isLOSHeightProfile = (double *)NULL; + isLOSSurfaceFrac = quietNaN; + + dataSet = (AfcManager *)NULL; + + startFreq = quietNaN; + stopFreq = quietNaN; + noiseBandwidth = quietNaN; + pathNumber = -1; + rxAntennaNumber = -1; + rxLatitudeDeg = quietNaN; + rxLongitudeDeg = quietNaN; + rxTerrainHeight = quietNaN; + rxHeightAboveTerrain = quietNaN; + rxHeightAMSL = quietNaN; + rxGroundElevation = quietNaN; + rxHeightSource = CConst::unknownHeightSource; + rxLidarRegion = -1; + rxTerrainHeightFlag = false; + txLatitudeDeg = quietNaN; + txLongitudeDeg = quietNaN; + txGroundElevation = quietNaN; + txTerrainHeight = quietNaN; + txHeightAboveTerrain = quietNaN; + txHeightAMSL = quietNaN; + txHeightSource = CConst::unknownHeightSource; + azimuthAngleToTx = quietNaN; + elevationAngleToTx = quietNaN; + txCenterToRAATHeight = quietNaN; + txLidarRegion = -1; + txTerrainHeightFlag = false; + noiseLevelDBW = quietNaN; + txGain = quietNaN; + rxGain = quietNaN; + rxDlambda = quietNaN; + rxNearFieldAntDiameter = quietNaN; + rxNearFieldDistLimit = quietNaN; + rxNearFieldAntEfficiency = quietNaN; + rxAntennaCategory = CConst::UnknownAntennaCategory; + txEIRP = quietNaN; + linkDistance = quietNaN; + propLoss = quietNaN; + + hasDiversity = false; + diversityGain = quietNaN; + diversityDlambda = quietNaN; + diversityHeightAboveTerrain = quietNaN; + diversityHeightAMSL = quietNaN; + + minPathLossDB = quietNaN; + maxPathLossDB = quietNaN; + antHeight = quietNaN; + type = CConst::ESULSType; + + satellitePosnData = (ListClass *)NULL; + mobilePopGrid = (PopGridClass *)NULL; + rxAntennaType = CConst::UnknownAntennaType; + txAntennaType = CConst::UnknownAntennaType; + rxAntenna = (AntennaClass *)NULL; + txAntenna = (AntennaClass *)NULL; + rxAntennaFeederLossDB = quietNaN; + fadeMarginDB = quietNaN; + pairIdx = -1; + numOutOfBandRLAN = 0; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ULSClass::~ULSClass() ****/ +/******************************************************************************************/ +ULSClass::~ULSClass() +{ + clearData(); + + delete[] prList; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** ULSClass:: static members ****/ +/******************************************************************************************/ +double ULSClass::azPointing = 0.0; +CConst::AngleUnitEnum ULSClass::azPointingUnit = CConst::degreeAngleUnit; + +double ULSClass::elPointing = 3.0 * M_PI / 180.0; +CConst::AngleUnitEnum ULSClass::elPointingUnit = CConst::degreeAngleUnit; + +CConst::PathLossModelEnum ULSClass::pathLossModel = CConst::unknownPathLossModel; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** ULSClass:: SET/GET Functions ****/ +/******************************************************************************************/ +int ULSClass::getID() const +{ + return (id); +} +int ULSClass::getDBIdx() const +{ + return (dbIdx); +} +std::string ULSClass::getRegion() const +{ + return (region); +} +Vector3 ULSClass::getRxPosition() +{ + return (rxPosition); +} +Vector3 ULSClass::getTxPosition() +{ + return (txPosition); +} +Vector3 ULSClass::getAntennaPointing() +{ + return (antennaPointing); +} +CConst::ULSTypeEnum ULSClass::getType() +{ + return (type); +} +ListClass *ULSClass::getSatellitePositionData() +{ + return (satellitePosnData); +} +double ULSClass::getStartFreq() +{ + return (startFreq); +} +double ULSClass::getStopFreq() +{ + return (stopFreq); +} +double ULSClass::getNoiseBandwidth() +{ + return (noiseBandwidth); +} +int ULSClass::getNumPR() +{ + return (numPR); +} +std::string ULSClass::getRadioService() +{ + return (radioService); +} +std::string ULSClass::getEntityName() +{ + return (entityName); +} +std::string ULSClass::getCallsign() +{ + return (callsign); +} +int ULSClass::getPathNumber() +{ + return (pathNumber); +} +std::string ULSClass::getRxCallsign() +{ + return (rxCallsign); +} +int ULSClass::getRxAntennaNumber() +{ + return (rxAntennaNumber); +} +double ULSClass::getRxLongitudeDeg() const +{ + return (rxLongitudeDeg); +} +double ULSClass::getRxLatitudeDeg() const +{ + return (rxLatitudeDeg); +} +double ULSClass::getRxGroundElevation() +{ + return (rxGroundElevation); +} +double ULSClass::getRxTerrainHeight() +{ + return (rxTerrainHeight); +} +double ULSClass::getRxHeightAboveTerrain() +{ + return (rxHeightAboveTerrain); +} +double ULSClass::getRxHeightAMSL() +{ + return (rxHeightAMSL); +} +CConst::HeightSourceEnum ULSClass::getRxHeightSource() +{ + return (rxHeightSource); +} +double ULSClass::getTxLongitudeDeg() +{ + return (txLongitudeDeg); +} +double ULSClass::getTxLatitudeDeg() +{ + return (txLatitudeDeg); +} +std::string ULSClass::getTxPolarization() +{ + return (txPolarization); +} +double ULSClass::getTxGroundElevation() +{ + return (txGroundElevation); +} +double ULSClass::getTxTerrainHeight() +{ + return (txTerrainHeight); +} +double ULSClass::getTxHeightAboveTerrain() +{ + return (txHeightAboveTerrain); +} +double ULSClass::getTxHeightAMSL() +{ + return (txHeightAMSL); +} +CConst::HeightSourceEnum ULSClass::getTxHeightSource() +{ + return (txHeightSource); +} +double ULSClass::getAzimuthAngleToTx() +{ + return (azimuthAngleToTx); +} +double ULSClass::getElevationAngleToTx() +{ + return (elevationAngleToTx); +} + +double ULSClass::getNoiseLevelDBW() +{ + return (noiseLevelDBW); +} +double ULSClass::getRxGain() +{ + return (rxGain); +} +double ULSClass::getRxDlambda() +{ + return (rxDlambda); +} +double ULSClass::getRxNearFieldAntDiameter() +{ + return (rxNearFieldAntDiameter); +} +double ULSClass::getRxNearFieldDistLimit() +{ + return (rxNearFieldDistLimit); +} +double ULSClass::getRxNearFieldAntEfficiency() +{ + return (rxNearFieldAntEfficiency); +} +CConst::AntennaCategoryEnum ULSClass::getRxAntennaCategory() +{ + return (rxAntennaCategory); +} +double ULSClass::getRxAntennaFeederLossDB() +{ + return (rxAntennaFeederLossDB); +} +double ULSClass::getFadeMarginDB() +{ + return (fadeMarginDB); +} +std::string ULSClass::getStatus() +{ + return (status); +} +CConst::ULSAntennaTypeEnum ULSClass::getRxAntennaType() +{ + return (rxAntennaType); +} +CConst::ULSAntennaTypeEnum ULSClass::getTxAntennaType() +{ + return (txAntennaType); +} +AntennaClass *ULSClass::getRxAntenna() +{ + return (rxAntenna); +} +AntennaClass *ULSClass::getTxAntenna() +{ + return (txAntenna); +} +double ULSClass::getTxGain() +{ + return (txGain); +} +double ULSClass::getTxEIRP() +{ + return (txEIRP); +} +double ULSClass::getLinkDistance() +{ + return (linkDistance); +} +double ULSClass::getPropLoss() +{ + return (propLoss); +} +int ULSClass::getPairIdx() +{ + return (pairIdx); +} +int ULSClass::getRxLidarRegion() +{ + return (rxLidarRegion); +} +int ULSClass::getTxLidarRegion() +{ + return (txLidarRegion); +} +bool ULSClass::getRxTerrainHeightFlag() +{ + return (rxTerrainHeightFlag); +} +bool ULSClass::getTxTerrainHeightFlag() +{ + return (txTerrainHeightFlag); +} +int ULSClass::getNumOutOfBandRLAN() +{ + return (numOutOfBandRLAN); +} + +void ULSClass::setSatellitePositionData(ListClass *spd) +{ + satellitePosnData = spd; + return; +} +void ULSClass::setRxPosition(Vector3 &p) +{ + rxPosition = p; + return; +} +void ULSClass::setTxPosition(Vector3 &p) +{ + txPosition = p; + return; +} +void ULSClass::setAntennaPointing(Vector3 &p) +{ + antennaPointing = p; + return; +} +void ULSClass::setType(CConst::ULSTypeEnum typeVal) +{ + type = typeVal; + return; +} +void ULSClass::setStartFreq(double f) +{ + startFreq = f; + return; +} +void ULSClass::setStopFreq(double f) +{ + stopFreq = f; + return; +} +void ULSClass::setNoiseBandwidth(double b) +{ + noiseBandwidth = b; + return; +} +void ULSClass::setRadioService(std::string radioServiceVal) +{ + radioService = radioServiceVal; + return; +} +void ULSClass::setEntityName(std::string entityNameVal) +{ + entityName = entityNameVal; + return; +} +void ULSClass::setCallsign(std::string callsignVal) +{ + callsign = callsignVal; + return; +} +void ULSClass::setPathNumber(int pathNumberVal) +{ + pathNumber = pathNumberVal; + return; +} +void ULSClass::setRxCallsign(std::string rxCallsignVal) +{ + rxCallsign = rxCallsignVal; + return; +} +void ULSClass::setRxAntennaNumber(int rxAntennaNumberVal) +{ + rxAntennaNumber = rxAntennaNumberVal; + return; +} +void ULSClass::setRxLatitudeDeg(double rxLatitudeDegVal) +{ + rxLatitudeDeg = rxLatitudeDegVal; + return; +} +void ULSClass::setRxLongitudeDeg(double rxLongitudeDegVal) +{ + rxLongitudeDeg = rxLongitudeDegVal; + return; +} +void ULSClass::setRxGroundElevation(double rxGroundElevationVal) +{ + rxGroundElevation = rxGroundElevationVal; + return; +} +void ULSClass::setRxTerrainHeight(double rxTerrainHeightVal) +{ + rxTerrainHeight = rxTerrainHeightVal; + return; +} +void ULSClass::setRxHeightAboveTerrain(double rxHeightAboveTerrainVal) +{ + rxHeightAboveTerrain = rxHeightAboveTerrainVal; + return; +} +void ULSClass::setRxHeightAMSL(double rxHeightAMSLVal) +{ + rxHeightAMSL = rxHeightAMSLVal; + return; +} +void ULSClass::setRxHeightSource(CConst::HeightSourceEnum rxHeightSourceVal) +{ + rxHeightSource = rxHeightSourceVal; + return; +} +void ULSClass::setTxLatitudeDeg(double txLatitudeDegVal) +{ + txLatitudeDeg = txLatitudeDegVal; + return; +} +void ULSClass::setTxLongitudeDeg(double txLongitudeDegVal) +{ + txLongitudeDeg = txLongitudeDegVal; + return; +} +void ULSClass::setTxPolarization(std::string txPolarizationVal) +{ + txPolarization = txPolarizationVal; + return; +} +void ULSClass::setTxGroundElevation(double txGroundElevationVal) +{ + txGroundElevation = txGroundElevationVal; + return; +} +void ULSClass::setTxTerrainHeight(double txTerrainHeightVal) +{ + txTerrainHeight = txTerrainHeightVal; + return; +} +void ULSClass::setTxHeightAboveTerrain(double txHeightAboveTerrainVal) +{ + txHeightAboveTerrain = txHeightAboveTerrainVal; + return; +} +void ULSClass::setTxHeightAMSL(double txHeightAMSLVal) +{ + txHeightAMSL = txHeightAMSLVal; + return; +} +void ULSClass::setTxHeightSource(CConst::HeightSourceEnum txHeightSourceVal) +{ + txHeightSource = txHeightSourceVal; + return; +} +void ULSClass::setAzimuthAngleToTx(double azimuthAngleToTxVal) +{ + azimuthAngleToTx = azimuthAngleToTxVal; + return; +} +void ULSClass::setElevationAngleToTx(double elevationAngleToTxVal) +{ + elevationAngleToTx = elevationAngleToTxVal; + return; +} +void ULSClass::setNoiseLevelDBW(double noiseLevelDBWVal) +{ + noiseLevelDBW = noiseLevelDBWVal; +} +void ULSClass::setRxGain(double rxGainVal) +{ + rxGain = rxGainVal; + return; +} +void ULSClass::setRxDlambda(double rxDlambdaVal) +{ + rxDlambda = rxDlambdaVal; + return; +} +void ULSClass::setRxNearFieldAntDiameter(double rxNearFieldAntDiameterVal) +{ + rxNearFieldAntDiameter = rxNearFieldAntDiameterVal; + return; +} +void ULSClass::setRxNearFieldDistLimit(double rxNearFieldDistLimitVal) +{ + rxNearFieldDistLimit = rxNearFieldDistLimitVal; + return; +} +void ULSClass::setRxNearFieldAntEfficiency(double rxNearFieldAntEfficiencyVal) +{ + rxNearFieldAntEfficiency = rxNearFieldAntEfficiencyVal; + return; +} +void ULSClass::setRxAntennaCategory(CConst::AntennaCategoryEnum rxAntennaCategoryVal) +{ + rxAntennaCategory = rxAntennaCategoryVal; + return; +} +void ULSClass::setRxAntennaFeederLossDB(double rxAntennaFeederLossDBVal) +{ + rxAntennaFeederLossDB = rxAntennaFeederLossDBVal; + return; +} +void ULSClass::setFadeMarginDB(double fadeMarginDBVal) +{ + fadeMarginDB = fadeMarginDBVal; + return; +} +void ULSClass::setStatus(std::string statusVal) +{ + status = statusVal; + return; +} +void ULSClass::setRxAntennaType(CConst::ULSAntennaTypeEnum rxAntennaTypeVal) +{ + rxAntennaType = rxAntennaTypeVal; + return; +} +void ULSClass::setTxAntennaType(CConst::ULSAntennaTypeEnum txAntennaTypeVal) +{ + txAntennaType = txAntennaTypeVal; + return; +} +void ULSClass::setRxAntenna(AntennaClass *rxAntennaVal) +{ + rxAntenna = rxAntennaVal; + return; +} +void ULSClass::setTxAntenna(AntennaClass *txAntennaVal) +{ + txAntenna = txAntennaVal; + return; +} +void ULSClass::setTxGain(double txGainVal) +{ + txGain = txGainVal; + return; +} +void ULSClass::setTxEIRP(double txEIRPVal) +{ + txEIRP = txEIRPVal; + return; +} +void ULSClass::setLinkDistance(double linkDistanceVal) +{ + linkDistance = linkDistanceVal; + return; +} +void ULSClass::setPropLoss(double propLossVal) +{ + propLoss = propLossVal; + return; +} +void ULSClass::setPairIdx(int pairIdxVal) +{ + pairIdx = pairIdxVal; + return; +} +void ULSClass::setRxLidarRegion(int lidarRegionVal) +{ + rxLidarRegion = lidarRegionVal; + return; +} +void ULSClass::setTxLidarRegion(int lidarRegionVal) +{ + txLidarRegion = lidarRegionVal; + return; +} +void ULSClass::setRxTerrainHeightFlag(bool terrainHeightFlagVal) +{ + rxTerrainHeightFlag = terrainHeightFlagVal; + return; +} +void ULSClass::setTxTerrainHeightFlag(bool terrainHeightFlagVal) +{ + txTerrainHeightFlag = terrainHeightFlagVal; + return; +} +void ULSClass::setNumOutOfBandRLAN(int numOutOfBandRLANVal) +{ + numOutOfBandRLAN = numOutOfBandRLANVal; + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ULSClass::clearData ****/ +/******************************************************************************************/ +void ULSClass::clearData() +{ + if (satellitePosnData) { + delete satellitePosnData; + satellitePosnData = (ListClass *)NULL; + } + + if (location) { + free(location); + location = (char *)NULL; + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ULSClass::computeRxGain ****/ +/******************************************************************************************/ +double ULSClass::computeRxGain(double angleOffBoresightDeg, + double elevationAngleDeg, + double frequency, + std::string &subModelStr, + int divIdx) +{ + double rxGainDB; + subModelStr = ""; + + double maxGain = (divIdx == 0 ? rxGain : diversityGain); + double Dlambda = (divIdx == 0 ? rxDlambda : diversityDlambda); + + switch (rxAntennaType) { + case CConst::F1245AntennaType: + rxGainDB = calcItu1245::CalcITU1245(angleOffBoresightDeg, maxGain, Dlambda); + break; + case CConst::F699AntennaType: + rxGainDB = calcItu699::CalcITU699(angleOffBoresightDeg, maxGain, Dlambda); + break; + case CConst::F1336OmniAntennaType: + rxGainDB = calcItu1336_4::CalcITU1336_omni_avg(elevationAngleDeg, + maxGain, + frequency); + break; + case CConst::R2AIP07AntennaType: + rxGainDB = calcR2AIP07Antenna(angleOffBoresightDeg, + frequency, + rxAntennaModel, + rxAntennaCategory, + subModelStr, + divIdx, + maxGain, + Dlambda); + break; + case CConst::R2AIP07CANAntennaType: + rxGainDB = calcR2AIP07CANAntenna(angleOffBoresightDeg, + frequency, + rxAntennaModel, + rxAntennaCategory, + subModelStr, + divIdx, + maxGain, + Dlambda); + break; + case CConst::OmniAntennaType: + rxGainDB = 0.0; + break; + case CConst::LUTAntennaType: + rxGainDB = rxAntenna->gainDB(angleOffBoresightDeg * M_PI / 180.0) + rxGain; + break; + default: + throw std::runtime_error( + ErrStream() << "ERROR in ULSClass::computeRxGain: rxAntennaType = " + << rxAntennaType << " INVALID value for FSID = " << id); + break; + } + + return (rxGainDB); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ULSClass::calcR2AIP07Antenna ****/ +/******************************************************************************************/ +double ULSClass::calcR2AIP07Antenna(double angleOffBoresightDeg, + double frequency, + std::string antennaModel, + CConst::AntennaCategoryEnum category, + std::string &subModelStr, + int divIdx, + double maxGain, + double Dlambda) +{ + // int freqIdx; + double rxGainDB; + + // if ((frequency >= 5925.0e6) && (frequency <= 6425.0e6)) { + // freqIdx = 0; + // } else if ((frequency >= 6525.0e6) && (frequency <= 6875.0e6)) { + // freqIdx = 1; + // } else { + // throw std::runtime_error(ErrStream() << "ERROR in ULSClass::calcR2AIP07Antenna: frequency + // = " << frequency << " INVALID value"); + // } + + if (maxGain < 38) { + if (angleOffBoresightDeg < 5) { + subModelStr = ":F.699"; + rxGainDB = calcItu699::CalcITU699(angleOffBoresightDeg, maxGain, Dlambda); + } else if (divIdx == 0) { + // Table 2, Category B2 + double minSuppression; + if (angleOffBoresightDeg < 10.0) { + minSuppression = 15.0; + } else if (angleOffBoresightDeg < 15.0) { + minSuppression = 20.0; + } else if (angleOffBoresightDeg < 20.0) { + minSuppression = 23.0; + } else if (angleOffBoresightDeg < 30.0) { + minSuppression = 28.0; + } else if (angleOffBoresightDeg < 100.0) { + minSuppression = 29.0; + } else { + minSuppression = 60.0; + } + subModelStr = ":catB2"; + rxGainDB = maxGain - minSuppression; + } else { + // Table 2, Category B1 + double minSuppression; + if (angleOffBoresightDeg < 10.0) { + minSuppression = 21.0; + } else if (angleOffBoresightDeg < 15.0) { + minSuppression = 25.0; + } else if (angleOffBoresightDeg < 20.0) { + minSuppression = 29.0; + } else if (angleOffBoresightDeg < 30.0) { + minSuppression = 32.0; + } else if (angleOffBoresightDeg < 100.0) { + minSuppression = 35.0; + } else if (angleOffBoresightDeg < 140.0) { + minSuppression = 39.0; + } else { + minSuppression = 45.0; + } + subModelStr = ":catB1"; + rxGainDB = maxGain - minSuppression; + } + } else { + if (angleOffBoresightDeg < 5) { + subModelStr = ":F.699"; + rxGainDB = calcItu699::CalcITU699(angleOffBoresightDeg, maxGain, Dlambda); + } else { + bool categoryB1Flag = (category == CConst::B1AntennaCategory); + bool knownHighPerformance = (category == CConst::HPAntennaCategory); + + if (categoryB1Flag) { + // Table 2, Category B1 + double minSuppression; + if (angleOffBoresightDeg < 10.0) { + minSuppression = 21.0; + } else if (angleOffBoresightDeg < 15.0) { + minSuppression = 25.0; + } else if (angleOffBoresightDeg < 20.0) { + minSuppression = 29.0; + } else if (angleOffBoresightDeg < 30.0) { + minSuppression = 32.0; + } else if (angleOffBoresightDeg < 100.0) { + minSuppression = 35.0; + } else if (angleOffBoresightDeg < 140.0) { + minSuppression = 39.0; + } else { + minSuppression = 45.0; + } + subModelStr = ":catB1"; + rxGainDB = maxGain - minSuppression; + } else if (knownHighPerformance) { + // Table 2, Category A + double minSuppressionA; + if (angleOffBoresightDeg < 10.0) { + minSuppressionA = 25.0; + } else if (angleOffBoresightDeg < 15.0) { + minSuppressionA = 29.0; + } else if (angleOffBoresightDeg < 20.0) { + minSuppressionA = 33.0; + } else if (angleOffBoresightDeg < 30.0) { + minSuppressionA = 36.0; + } else if (angleOffBoresightDeg < 100.0) { + minSuppressionA = 42.0; + } else { + minSuppressionA = 55.0; + } + + double descrimination699 = + maxGain - calcItu699::CalcITU699(angleOffBoresightDeg, + maxGain, + Dlambda); + + double descriminationDB; + if (descrimination699 >= minSuppressionA) { + subModelStr = ":F.699"; + descriminationDB = descrimination699; + } else { + subModelStr = ":catA"; + descriminationDB = minSuppressionA; + } + + rxGainDB = maxGain - descriminationDB; + } else { + // Table 2, Category A + double minSuppressionA; + if (angleOffBoresightDeg < 10.0) { + minSuppressionA = 25.0; + } else if (angleOffBoresightDeg < 15.0) { + minSuppressionA = 29.0; + } else if (angleOffBoresightDeg < 20.0) { + minSuppressionA = 33.0; + } else if (angleOffBoresightDeg < 30.0) { + minSuppressionA = 36.0; + } else if (angleOffBoresightDeg < 100.0) { + minSuppressionA = 42.0; + } else { + minSuppressionA = 55.0; + } + + subModelStr = ":catA"; + rxGainDB = maxGain - minSuppressionA; + } + } + } + + return (rxGainDB); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ULSClass::calcR2AIP07CANAntenna ****/ +/******************************************************************************************/ +double ULSClass::calcR2AIP07CANAntenna(double angleOffBoresightDeg, + double frequency, + std::string antennaModel, + CConst::AntennaCategoryEnum category, + std::string &subModelStr, + int divIdx, + double maxGain, + double Dlambda) +{ + int freqIdx; + double rxGainDB; + + if ((frequency >= 5925.0e6) && (frequency <= 6425.0e6)) { + freqIdx = 0; + } else if ((frequency >= 6425.0e6) && (frequency <= 6930.0e6)) { + freqIdx = 1; + } else { + freqIdx = -1; + } + + double minSuppression; + if (freqIdx == 0) { + if (angleOffBoresightDeg < 1.7) { + minSuppression = 0.0; + } else if (angleOffBoresightDeg < 5.8) { + minSuppression = 2.6; + } else if (angleOffBoresightDeg < 8.0) { + minSuppression = 17.0; + } else if (angleOffBoresightDeg < 11.0) { + minSuppression = 21.0; + } else if (angleOffBoresightDeg < 15.0) { + minSuppression = 23.0; + } else if (angleOffBoresightDeg < 20.0) { + minSuppression = 28.0; + } else if (angleOffBoresightDeg < 30.0) { + minSuppression = 30.0; + } else if (angleOffBoresightDeg < 35.0) { + minSuppression = 33.0; + } else if (angleOffBoresightDeg < 100.0) { + minSuppression = 35.0; + } else if (angleOffBoresightDeg < 140.0) { + minSuppression = 39.0; + } else { + minSuppression = 45.0; + } + } else if (freqIdx == 1) { + if (angleOffBoresightDeg < 1.1) { + minSuppression = 0.0; + } else if (angleOffBoresightDeg < 5.0) { + minSuppression = 3.0; + } else if (angleOffBoresightDeg < 10.0) { + minSuppression = 21.0; + } else if (angleOffBoresightDeg < 15.0) { + minSuppression = 25.0; + } else if (angleOffBoresightDeg < 20.0) { + minSuppression = 29.0; + } else if (angleOffBoresightDeg < 30.0) { + minSuppression = 32.0; + } else if (angleOffBoresightDeg < 100.0) { + minSuppression = 35.0; + } else if (angleOffBoresightDeg < 140.0) { + minSuppression = 39.0; + } else { + minSuppression = 45.0; + } + } else { + throw std::runtime_error(ErrStream() + << "ERROR in ULSClass::calcR2AIP07CANAntenna: frequency = " + << frequency << " INVALID value"); + } + + rxGainDB = maxGain - minSuppression; + + return (rxGainDB); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ULSClass::computeBeamWidth ****/ +/******************************************************************************************/ +double ULSClass::computeBeamWidth(double attnDB) +{ + double rxGainDB; + std::ostringstream errStr; + + CConst::ULSAntennaTypeEnum ulsRxAntennaType = getRxAntennaType(); + + double g0 = getRxGain(); + + // std::cout << "ULS: " << getID() << " GAIN (DB) = " << g0 << std::endl; + + if (ulsRxAntennaType == CConst::F1336OmniAntennaType) { + throw std::runtime_error(ErrStream() + << "ERROR in ULSClass::computeBeamWidth: ulsRxAntennaType " + "= F1336OmniAntennaType not supported"); + } + + double a1 = 0.0; + double frequency = (startFreq + stopFreq) / 2; + + double a2 = a1; + double e2; + do { + if (a2 == 180.0) { + errStr << "ERROR: Unable to compute " << attnDB + << " dB beamwidth with GAIN (DB) = " << g0 << std::endl; + throw std::runtime_error(errStr.str()); + } + a2 += 2.0 * exp(-g0 * log(10.0) / 20.0) * 180.0 / M_PI; + if (a2 > 180.0) { + a2 = 180.0; + } + double angleOffBoresightDeg = a2; + + std::string subModelStr; + rxGainDB = computeRxGain(angleOffBoresightDeg, -1.0, frequency, subModelStr, 0); + + e2 = rxGainDB - g0 + attnDB; + } while (e2 > 0.0); + + while (a2 - a1 > 1.0e-8) { + double a3 = (a1 + a2) / 2; + double angleOffBoresightDeg = a3; + + std::string subModelStr; + rxGainDB = computeRxGain(angleOffBoresightDeg, -1.0, frequency, subModelStr, 0); + + double e3 = rxGainDB - g0 + attnDB; + + if (e3 > 0.0) { + a1 = a3; + } else { + a2 = a3; + e2 = e3; + } + } + + double beamWidthDeg = a1; + // std::cout << "ULS: " << getID() << " BEAMWIDTH (deg) = " << beamWidthDeg << std::endl; + + return (beamWidthDeg); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PRClass::PRClass() ****/ +/******************************************************************************************/ +PRClass::PRClass() +{ + pathSegGain = quietNaN; + effectiveGain = quietNaN; + + type = CConst::unknownPRType; + + latitudeDeg = quietNaN; + longitudeDeg = quietNaN; + heightAboveTerrainRx = quietNaN; + heightAboveTerrainTx = quietNaN; + + terrainHeight = quietNaN; + heightAMSLRx = quietNaN; + heightAMSLTx = quietNaN; + heightSource = CConst::unknownHeightSource; + lidarRegion = -1; + terrainHeightFlag = false; + positionRx = Vector3(quietNaN, quietNaN, quietNaN); + positionTx = Vector3(quietNaN, quietNaN, quietNaN); + pointing = Vector3(quietNaN, quietNaN, quietNaN); + segmentDistance = quietNaN; + + txGain = quietNaN; + txDlambda = quietNaN; + rxGain = quietNaN; + rxDlambda = quietNaN; + antCategory = CConst::UnknownAntennaCategory; + antModel = ""; + antennaType = CConst::UnknownAntennaType; + antenna = (AntennaClass *)NULL; + + reflectorHeightLambda = quietNaN; + reflectorWidthLambda = quietNaN; + + reflectorX = Vector3(quietNaN, quietNaN, quietNaN); + reflectorY = Vector3(quietNaN, quietNaN, quietNaN); + reflectorZ = Vector3(quietNaN, quietNaN, quietNaN); + + reflectorThetaIN = quietNaN; + reflectorKS = quietNaN; + reflectorQ = quietNaN; + + reflectorSLambda = quietNaN; + reflectorTheta1 = quietNaN; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PRClass::~PRClass() ****/ +/******************************************************************************************/ +PRClass::~PRClass() +{ +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: PRClass::computeDiscriminationGain ****/ +/******************************************************************************************/ +double PRClass::computeDiscriminationGain(double angleOffBoresightDeg, + double elevationAngleDeg, + double frequency, + double &reflectorD0, + double &reflectorD1) +{ + double discriminationDB; + + switch (type) { + case CConst::backToBackAntennaPRType: { + std::string subModelStr; + switch (antennaType) { + case CConst::F1245AntennaType: + discriminationDB = + calcItu1245::CalcITU1245(angleOffBoresightDeg, + rxGain, + rxDlambda) - + rxGain; + break; + case CConst::F699AntennaType: + discriminationDB = + calcItu699::CalcITU699(angleOffBoresightDeg, + rxGain, + rxDlambda) - + rxGain; + break; + case CConst::F1336OmniAntennaType: + discriminationDB = calcItu1336_4::CalcITU1336_omni_avg( + elevationAngleDeg, + rxGain, + frequency) - + rxGain; + break; + case CConst::R2AIP07AntennaType: + discriminationDB = + ULSClass::calcR2AIP07Antenna(angleOffBoresightDeg, + frequency, + antModel, + antCategory, + subModelStr, + 0, + rxGain, + rxDlambda) - + rxGain; + break; + case CConst::R2AIP07CANAntennaType: + discriminationDB = ULSClass::calcR2AIP07CANAntenna( + angleOffBoresightDeg, + frequency, + antModel, + antCategory, + subModelStr, + 0, + rxGain, + rxDlambda) - + rxGain; + break; + case CConst::OmniAntennaType: + discriminationDB = 0.0; + break; + case CConst::LUTAntennaType: + discriminationDB = antenna->gainDB(angleOffBoresightDeg * + M_PI / 180.0); + break; + default: + throw std::runtime_error( + ErrStream() + << "ERROR in PRClass::computeDiscriminationGain: " + "antennaType = " + << antennaType << " INVALID value"); + break; + } + + reflectorD0 = quietNaN; + reflectorD1 = quietNaN; + } break; + case CConst::billboardReflectorPRType: { + double D0 = -10.0 * + log10(4 * M_PI * reflectorWidthLambda * reflectorHeightLambda * + cos(reflectorThetaIN * M_PI / 180.0)); + double D1; + double u_over_PI = reflectorSLambda * + sin(angleOffBoresightDeg * M_PI / 180.0); + + if (angleOffBoresightDeg <= reflectorTheta1) { + D1 = 20 * log10(MathHelpers::sinc(u_over_PI)); + } else if (angleOffBoresightDeg <= 20.0) { + D1 = -20 * log10(fabs(M_PI * u_over_PI)); + } else { + double u0_over_PI = reflectorSLambda * sin(20.0 * M_PI / 180.0); + D1 = -20 * log10(fabs(M_PI * u0_over_PI)) - + 0.4165 * (angleOffBoresightDeg - 20.0); + } + discriminationDB = std::max(D0, D1); + + reflectorD0 = D0; + reflectorD1 = D1; + } break; + default: + discriminationDB = quietNaN; + CORE_DUMP; + break; + } + + return discriminationDB; +} +/******************************************************************************************/ diff --git a/src/afc-engine/uls.h b/src/afc-engine/uls.h new file mode 100644 index 0000000..858f24f --- /dev/null +++ b/src/afc-engine/uls.h @@ -0,0 +1,420 @@ +/******************************************************************************************/ +/**** FILE : uls.h ****/ +/******************************************************************************************/ + +#ifndef ULS_H +#define ULS_H + +#include "Vector3.h" +#include "cconst.h" +#include "pop_grid.h" + +class GdalDataDir; +class WorldData; +class AfcManager; +class AntennaClass; + +template +class ListClass; + +/******************************************************************************************/ +/**** CLASS: PRClass - Passive Repeater ****/ +/******************************************************************************************/ +class PRClass +{ + public: + PRClass(); + ~PRClass(); + + double computeDiscriminationGain(double angleOffBoresightDeg, + double elevationAngleDeg, + double frequency, + double &reflectorD0, + double &reflectorD1); + + // Path segment gain as defined in R2-AIP-31 + double pathSegGain; + double effectiveGain; + + CConst::PRTypeEnum type; + + double latitudeDeg; + double longitudeDeg; + double heightAboveTerrainRx; + double heightAboveTerrainTx; + + double terrainHeight; + double heightAMSLRx; + double heightAMSLTx; + CConst::HeightSourceEnum heightSource; + int lidarRegion; + bool terrainHeightFlag; + Vector3 positionRx; + Vector3 positionTx; + Vector3 pointing; + double segmentDistance; + + double txGain; + double txDlambda; + double rxGain; + double rxDlambda; + CConst::AntennaCategoryEnum antCategory; + std::string antModel; + CConst::ULSAntennaTypeEnum antennaType; + AntennaClass *antenna; + + double reflectorHeightLambda; + double reflectorWidthLambda; + + // Reflector 3D coordinate system: + // X: horizontal vector on reflector surface in direction of width + // Y: vector on reflector surface in direction of height. Note that when reflector + // is tilted, this is not vertical relative to the ground. X: vector perpendicular + // to reflector surface X, Y, Z are orthonormal basis + Vector3 reflectorX, reflectorY, reflectorZ; + + double reflectorThetaIN; // Inclusion angle between incident and reflected waves at + // reflector is 2*thetaIN + double reflectorKS; + double reflectorQ; + // double reflectorAlphaEL; // Inclusion angle in elevation plane is 2*alphaEL + // (relative to reflector orthonormal basis) double reflectorAlphaAZ; // Inclusion + // angle in azimuthal plane is 2*alphaAZ (relative to reflector orthonormal basis) + + double reflectorSLambda; // (s/lambda) used in calculation of discrimination gain + double reflectorTheta1; // Theta1 used in calculation of discrimination gain +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CLASS: ULSClass ****/ +/******************************************************************************************/ +class ULSClass +{ + public: + ULSClass(AfcManager *dataSetVal, + int idVal, + int dbIdx, + int numPRVal, + std::string regionVal); + ~ULSClass(); + + int getID() const; + int getDBIdx() const; + std::string getRegion() const; + CConst::ULSTypeEnum getType(); + ListClass *getSatellitePositionData(); + double getStartFreq(); + double getStopFreq(); + double getNoiseBandwidth(); + std::string getRadioService(); + std::string getEntityName(); + std::string getCallsign(); + int getPathNumber(); + std::string getRxCallsign(); + int getRxAntennaNumber(); + int getNumPR(); + PRClass &getPR(int prIdx) + { + return prList[prIdx]; + } + Vector3 getRxPosition(); + Vector3 getTxPosition(); + Vector3 getAntennaPointing(); + + double getRxLatitudeDeg() const; + double getRxLongitudeDeg() const; + double getRxGroundElevation(); + double getRxTerrainHeight(); + double getRxHeightAboveTerrain(); + double getRxHeightAMSL(); + CConst::HeightSourceEnum getRxHeightSource(); + + double getTxLatitudeDeg(); + double getTxLongitudeDeg(); + double getTxGroundElevation(); + double getTxTerrainHeight(); + double getTxHeightAboveTerrain(); + double getTxHeightAMSL(); + CConst::HeightSourceEnum getTxHeightSource(); + double getAzimuthAngleToTx(); + double getElevationAngleToTx(); + + std::string getTxPolarization(); + double getTxSrtmHeight(); + double getTxCenterToRAATHeight(); + double getNoiseLevelDBW(); + double getRxGain(); + double getRxDlambda(); + double getRxNearFieldAntDiameter(); + double getRxNearFieldDistLimit(); + double getRxNearFieldAntEfficiency(); + CConst::AntennaCategoryEnum getRxAntennaCategory(); + double getRxAntennaFeederLossDB(); + double getFadeMarginDB(); + std::string getStatus(); + double computeBeamWidth(double attnDB); + double computeRxGain(double angleOffBoresightDeg, + double elevationAngleDeg, + double frequency, + std::string &subModelStr, + int divIdx); + + static double calcR2AIP07Antenna(double angleOffBoresightDeg, + double frequency, + std::string antennaModel, + CConst::AntennaCategoryEnum category, + std::string &subModelStr, + int divIdx, + double maxGain, + double Dlambda); + static double calcR2AIP07CANAntenna(double angleOffBoresightDeg, + double frequency, + std::string antennaModel, + CConst::AntennaCategoryEnum category, + std::string &subModelStr, + int divIdx, + double maxGain, + double Dlambda); + + std::string getRxAntennaModel() + { + return rxAntennaModel; + } + CConst::ULSAntennaTypeEnum getRxAntennaType(); + CConst::ULSAntennaTypeEnum getTxAntennaType(); + AntennaClass *getRxAntenna(); + AntennaClass *getTxAntenna(); + double getTxGain(); + double getTxEIRP(); + double getLinkDistance(); + double getPropLoss(); + int getPairIdx(); + int getRxLidarRegion(); + int getTxLidarRegion(); + bool getRxTerrainHeightFlag(); + bool getTxTerrainHeightFlag(); + int getNumOutOfBandRLAN(); + + bool getHasDiversity() + { + return hasDiversity; + } + double getDiversityGain() + { + return diversityGain; + } + double getDiversityDlambda() + { + return diversityDlambda; + } + double getDiversityHeightAboveTerrain() + { + return diversityHeightAboveTerrain; + } + double getDiversityHeightAMSL() + { + return diversityHeightAMSL; + } + Vector3 getDiversityPosition() + { + return diversityPosition; + } + Vector3 getDiversityAntennaPointing() + { + return diversityAntennaPointing; + } + + void setStartFreq(double f); + void setStopFreq(double f); + void setNoiseBandwidth(double b); + void setRadioService(std::string radioServiceVal); + void setEntityName(std::string entityNameVal); + void setCallsign(std::string callsignVal); + void setPathNumber(int pathNumberVal); + + void setRxCallsign(std::string rxCallsignVal); + void setRxAntennaNumber(int rxAntennaNumberVal); + void setRxLatitudeDeg(double latitudeDegVal); + void setRxLongitudeDeg(double longitudeDegVal); + void setRxGroundElevation(double rxGroundElevationVal); + void setRxTerrainHeight(double rxTerrainHeightVal); + void setRxHeightAboveTerrain(double rxHeightAboveTerrainVal); + void setRxHeightAMSL(double rxHeightAMSLVal); + void setRxHeightSource(CConst::HeightSourceEnum rxHeightSourceVal); + + void setTxLatitudeDeg(double latitudeDegVal); + void setTxLongitudeDeg(double longitudeDegVal); + void setTxGroundElevation(double txGroundElevationVal); + void setTxTerrainHeight(double txTerrainHeightVal); + void setTxHeightAboveTerrain(double txHeightAboveTerrainVal); + void setTxHeightAMSL(double txHeightAMSLVal); + void setTxHeightSource(CConst::HeightSourceEnum txHeightSourceVal); + void setTxPolarization(std::string txPolarizationVal); + void setAzimuthAngleToTx(double azimuthAngleToTxVal); + void setElevationAngleToTx(double elevationAngleToTxVal); + + void setNoiseLevelDBW(double noiseLevelDBWVal); + void setRxGain(double rxGainVal); + void setRxDlambda(double rxDlambdaVal); + void setRxNearFieldAntDiameter(double rxNearFieldAntDiameterVal); + void setRxNearFieldDistLimit(double rxNearFieldDistLimitVal); + void setRxNearFieldAntEfficiency(double rxNearFieldAntEfficiencyVal); + void setRxAntennaCategory(CConst::AntennaCategoryEnum rxAntennaCategoryVal); + void setRxAntennaFeederLossDB(double rxAntennaFeederLossDBVal); + void setFadeMarginDB(double fadeMarginDBVal); + void setStatus(std::string statusVal); + void setRxAntennaModel(std::string rxAntennaModelVal) + { + rxAntennaModel = rxAntennaModelVal; + return; + } + void setRxAntennaType(CConst::ULSAntennaTypeEnum rxAntennaTypeVal); + void setTxAntennaType(CConst::ULSAntennaTypeEnum txAntennaTypeVal); + void setRxAntenna(AntennaClass *rxAntennaVal); + void setTxAntenna(AntennaClass *txAntennaVal); + void setTxGain(double txGainVal); + void setTxEIRP(double txEIRPVal); + void setLinkDistance(double linkDistanceVal); + void setPropLoss(double propLossVal); + void setPairIdx(int pairIdxVal); + void setRxLidarRegion(int lidarRegionVal); + void setTxLidarRegion(int lidarRegionVal); + void setRxTerrainHeightFlag(bool terrainHeightFlagVal); + void setTxTerrainHeightFlag(bool terrainHeightFlagVal); + void setNumOutOfBandRLAN(int numOutOfBandRLANVal); + + void setRxPosition(Vector3 &p); + void setTxPosition(Vector3 &p); + void setAntennaPointing(Vector3 &p); + void setType(CConst::ULSTypeEnum typeVal); + void setSatellitePositionData(ListClass *spd); + + void setHasDiversity(bool hasDiversityVal) + { + hasDiversity = hasDiversityVal; + } + void setDiversityGain(double diversityGainVal) + { + diversityGain = diversityGainVal; + } + void setDiversityDlambda(double diversityDlambdaVal) + { + diversityDlambda = diversityDlambdaVal; + } + void setDiversityHeightAboveTerrain(double diversityHeightAboveTerrainVal) + { + diversityHeightAboveTerrain = diversityHeightAboveTerrainVal; + } + void setDiversityHeightAMSL(double diversityHeightAMSLVal) + { + diversityHeightAMSL = diversityHeightAMSLVal; + } + void setDiversityPosition(Vector3 &diversityPositionVal) + { + diversityPosition = diversityPositionVal; + } + void setDiversityAntennaPointing(Vector3 &diversityAntennaPointingVal) + { + diversityAntennaPointing = diversityAntennaPointingVal; + } + + void clearData(); + + static const int numPtsPDF = 1000; + static double azPointing, elPointing; + static CConst::AngleUnitEnum azPointingUnit, elPointingUnit; + static GdalDataDir *gdalDir; + static WorldData *globeModel; + static CConst::PathLossModelEnum pathLossModel; + + char *location; + double *ITMHeightProfile; + double *isLOSHeightProfile; + double isLOSSurfaceFrac; + +#if DEBUG_AFC + std::vector ITMHeightType; +#endif + + private: + AfcManager *dataSet; + + int id; + int dbIdx; + int numPR; + std::string region; + double startFreq; + double stopFreq; + double noiseBandwidth; + std::string callsign; + int pathNumber; + std::string rxCallsign; + int rxAntennaNumber; + std::string radioService; + std::string entityName; + double rxLatitudeDeg; + double rxLongitudeDeg; + double rxTerrainHeight; + double rxHeightAboveTerrain; + double rxHeightAMSL; + double rxGroundElevation; + CConst::HeightSourceEnum rxHeightSource; + int rxLidarRegion; + bool rxTerrainHeightFlag; + double txLatitudeDeg; + double txLongitudeDeg; + double txGroundElevation; + double txTerrainHeight; + double txHeightAboveTerrain; + double txHeightAMSL; + CConst::HeightSourceEnum txHeightSource; + double azimuthAngleToTx; + double elevationAngleToTx; + std::string txPolarization; + double txCenterToRAATHeight; + int txLidarRegion; + bool txTerrainHeightFlag; + double noiseLevelDBW; + double txGain, rxGain, rxDlambda; + double rxNearFieldAntDiameter; + double rxNearFieldDistLimit; + double rxNearFieldAntEfficiency; + CConst::AntennaCategoryEnum rxAntennaCategory; + double txEIRP; + double linkDistance; + double propLoss; + + bool hasDiversity; + double diversityGain; + double diversityDlambda; + double diversityHeightAboveTerrain; + double diversityHeightAMSL; + Vector3 diversityPosition; + + Vector3 diversityAntennaPointing; + + PRClass *prList; + + double minPathLossDB, maxPathLossDB; + Vector3 txPosition, rxPosition; + Vector3 antennaPointing; + double antHeight; + CConst::ULSTypeEnum type; + + ListClass *satellitePosnData; + PopGridClass *mobilePopGrid; // Pop grid for mobile simulation + CConst::ULSAntennaTypeEnum rxAntennaType; + CConst::ULSAntennaTypeEnum txAntennaType; + std::string rxAntennaModel; + AntennaClass *rxAntenna; + AntennaClass *txAntenna; + double rxAntennaFeederLossDB; + double fadeMarginDB; + std::string status; + int pairIdx; + int numOutOfBandRLAN; +}; +/******************************************************************************************/ + +#endif diff --git a/src/afc-packages/afcmodels/afcmodels/__init__.py b/src/afc-packages/afcmodels/afcmodels/__init__.py new file mode 100644 index 0000000..13ce5a2 --- /dev/null +++ b/src/afc-packages/afcmodels/afcmodels/__init__.py @@ -0,0 +1 @@ +from . import base, aaa diff --git a/src/afc-packages/afcmodels/afcmodels/aaa.py b/src/afc-packages/afcmodels/afcmodels/aaa.py new file mode 100644 index 0000000..2ed380c --- /dev/null +++ b/src/afc-packages/afcmodels/afcmodels/aaa.py @@ -0,0 +1,232 @@ +# +# This Python file uses the following encoding: utf-8 +# +# Portions copyright (C) 2021 Broadcom. +# All rights reserved. The term “Broadcom” refers solely +# to the Broadcom Inc. corporate affiliate that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which +# is included with this software program. +# +# pylint: disable=no-member +''' Authentication, authorization, and accounting classes. +''' +import time +import datetime +from .base import db, UserDbInfo +import jwt +from appcfg import OIDCConfigurator +import os +from sqlalchemy.schema import Sequence +from sqlalchemy.dialects.postgresql import JSON + +OIDC_LOGIN = OIDCConfigurator().OIDC_LOGIN + +if OIDC_LOGIN: + from flask_login import UserMixin +else: + from flask_user import UserMixin + + +class User(db.Model, UserMixin): + ''' Each user account in the system. ''' + __tablename__ = 'aaa_user' + __table_args__ = ( + db.UniqueConstraint('email'), + ) + id = db.Column(db.Integer, primary_key=True) + + # UserDbInfo.VER indicates the version of the user database. + # This can be overriden with env variables in case the server + # is boot up with an older database before it's migrated. + org = db.Column(db.String(255), nullable=True) + username = db.Column(db.String(50), nullable=False, unique=True) + sub = db.Column(db.String(255)) + email = db.Column(db.String(255), nullable=False) + email_confirmed_at = db.Column(db.DateTime()) + password = db.Column(db.String(255), nullable=False) + active = db.Column(db.Boolean()) + + # Application data fields + first_name = db.Column(db.String(50)) + last_name = db.Column(db.String(50)) + + # Relationships + roles = db.relationship( + 'Role', secondary='aaa_user_role', back_populates='users') + + @staticmethod + def get(user_id): + return User.query.filter_by(id=user_id).first() + + @staticmethod + def getsub(user_sub): + return User.query.filter_by(sub=user_sub).first() + + @staticmethod + def getemail(user_email): + return User.query.filter_by(email=user_email).first() + + +class Role(db.Model): + ''' A role is used for authorization. ''' + __tablename__ = 'aaa_role' + __table_args__ = ( + db.UniqueConstraint('name'), + ) + id = db.Column(db.Integer(), primary_key=True) + + #: Role definition + name = db.Column(db.String(50)) + + # Relationships + users = db.relationship( + 'User', secondary='aaa_user_role', back_populates='roles') + + +class UserRole(db.Model): + ''' Map users to roles. ''' + __tablename__ = 'aaa_user_role' + __table_args__ = ( + db.UniqueConstraint('user_id', 'role_id'), + ) + id = db.Column(db.Integer(), primary_key=True) + + user_id = db.Column(db.Integer(), db.ForeignKey( + 'aaa_user.id', ondelete='CASCADE')) + role_id = db.Column(db.Integer(), db.ForeignKey( + 'aaa_role.id', ondelete='CASCADE')) + + +class CertId(db.Model): + ''' entry to designate allowed AP's for the PAWS interface ''' + + __tablename__ = 'cert_id' + UNKNOWN = 0 + OUTDOOR = 2 + INDOOR = 1 + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + certification_id = db.Column(db.String(64), nullable=False) + refreshed_at = db.Column(db.DateTime()) + ruleset_id = db.Column(db.Integer, db.ForeignKey( + 'aaa_ruleset.id', ondelete='CASCADE'), nullable=False) + location = db.Column(db.Integer, nullable=False) + + def __init__(self, certification_id, location): + self.certification_id = certification_id + self.location = location + self.refreshed_at = datetime.datetime.now() + + +class AccessPointDeny(db.Model): + ''' entry to designate allowed AP's for the PAWS interface ''' + + __tablename__ = 'access_point_deny' + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + serial_number = db.Column(db.String(64), nullable=True, index=True) + certification_id = db.Column(db.String(64)) + org_id = db.Column(db.Integer, db.ForeignKey( + 'aaa_org.id', ondelete='CASCADE')) + ruleset_id = db.Column(db.Integer, db.ForeignKey( + 'aaa_ruleset.id', ondelete='CASCADE')) + + def __init__(self, serial_number=None, certification_id=None): + self.serial_number = serial_number + self.certification_id = certification_id + + +class MTLS(db.Model): + ''' entry to designate allowed MTLS's for the PAWS interface ''' + + __tablename__ = 'MTLS' + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + # 32KB limit of certificate data + cert = db.Column(db.String(32768), nullable=False, unique=False) + note = db.Column(db.String(128), nullable=True, unique=False) + org = db.Column(db.String(64), nullable=False) + created = db.Column(db.DateTime(), nullable=False) + + def __init__(self, cert, note, org): + self.cert = cert + self.note = note + self.org = org + self.created = datetime.datetime.fromtimestamp(time.time()) + + +class Limit(db.Model): + ''' entry for limits ''' + # this table is ready to expand to hold different limits if needed, but the init method would need to be + # upgraded to have more than a boolean for indoor/outdoor eirp mins. + + __tablename__ = 'limits' + __table_args__ = ( + db.UniqueConstraint('name', name="limits_name_unique"), + ) + # only one set of limits currently + id = db.Column(db.Integer(), primary_key=True) + # Application data fields + limit = db.Column(db.Numeric(50)) + enforce = db.Column(db.Boolean()) + name = db.Column(db.String(64)) + + def __init__(self, min_eirp, enforce, isOutdoor): + if not isOutdoor: + self.id = 0 + self.limit = min_eirp + self.enforce = enforce + self.name = "Indoor Min EIRP" + else: + self.id = 1 + self.limit = min_eirp + self.enforce = enforce + self.name = "Outdoor Min EIRP" + + +class AFCConfig(db.Model): + ''' entry for AFC Config ''' + + __tablename__ = 'AFCConfig' + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + # 4KB limit of afc config json string + config = db.Column(JSON) + created = db.Column(db.DateTime(), nullable=False) + + def __init__(self, config): + self.config = config + self.created = datetime.datetime.fromtimestamp(time.time()) + + +class Organization(db.Model): + ''' entry for Organization ''' + + __tablename__ = 'aaa_org' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + name = db.Column(db.String(50), nullable=False, unique=True) + aps = db.relationship("AccessPointDeny", backref="org") + + def __init__(self, name): + self.name = name + + +class Ruleset(db.Model): + ''' entry for Organization ''' + + __tablename__ = 'aaa_ruleset' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + name = db.Column(db.String(50), nullable=False, unique=True) + aps = db.relationship("AccessPointDeny", backref="ruleset") + cert_ids = db.relationship("CertId", backref="ruleset") + + def __init__(self, name): + self.name = name + + +# Local Variables: +# mode: Python +# indent-tabs-mode: nil +# python-indent: 4 +# End: +# +# vim: sw=4:et:tw=80:cc=+1 diff --git a/src/afc-packages/afcmodels/afcmodels/base.py b/src/afc-packages/afcmodels/afcmodels/base.py new file mode 100644 index 0000000..b5cc2b8 --- /dev/null +++ b/src/afc-packages/afcmodels/afcmodels/base.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# + +""" +Description + +Non-table database definitions +""" + +from flask_sqlalchemy import SQLAlchemy + +#: Application database object +db = SQLAlchemy() + + +class UserDbInfo(): + VER = 1 diff --git a/src/afc-packages/afcmodels/setup.py b/src/afc-packages/afcmodels/setup.py new file mode 100644 index 0000000..508094b --- /dev/null +++ b/src/afc-packages/afcmodels/setup.py @@ -0,0 +1,25 @@ +from setuptools import setup, find_packages +from setuptools.command.install import install +import inspect + + +class InstallCmdWrapper(install): + def run(self): + print(f"{inspect.stack()[0][3]}()") + install.run(self) + + +setup( + name='afcmodels', + # Label compatible with PEP 440 + version='0.1.0', + description='AFC packages', + packages=['afcmodels'], + cmdclass={ + 'install': InstallCmdWrapper, + }, + install_requires=[ + 'Flask==2.3.2', + 'Flask-SQLAlchemy==2.5.1' + ] +) diff --git a/src/afc-packages/afcobjst/afcobjst/__init__.py b/src/afc-packages/afcobjst/afcobjst/__init__.py new file mode 100644 index 0000000..cc5915e --- /dev/null +++ b/src/afc-packages/afcobjst/afcobjst/__init__.py @@ -0,0 +1,5 @@ +''' Package and app definition. +''' + +from .filestorage import objst_app +from .history import hist_app diff --git a/src/afc-packages/afcobjst/afcobjst/filestorage.py b/src/afc-packages/afcobjst/afcobjst/filestorage.py new file mode 100755 index 0000000..983fb1a --- /dev/null +++ b/src/afc-packages/afcobjst/afcobjst/filestorage.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 + +# Copyright 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +""" +Provides HTTP server for file exchange between Celery clients and workers. +""" + +import os +import logging +import shutil +import socket +import abc +import waitress +from posix_ipc import Semaphore, O_CREAT +from flask import Flask, request, abort, make_response +import google.cloud.storage +from .objstconf import ObjstConfigInternal + +NET_TIMEOUT = 600 # The amount of time, in seconds, to wait for the server response +SEM_TIMEOUT = 60 # Per file semaphore timeout + +objst_app = Flask(__name__) +objst_app.config.from_object(ObjstConfigInternal()) + +if objst_app.config['AFC_OBJST_LOG_FILE']: + logging.basicConfig(filename=objst_app.config['AFC_OBJST_LOG_FILE'], + level=objst_app.config['AFC_OBJST_LOG_LVL']) +else: + logging.basicConfig(level=objst_app.config['AFC_OBJST_LOG_LVL']) + +if objst_app.config["AFC_OBJST_MEDIA"] == "GoogleCloudBucket": + os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = objst_app.config["AFC_OBJST_GOOGLE_CLOUD_CREDENTIALS_JSON"] + client = google.cloud.storage.client.Client() + bucket = client.bucket(objst_app.config["AFC_OBJST_GOOGLE_CLOUD_BUCKET"]) + + +class ObjInt: + """ Abstract class for data prot operations """ + __metaclass__ = abc.ABCMeta + + def __init__(self, file_name): + self._file_name = file_name + + @abc.abstractmethod + def write(self, data): + pass + + @abc.abstractmethod + def read(self): + pass + + @abc.abstractmethod + def head(self): + pass + + @abc.abstractmethod + def delete(self): + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + pass + + +class ObjIntLocalFS(ObjInt): + def __lock(self, name): + sem_name = "/" + name[1:].replace("/", "_") + sem = Semaphore(sem_name, O_CREAT, initial_value=1) + sem.acquire(timeout=SEM_TIMEOUT) + return sem + + def __unlock(self, sem): + sem.release() + sem.close() + try: + sem.unlink() + except BaseException: + pass + + def write(self, data): + self.__mkdir_local(os.path.dirname(self._file_name)) + sem = self.__lock(self._file_name) + with open(self._file_name, 'wb') as f: + f.write(data) + self.__unlock(sem) + + def read(self): + if not os.path.isfile(self._file_name): + return None + sem = self.__lock(self._file_name) + with open(self._file_name, "rb") as hfile: + ret = hfile.read() + self.__unlock(sem) + return ret + + def head(self): + sem = self.__lock(self._file_name) + ret = os.path.exists(self._file_name) + self.__unlock(sem) + return ret + + def delete(self): + """ During recursive dir delete only the dir is protected by semaphore from + parallel use. Files in the dir arn't protected. """ + if os.path.exists(self._file_name): + sem = self.__lock(self._file_name) + if os.path.isdir(self._file_name): + shutil.rmtree(self._file_name) + else: + os.remove(self._file_name) + self.__unlock(sem) + + def __mkdir_local(self, path): + os.makedirs(path, exist_ok=True) + + +class ObjIntGoogleCloudBucket(ObjInt): + def write(self, data): + blob = bucket.blob(self._file_name) + blob.upload_from_string(data, + content_type="application/octet-stream", + timeout=NET_TIMEOUT) + + def read(self): + blob = bucket.blob(self._file_name) + return blob.download_as_bytes(raw_download=True, + timeout=NET_TIMEOUT) + + def head(self): + blobs = client.list_blobs(bucket, + prefix=self._file_name, + delimeter="/", + timeout=NET_TIMEOUT) + return blobs is not None + + def delete(self): + blobs = client.list_blobs(bucket, + prefix=self._file_name, + delimeter="/", + timeout=NET_TIMEOUT) + for blob in blobs: + try: + blob.delete(timeout=NET_TIMEOUT) + except BaseException: + pass # ignore google.cloud.exceptions.NotFound + + +class Objstorage: + def open(self, name): + """ Create ObjInt instance """ + if objst_app.config["AFC_OBJST_MEDIA"] == "GoogleCloudBucket": + return ObjIntGoogleCloudBucket(name) + if objst_app.config["AFC_OBJST_MEDIA"] == "LocalFS": + return ObjIntLocalFS(name) + + +def get_local_path(path): + path = os.path.join(objst_app.config["AFC_OBJST_FILE_LOCATION"], path) + return path + + +@objst_app.route('/' + '', methods=['POST']) +def post(path): + ''' File upload handler. ''' + objst_app.logger.debug(f'post {path}') + try: + path = get_local_path(path) + + data = None + + if 'file' in request.files: + if not request.files['file']: + objst_app.logger.error('No file in request') + abort(400) + if request.files['file'].filename == '': + objst_app.logger.error('Empty filename') + abort(400) + path = os.path.join(path, request.files['file'].filename) + data = request.files['file'].read() + else: + data = request.get_data() + + objst = Objstorage() + with objst.open(path) as hobj: + hobj.write(data) + except Exception as e: + objst_app.logger.error(e) + return abort(500) + + return make_response('OK', 200) + + +@objst_app.route('/' + '', methods=["DELETE"]) +def delete(path): + ''' File/dir delete handler. ''' + objst_app.logger.debug(f'delete {path}') + path = get_local_path(path) + + try: + objst = Objstorage() + with objst.open(path) as hobj: + hobj.delete() + except Exception as e: + objst_app.logger.error(e) + return make_response('File not found', 404) + + return make_response('OK', 204) + + +@objst_app.route('/', defaults={'path': ''}, methods=['HEAD']) +# handle URL with filename +@objst_app.route('/' + '', methods=['HEAD']) +def head(path): + ''' Is file exist handler. ''' + objst_app.logger.debug(f'head {path}') + path = get_local_path(path) + + try: + objst = Objstorage() + with objst.open(path) as hobj: + if hobj.head(): + return make_response('OK', 200) + else: + return make_response('File not found', 404) + except Exception as e: + objst_app.logger.error(e) + return abort(500) + + +@objst_app.route('/healthy', methods=['GET']) +def healthcheck(): + ''' Get method for healthcheck. ''' + msg = 'The objst is healthy' + objst_app.logger.debug( + f"{msg}." + f" own ip: {socket.gethostbyname(socket.gethostname())}" + f" from: {request.remote_addr}") + return make_response(msg, 200) + + +# handle URL with filename +@objst_app.route('/' + '', methods=['GET']) +def get(path): + ''' File download handler. ''' + objst_app.logger.debug(f'get {path}') + path = get_local_path(path) + + try: + objst = Objstorage() + with objst.open(path) as hobj: + data = hobj.read() + if data: + return data + objst_app.logger.error('{}: File not found'.format(path)) + return make_response('File not found', 404) + except Exception as e: + objst_app.logger.error(e) + return abort(500) + + +if __name__ == '__main__': + objst_app.logger.debug( + "port={} AFC_OBJST_FILE_LOCATION={} AFC_OBJST_MEDIA={}". format( + objst_app.config['AFC_OBJST_PORT'], + objst_app.config['AFC_OBJST_FILE_LOCATION'], + objst_app.config["AFC_OBJST_MEDIA"])) + os.makedirs(objst_app.config['AFC_OBJST_FILE_LOCATION'], exist_ok=True) + # production env: + waitress.serve( + objst_app, port=objst_app.config['AFC_OBJST_PORT'], host="0.0.0.0") + # Development env: + # objst_app.run(port=objst_app.config['AFC_OBJST_PORT'], host="0.0.0.0", debug=True) + +# Local Variables: +# mode: Python +# indent-tabs-mode: nil +# python-indent: 4 +# End: +# +# vim: sw=4:et:tw=80:cc=+1 diff --git a/src/afc-packages/afcobjst/afcobjst/history.py b/src/afc-packages/afcobjst/afcobjst/history.py new file mode 100755 index 0000000..e80b0f1 --- /dev/null +++ b/src/afc-packages/afcobjst/afcobjst/history.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 + +# Copyright 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +""" +Provides HTTP server for getting history. +""" + +import os +import logging +import io +import abc +import waitress +from flask import Flask, request, helpers, abort +import google.cloud.storage +from .objstconf import ObjstConfigInternal + +NET_TIMEOUT = 60 # The amount of time, in seconds, to wait for the server response + +hist_app = Flask(__name__) +hist_app.config.from_object(ObjstConfigInternal()) + +if hist_app.config['AFC_OBJST_LOG_FILE']: + logging.basicConfig(filename=hist_app.config['AFC_OBJST_LOG_FILE'], + level=hist_app.config['AFC_OBJST_LOG_LVL']) +else: + logging.basicConfig(level=hist_app.config['AFC_OBJST_LOG_LVL']) + +if hist_app.config["AFC_OBJST_MEDIA"] == "GoogleCloudBucket": + os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = hist_app.config["AFC_OBJST_GOOGLE_CLOUD_CREDENTIALS_JSON"] + client = google.cloud.storage.client.Client() + bucket = client.bucket(hist_app.config["AFC_OBJST_GOOGLE_CLOUD_BUCKET"]) + + +def generateHtml(rurl, path, dirs, files): + hist_app.logger.debug(f"generateHtml({rurl}, {path}, {dirs}, {files})") + dirs.sort() + files.sort() + vpath = "history" + if path is not None and path != "": + vpath += "/" + path + + html = """ + + + + + +
+
    +""" + + for e in dirs: + html += "
  • " + e + """/
  • +""" + for e in files: + html += "
  • " + e + """
  • +""" + + html += """
+
+ + +""" + + hist_app.logger.debug(html) + + return html.encode() + + +class ObjInt: + """ Abstract class for data prot operations """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def isdir(self): + pass + + @abc.abstractmethod + def list(self): + pass + + @abc.abstractmethod + def read(self): + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + pass + + +class ObjIntLocalFS(ObjInt): + def __init__(self, file_name): + self.__file_name = file_name + + def isdir(self): + return os.path.isdir(self.__file_name) + + def list(self): + hist_app.logger.debug("ObjIntLocalFS.list") + ls = os.listdir(self.__file_name) + files = [f for f in ls if os.path.isfile( + os.path.join(self.__file_name, f))] + dirs = [f for f in ls if os.path.isdir( + os.path.join(self.__file_name, f))] + return dirs, files + + def read(self): + hist_app.logger.debug( + "ObjIntLocalFS.read({})".format(self.__file_name)) + if os.path.isfile(self.__file_name): + with open(self.__file_name, "rb") as hfile: + return hfile.read() + return None + + +class ObjIntGoogleCloudBucket(ObjInt): + def __init__(self, file_name): + self.__file_name = file_name + self.__blob = bucket.blob(self.__file_name) + + def isdir(self): + return not self.__blob.exists() + + def list(self): + hist_app.logger.debug("ObjIntGoogleCloudBucket.list") + blobs = bucket.list_blobs(prefix=self.__file_name + "/") + files = [] + dirs = set() + for blob in blobs: + name = blob.name.removeprefix(self.__file_name + "/") + if name.count("/"): + dirs.add(name.split("/")[0]) + else: + files.append(name) + return list(dirs), files + + def read(self): + blob = bucket.blob(self.__file_name) + return blob.download_as_bytes(raw_download=True, + timeout=NET_TIMEOUT) + + +class Objstorage: + def open(self, name): + """ Create ObjInt instance """ + hist_app.logger.debug("Objstorage.open({})".format(name)) + if hist_app.config["AFC_OBJST_MEDIA"] == "GoogleCloudBucket": + return ObjIntGoogleCloudBucket(name) + if hist_app.config["AFC_OBJST_MEDIA"] == "LocalFS": + return ObjIntLocalFS(name) + raise Exception("Unsupported AFC_OBJST_MEDIA \"{}\"". + format(hist_app.config["AFC_OBJST_MEDIA"])) + + +@hist_app.route('/', defaults={'path': ""}, methods=['GET']) +@hist_app.route('/' + '', methods=['GET']) +def get(path): + ''' File download handler. ''' + # ratapi URL preffix + rurl = request.args["url"] + fwd_proto = request.headers.get('X-Forwarded-Proto') + if (fwd_proto == 'https') and (request.scheme == "http"): + rurl = rurl.replace("http:", "https:") + hist_app.logger.debug( + f'get method={request.method}, path={path} url={rurl}') + # local path in the storage + lpath = os.path.join( + hist_app.config["AFC_OBJST_FILE_LOCATION"], "history", path) + + try: + objst = Objstorage() + with objst.open(lpath) as hobj: + if hobj.isdir() is True: + dirs, files = hobj.list() + return generateHtml(rurl, path, dirs, files) + data = hobj.read() + return helpers.send_file( + io.BytesIO(data), + download_name=os.path.basename(path)) + except Exception as e: + hist_app.logger.error(e) + return abort(500) + + +if __name__ == '__main__': + os.makedirs(os.path.join( + hist_app.config["AFC_OBJST_FILE_LOCATION"], "history"), exist_ok=True) + waitress.serve( + hist_app, port=hist_app.config["AFC_OBJST_HIST_PORT"], host="0.0.0.0") + + # hist_app.run(port=hist_app.config['AFC_OBJST_HIST_PORT'], host="0.0.0.0", debug=True) diff --git a/src/afc-packages/afcobjst/afcobjst/objstconf.py b/src/afc-packages/afcobjst/afcobjst/objstconf.py new file mode 100644 index 0000000..8ac2ff0 --- /dev/null +++ b/src/afc-packages/afcobjst/afcobjst/objstconf.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +# Copyright 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +""" +Provides env var config for filestorage and history. +""" + +import os +from appcfg import ObjstConfigBase, InvalidEnvVar + + +class ObjstConfigInternal(ObjstConfigBase): + """Filestorage internal config""" + + def __init__(self): + ObjstConfigBase.__init__(self) + self.AFC_OBJST_LOG_FILE = os.getenv( + "AFC_OBJST_LOG_FILE", "/proc/self/fd/2") + self.AFC_OBJST_LOG_LVL = os.getenv("AFC_OBJST_LOG_LVL", "ERROR") + # supported AFC_OBJST_MEDIA backends are "GoogleCloudBucket" and + # "LocalFS" + self.AFC_OBJST_MEDIA = os.getenv("AFC_OBJST_MEDIA", "LocalFS") + if self.AFC_OBJST_MEDIA not in ("GoogleCloudBucket", "LocalFS"): + raise InvalidEnvVar("Invalid AFC_OBJST_MEDIA env var.") + if self.AFC_OBJST_MEDIA == "LocalFS": + # file download/upload location on the server in case of + # AFC_OBJST_MEDIA=LocalFS + self.AFC_OBJST_FILE_LOCATION = os.getenv( + "AFC_OBJST_LOCAL_DIR", "/storage") + else: + self.AFC_OBJST_GOOGLE_CLOUD_CREDENTIALS_JSON = os.getenv( + "AFC_OBJST_GOOGLE_CLOUD_CREDENTIALS_JSON") + self.AFC_OBJST_GOOGLE_CLOUD_BUCKET = os.getenv( + "AFC_OBJST_GOOGLE_CLOUD_BUCKET") diff --git a/src/afc-packages/afcobjst/setup.py b/src/afc-packages/afcobjst/setup.py new file mode 100644 index 0000000..93d5d98 --- /dev/null +++ b/src/afc-packages/afcobjst/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages +from setuptools.command.install import install +import os + + +class InstallCmdWrapper(install): + def run(self): + install.run(self) + + +setup( + name='afcobjst', + # Label compatible with PEP 440 + version='1.0.0', + description='AFC packages', + py_modules=["afcobjst"], + packages=["afcobjst"], + install_requires=["requests==2.31.0", "flask==2.3.2", "werkzeug==3.0.1", + "waitress==2.1.2", "google.cloud.storage==2.9.0", "posix_ipc==1.1.1"], + cmdclass={ + 'install': InstallCmdWrapper, + } +) diff --git a/src/afc-packages/afctask/afctask.py b/src/afc-packages/afctask/afctask.py new file mode 100644 index 0000000..3a67716 --- /dev/null +++ b/src/afc-packages/afctask/afctask.py @@ -0,0 +1,123 @@ +# coding=utf-8 + +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +""" +Provides replacement for AsyncResult routines +""" + +import logging +import json +import time +import os + +LOGGER = logging.getLogger(__name__) + + +class Task(): + """ Replacement for AsyncResult class and self serialization""" + + STAT_PENDING = "PENDING" + STAT_PROGRESS = "PROGRESS" + STAT_SUCCESS = "SUCCESS" + STAT_FAILURE = "FAILURE" + + def __init__(self, task_id, dataif, hash_val=None, history_dir=None, + is_internal_request=False): + LOGGER.debug(f"Task.__init__() {task_id}") + self.__dataif = dataif + self.__task_id = task_id + self.__stat = { + 'status': self.STAT_PENDING, + 'history_dir': history_dir, + 'hash': hash_val, + 'runtime_opts': None, + 'exit_code': 0, + 'is_internal_request': is_internal_request + } + + def get(self): + LOGGER.debug("Task.get()") + data = None + fstatus = os.path.join("/responses", self.__task_id, "status.json") + try: + with self.__dataif.open(fstatus) as hfile: + data = hfile.read() + except BaseException: + LOGGER.debug("task.get() no {}".format( + self.__dataif.rname(fstatus))) + return self.__toDict(self.STAT_PENDING) + stat = json.loads(data) + + LOGGER.debug("task.get() {}".format(stat)) + if ('status' not in stat or + 'history_dir' not in stat or + 'hash' not in stat or + 'runtime_opts' not in stat or + 'exit_code' not in stat): + LOGGER.error("task.get() bad status.json: {}".format(stat)) + raise Exception("Bad status.json") + if (stat['status'] != self.STAT_PROGRESS and + stat['status'] != self.STAT_SUCCESS and + stat['status'] != self.STAT_FAILURE): + LOGGER.error( + "task.get() bad status {} in status.json".format( + stat['status'])) + raise Exception("Bad status in status.json") + self.__stat = stat + return self.__stat + + def wait(self, timeout, delay=2): + LOGGER.debug("Task.wait() timeout={timeout}") + stat = None + time0 = time.time() + while True: + time.sleep(delay) + stat = self.get() + LOGGER.debug("task.wait() status {}".format(stat['status'])) + if (stat['status'] == Task.STAT_SUCCESS or + stat['status'] == Task.STAT_FAILURE): + return stat + if (time.time() - time0) > timeout: + LOGGER.error("task.wait() timeout") + return self.__toDict(Task.STAT_PROGRESS) + LOGGER.debug("task.wait() exit") + + def ready(self, stat): + return stat['status'] == Task.STAT_SUCCESS or \ + stat['status'] == self.STAT_FAILURE + + def successful(self, stat): + return stat['status'] == Task.STAT_SUCCESS + + def __toDict(self, status, runtime_opts=None, exit_code=0): + self.__stat['status'] = status + self.__stat['runtime_opts'] = runtime_opts + self.__stat['exit_code'] = exit_code + return self.__stat + + def toJson(self, status, runtime_opts=None, exit_code=0): + LOGGER.debug("toJson({})".format(status)) + data = json.dumps(self.__toDict(status, runtime_opts, exit_code)) + fstatus = os.path.join("/responses", self.__task_id, "status.json") + with self.__dataif.open(fstatus) as hfile: + LOGGER.debug("toJson() write {}".format(data)) + hfile.write(data) + + def forget(self): + fstatus = os.path.join("/responses", self.__task_id, "status.json") + with self.__dataif.open(fstatus) as hfile: + hfile.delete() + + def getStat(self): + return self.__stat + + def getDataif(self): + return self.__dataif + + def getId(self): + return self.__task_id diff --git a/src/afc-packages/afctask/setup.py b/src/afc-packages/afctask/setup.py new file mode 100644 index 0000000..ae7393e --- /dev/null +++ b/src/afc-packages/afctask/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup, find_packages +from setuptools.command.install import install +import os + + +class InstallCmdWrapper(install): + def run(self): + install.run(self) + + +setup( + name='afctask', + # Label compatible with PEP 440 + version='0.1.0', + description='AFC packages', + py_modules=["afctask"], + cmdclass={ + 'install': InstallCmdWrapper, + } +) diff --git a/src/afc-packages/afcworker/afc_worker.py b/src/afc-packages/afcworker/afc_worker.py new file mode 100644 index 0000000..2af1245 --- /dev/null +++ b/src/afc-packages/afcworker/afc_worker.py @@ -0,0 +1,252 @@ +# +# This Python file uses the following encoding: utf-8 +# +# Portions copyright (C) 2021 Broadcom. +# All rights reserved. The term “Broadcom” refers solely +# to the Broadcom Inc. corporate affiliate that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which +# is included with this software program. +# +import os +import subprocess +import shutil +import tempfile +import zlib +from celery import Celery +from celery.utils.log import get_task_logger +from appcfg import BrokerConfigurator +from fst import DataIf +import defs +import afctask +import als +import json +from rcache_models import RcacheClientSettings +from rcache_client import RcacheClient + +LOGGER = get_task_logger(__name__) + + +class WorkerConfig(BrokerConfigurator): + """Worker internal config""" + + def __init__(self): + BrokerConfigurator.__init__(self) + self.AFC_ENGINE = os.getenv("AFC_ENGINE") + self.AFC_ENGINE_LOG_LVL = os.getenv("AFC_ENGINE_LOG_LVL", "info") + self.AFC_WORKER_CELERY_LOG = os.getenv("AFC_WORKER_CELERY_LOG") + # worker task engine timeout + # the default value predefined by image environment (Dockefile) + self.AFC_WORKER_ENG_TOUT = os.getenv("AFC_WORKER_ENG_TOUT") + + +conf = WorkerConfig() + +rcache_settings = RcacheClientSettings() +# In this validation rcache is True to handle 'update_on_send' case +rcache_settings.validate_for(rmq=True, rcache=True) +rcache = RcacheClient(rcache_settings, rmq_receiver=False) \ + if rcache_settings.enabled else None + +LOGGER.info('Celery Broker: %s', conf.BROKER_URL) + +#: constant celery reference. Configure once flask app is created +client = Celery( + 'fbrat', + broker=conf.BROKER_URL, + task_ignore_result=True, + broker_pool_limit=0, + task_acks_late=True, + worker_prefetch_multiplier=1, +) + + +@client.task(ignore_result=True) +def run(prot, host, port, request_type, task_id, hash_val, + config_path, history_dir, runtime_opts, mntroot, rcache_queue, + request_str, config_str): + """ Run AFC Engine + + The parameters are all serializable so they can be passed through the message queue. + + :param request_type: Anslysis type to pass to AFC Engine + + :param task_id ID of associated task (if task-based synchronization is used), also used as name of directory for error files + + :param hash_val: md5 hex digest of request and config + + :param config_path: Objstore path of afc_config.json if task-based synchronization is used, None otherwise + + :param history_dir: Objstore directory for debug files files + + :param runtime_opts: Runtime options to pass to AFC Engine. Also specifies which debug fles to keep + + :param mntroot: path to directory where GeoData and config data are stored + + :param rcache_queue: None for task-based synchronization, RabbitMQ queue name for RMQ-based synchronization + + :param request_str None for task-based synchronization, request text for RMQ-based synchronization + + :param config_str None for task-based synchronization, config text for RMQ-based synchronization + """ + LOGGER.debug(f"run(prot={prot}, host={host}, port={port}, " + f"task_id={task_id}, hash={hash_val}, opts={runtime_opts}, " + f"mntroot={mntroot}, timeout={conf.AFC_WORKER_ENG_TOUT}, " + f"rcache_queue={rcache_queue}") + + use_tasks = rcache_queue is None + if use_tasks: + assert task_id and config_path and \ + (runtime_opts & defs.RNTM_OPT_AFCENGINE_HTTP_IO) + else: + assert rcache and request_str and config_str and \ + (not (runtime_opts & defs.RNTM_OPT_AFCENGINE_HTTP_IO)) + + proc = None + try: + tmpdir = tempfile.mkdtemp(prefix="afc_worker_") + + dataif = DataIf(prot, host, port) + if use_tasks: + tsk = afctask.Task(task_id, dataif, hash_val, history_dir) + tsk.toJson(afctask.Task.STAT_PROGRESS, runtime_opts=runtime_opts) + + err_file = open(os.path.join(tmpdir, "engine-error.txt"), "wb") + log_file = open(os.path.join(tmpdir, "engine-log.txt"), "wb") + + if use_tasks: + # pathes in objstorage + analysis_request_path = \ + dataif.rname(os.path.join("/responses", hash_val, + "analysisRequest.json")) + analysis_config_path = dataif.rname(config_path) + analysis_response_path = \ + dataif.rname(os.path.join("/responses", hash_val, + "analysisResponse.json.gz")) + else: + analysis_request_path = os.path.join(tmpdir, + "analysisRequest.json") + analysis_config_path = os.path.join(tmpdir, "afc_config.json") + analysis_response_path = os.path.join(tmpdir, + "analysisResponse.json.gz") + for fname, data in [(analysis_request_path, request_str), + (analysis_config_path, config_str)]: + with open(fname, "w", encoding="utf-8") as outfile: + outfile.write(data) + + tmp_objdir = os.path.join("/responses", task_id) + retcode = 0 + success = False + timeout = False + + # run the AFC Engine + try: + cmd = [ + conf.AFC_ENGINE, + "--request-type=" + request_type, + "--state-root=" + mntroot + "/rat_transfer", + "--mnt-path=" + mntroot, + "--input-file-path=" + analysis_request_path, + "--config-file-path=" + analysis_config_path, + "--output-file-path=" + analysis_response_path, + "--temp-dir=" + tmpdir, + "--log-level=" + conf.AFC_ENGINE_LOG_LVL, + "--runtime_opt=" + str(runtime_opts), + ] + LOGGER.debug(cmd) + retcode = 0 + proc = subprocess.Popen(cmd, stderr=err_file, stdout=log_file) + try: + retcode = proc.wait(timeout=int(conf.AFC_WORKER_ENG_TOUT)) + except Exception as e: + timeout = True + LOGGER.error(f"run(): afc-engine timeout " + f"{conf.AFC_WORKER_ENG_TOUT} error {type(e)}") + raise subprocess.CalledProcessError(retcode, cmd) + if retcode: + raise subprocess.CalledProcessError(retcode, cmd) + success = True + + except subprocess.CalledProcessError as error: + with open(os.path.join(tmpdir, "engine-error.txt"), + encoding="utf-8", errors="replace") as infile: + error_msg = infile.read(1000).strip() + LOGGER.error( + f"run(): afc-engine crashed. Task ID={task_id}, error " + f"message:\n{error_msg}") + try: + if use_tasks: + with dataif.open(os.path.join("/responses", hash_val, + "analysisRequest.json")) \ + as hfile: + request_str = \ + hfile.read().decode("utf-8", errors="replace") + with dataif.open(config_path) as hfile: + config_str = \ + hfile.read().decode("utf-8", errors="replace") + als.als_initialize() + als.als_json_log("afc_engine_crash", + {"task_id": task_id, "error_msg": error_msg, + "timeout": timeout, + "request": json.loads(request_str), + "config": json.loads(config_str)}) + except Exception as ex: + LOGGER.error( + f"Failed to make ALS report on engine crash: {ex}") + else: + LOGGER.info('finished with task computation') + + proc = None + log_file.close() + err_file.close() + + if not use_tasks: + try: + with open(analysis_response_path, "rb") as infile: + response_gz = infile.read() + except OSError: + response_gz = None + response_str = \ + zlib.decompress(response_gz, + 16 + zlib.MAX_WBITS).decode("utf-8") \ + if success and response_gz else None + rcache.rmq_send_response( + queue_name=rcache_queue, req_cfg_digest=hash_val, + request=request_str, response=response_str) + + if runtime_opts & defs.RNTM_OPT_GUI: + for fname in ("results.kmz", "mapData.json.gz"): + # copy if generated + if os.path.exists(os.path.join(tmpdir, fname)): + with dataif.open(os.path.join(tmp_objdir, fname)) as hfile: + with open(os.path.join(tmpdir, fname), "rb") as infile: + hfile.write(infile.read()) + + # copy contents of temporary directory to history directory + if runtime_opts & (defs.RNTM_OPT_DBG | defs.RNTM_OPT_SLOW_DBG): + for fname in os.listdir(tmpdir): + with dataif.open(os.path.join(history_dir, fname)) as hfile: + with open(os.path.join(tmpdir, fname), "rb") as infile: + hfile.write(infile.read()) + + LOGGER.debug('task completed') + if use_tasks: + tsk.toJson( + afctask.Task.STAT_SUCCESS if success + else afctask.Task.STAT_FAILURE, + runtime_opts=runtime_opts, exit_code=retcode) + + except Exception as exc: + raise exc + + finally: + LOGGER.info('Terminating worker') + + # we may be being told to stop worker so we have to terminate C++ code + # if it is running + if proc: + LOGGER.debug('terminating afc-engine') + proc.terminate() + LOGGER.debug('afc-engine terminated') + if os.path.exists(tmpdir): + shutil.rmtree(tmpdir) + LOGGER.info('Worker resources cleaned up') diff --git a/src/afc-packages/afcworker/setup.py b/src/afc-packages/afcworker/setup.py new file mode 100644 index 0000000..e578620 --- /dev/null +++ b/src/afc-packages/afcworker/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup, find_packages +from setuptools.command.install import install +import os + + +class InstallCmdWrapper(install): + def run(self): + install.run(self) + + +setup( + name='afcworker', + # Label compatible with PEP 440 + version='0.1.0', + description='AFC packages', + py_modules=["afc_worker"], + cmdclass={ + 'install': InstallCmdWrapper, + } +) diff --git a/src/afc-packages/als/als.py b/src/afc-packages/als/als.py new file mode 100644 index 0000000..d1c11ec --- /dev/null +++ b/src/afc-packages/als/als.py @@ -0,0 +1,615 @@ +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +""" AFC and JSON logging to PostgreSQL + +This module uses Kafka producer client from Confluent. +Overview-level documentation may be found here: +https://github.com/confluentinc/confluent-kafka-python +API documentation may be found here: +https://docs.confluent.io/platform/current/clients/confluent-kafka-python/html/ +Configuration parameters may be found here: +https://docs.confluent.io/platform/current/installation/configuration/ +and here: +https://docs.confluent.io/platform/current/clients/confluent-kafka-python/\ +html/index.html#kafka-client-configuration +""" + +# pylint: disable=invalid-name, too-many-arguments, global-statement +# pylint: disable=broad-exception-caught + +import collections +import datetime +import json +import logging +import os +import re +import threading +from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union + +LOGGER = logging.getLogger(__name__) + +import_failure: Optional[str] = None +try: + import dateutil.tz + import confluent_kafka +except ImportError as exc: + import_failure = repr(exc) + +# Topic name for AFC request/response logging +ALS_TOPIC = "ALS" +# AFC Config/Request/Response logging record format version +ALS_FORMAT_VERSION = "1.0" +# Data type value for AFC Config record +ALS_DT_CONFIG = "AFC_CONFIG" +# Data type value for AFC Request record +ALS_DT_REQUEST = "AFC_REQUEST" +# Data type value for AFC Response record +ALS_DT_RESPONSE = "AFC_RESPONSE" + +# Version field of AFC Config/Request/Response record +ALS_FIELD_VERSION = "version" +# AFC Server ID field of AFC Config/Request/Response record +ALS_FIELD_SERVER_ID = "afcServer" +# Timetag field of AFC Config/Request/Response record +ALS_FIELD_TIME = "time" +# Data type (ALS_DT_...) field of AFC Config/Request/Response record +ALS_FIELD_DATA_TYPE = "dataType" +# JSON data field of AFC Config/Request/Response record +ALS_FIELD_DATA = "jsonData" +# Customer name field of AFC Config/Request/Response record +ALS_FIELD_CUSTOMER = "customer" +# Geodetic data version field of AFC Config/Request/Response record +ALS_FIELD_GEO_DATA = "geoDataVersion" +# ULS data ID field of AFC Config/Request/Response record +ALS_FIELD_ULS_ID = "ulsId" +# Request indices field of AFC Config/Request/Response record +ALS_FIELD_REQ_INDICES = "requestIndexes" + +# JSON log record format version +JSON_LOG_VERSION = "1.0" + +# Version field of JSON log record +JSON_LOG_FIELD_VERSION = "version" +# AFC Server ID field of JSON log record +JSON_LOG_FIELD_SERVER_ID = "source" +# Timetag field of JSON log record +JSON_LOG_FIELD_TIME = "time" +# JSON data field of JSON log record +JSON_LOG_FIELD_DATA = "jsonData" + +# Once in this number of times Producer.poll() is called (to prevent send +# report accumulation) +POLL_PERIOD = 100 + +# Delay between connection attempts +CONNECT_ATTEMPT_INTERVAL = datetime.timedelta(seconds=10) + +# Regular expression for topic name check (Postgre requirement to table names) +TOPIC_NAME_REGEX = re.compile(r"^[_a-zA-Z][0-9a-zA-Z_]{,62}$") + +# Maximum um message size (default maximum of 1MB is way too small for GUI AFC +# Requests) +MAX_MSG_SIZE = (1 << 20) * 10 + + +def to_bytes(s: Optional[str]) -> Optional[bytes]: + """ Converts string to bytes """ + if s is None: + return None + return s.encode("utf-8") + + +def to_str(s: Optional[Union[bytes, str]]) -> Optional[str]: + """ Converts bytes to string """ + if (s is None) or isinstance(s, str): + return s + return s.decode("utf-8", errors="backslashreplace") + + +def timetag() -> str: + """ Timetag with timezone """ + return datetime.datetime.now(tz=dateutil.tz.tzlocal()).isoformat() + + +def random_hex(n: int) -> str: + """ Generates strongly random n-byte sequence and returns its hexadecimal + representation """ + return "".join(f"{b:02X}" for b in os.urandom(n)) + + +class ArgDsc: + """ KafkaProducer constructor argument descriptor + + Private attributes: + _env_var -- Environment variable containing value for parameter + _arg -- Parameter name + _type_conv -- Function to convert parameter value from string + _list_separator -- None for scalar argument, separator for list argument + _required -- True if this parameter required for KafkaProducer + initialization and its absence will lead to no logging + """ + + def __init__(self, env_var: str, arg: str, + type_conv: Callable[[str], Any] = str, + list_separator: Optional[str] = None, + required: bool = False, default: Any = None) -> None: + """ Constructor + + Arguments: + env_var -- Environment variable containing value for parameter + arg -- Parameter name + type_conv -- Function to convert parameter value from string + list_separator -- None for scalar argument, separator for list argument + required -- True if this parameter required for KafkaProducer + initialization and its absence will lead to no + logging + default -- None or default value + """ + self._env_var = env_var + self._arg = arg + self._type_conv = type_conv + self._list_separator = list_separator + self._required = required + self._default = default + + @property + def env_var(self) -> str: + """ Name of environment variable """ + return self._env_var + + def from_env(self, kwargs: Dict[str, Any]) -> bool: + """ Tries to read argument from environment + + Arguments: + kwargs -- Argument dictionary for KafkaProducer constructor to add + argument to + Returns True if there is no reason to abandon reading arguments from + environment + """ + env_val = os.environ.get(self._env_var) + if not env_val: + if self._default is not None: + kwargs[self._arg] = self._default + return not self._required + kwargs[self._arg] = \ + [self._type_conv(v) for v in env_val.split(self._list_separator)] \ + if self._list_separator else self._type_conv(env_val) + return True + + @classmethod + def str_or_int_type_conv(cls, str_val: str) -> Union[str, int]: + """ Type converter for argument that can be string or integer """ + try: + return int(str_val) + except (TypeError, ValueError): + return str_val + + @classmethod + def bool_type_conv(cls, str_val: str) -> bool: + """ Type converter for boolean argument """ + if str_val.lower() in ("+", "1", "yes", "true", "on"): + return True + if str_val.lower() in ("-", "0", "no", "false", "off"): + return False + raise ValueError("Wrong format of boolean Kafka parameter") + + +# Descriptors of KafkaProducer arguments +arg_dscs: List[ArgDsc] = [ + # Default value for stem part of "client_id" parameter passed to + # KafkaProducer and server ID used in messages + ArgDsc("ALS_KAFKA_SERVER_ID", "client.id"), + # Comma-separated list of Kafka (bootstrap) servers in form of 'hostname' + # or 'hostname:port' (default port is 9092). If not specified ALS logging + # is not performed + ArgDsc("ALS_KAFKA_CLIENT_BOOTSTRAP_SERVERS", "bootstrap.servers", + required=True), + # Number of Kafka confirmations before operation completion. + # Valid values: '0' (fire and forget), '1', 'all') + ArgDsc("ALS_KAFKA_CLIENT_ACKS", "acks", + type_conv=ArgDsc.str_or_int_type_conv, default=1), + # Number of retries. Default is 0 + ArgDsc("ALS_KAFKA_CLIENT_RETRIES", "retries", type_conv=int, default=5), + # Time to wait for batching. Default is 0 (send immediately) + ArgDsc("ALS_KAFKA_CLIENT_LINGER_MS", "linger.ms", type_conv=int), + # Request timeout in milliseconds. Default is 30000 + ArgDsc("ALS_KAFKA_CLIENT_REQUEST_TIMEOUT_MS", "request.timeout.ms", + type_conv=int), + # Maximum number of unconfirmed requests in flight. Default is 5 + ArgDsc("ALS_KAFKA_CLIENT_MAX_UNCONFIRMED_REQS", + "max.in.flight.requests.per.connection", type_conv=int), + # Security protocol: 'PLAINTEXT', 'SSL' (hope we do not need SASL_...). + # Default is 'PLAINTEXT' + ArgDsc("ALS_KAFKA_CLIENT_SECURITY_PROTOCOL", "security.protocol"), + # SSL. CA file for certificate verification + # Maximum message size + ArgDsc("ALS_KAFKA_MAX_REQUEST_SIZE", "message.max.bytes", type_conv=int)] + + +class Als: + """ Kafka client for ALS and JSON logs + + Private attributes: + _server_id -- Unique AFC server identity string. Computed by + appending random suffix to 'client_id' constructor + parameter or (if not specified) to ALS_KAFKA_CLIENT_ID + environment variable value or (if not specified) to + "Unknown" + _producer_kwargs -- Dictionary of parameters for KafkaProducer(). None if + parameters are invalid + _producer -- KafkaProducer. None if initialization was not + successful (yet?) + _config_cache -- None (if configs are not consolidated) or two-level + dictionary req_id-> + (config, customer, geo_version, uls_id)-> + request_indices + _req_idx -- Request message index + _send_count -- Number of sent records + """ + + def __init__(self, client_id: Optional[str] = None, + consolidate_configs: bool = True) -> None: + """ Constructor + + Arguments: + client_id -- Client identity string. Better be unique, but + this is not necessary (as unique random sugffix + is apended anyway). If not specified, value of + ALS_KAFKA_CLIENT_ID is used. If neither + specified, "Unknown" is assumed + consolidate_configs -- False to send configs as they arrive, True to + collect them and then send consolidated + """ + self._producer: Optional[confluent_kafka.Producer] = None + self._config_cache: \ + Optional[Dict[str, Dict[Tuple[str, str, str, str], List[int]]]] = \ + {} if consolidate_configs else None + self._req_idx = 0 + self._lock = threading.Lock() + kwargs: Dict[str, Any] = {} + has_parameters = True + for argdsc in arg_dscs: + try: + if not argdsc.from_env(kwargs): + has_parameters = False + break + except (TypeError, ValueError, LookupError) as ex: + has_parameters = False + LOGGER.error( + "Parameter environment variable '%s' has invalid value of " + "'%s': %s", + argdsc.env_var, os.environ.get(argdsc.env_var), repr(ex)) + break + self._server_id: str = \ + cast(str, (client_id or kwargs.get("client.id", "Unknown"))) + \ + "_" + random_hex(10) + self._send_count = 0 + kwargs["client.id"] = self._server_id + if not has_parameters: + LOGGER.warning( + "Parameters for Kafka ALS server connection not specified. " + "No ALS logging will be performed") + return + if import_failure is not None: + LOGGER.error("Some module(s) not installed: %s. " + "No ALS/JSON logging will be performed", + import_failure) + return + with self._lock: + try: + self._producer = \ + confluent_kafka.Producer(kwargs) + LOGGER.info("Kafka ALS server connection created") + except confluent_kafka.KafkaException as ex: + LOGGER.error( + "Kafka ALS server connection initialization error: %s", + repr(ex.args[0])) + + @property + def initialized(self) -> bool: + """ True if KafkaProducer initialization was successful """ + return self._producer is not None + + def afc_req_id(self) -> str: + """ Returns Als-instance unique request ID """ + with self._lock: + self._req_idx += 1 + return str(self._req_idx) + + def afc_request(self, req_id: str, req: Dict[str, Any]) -> None: + """ Send AFC Request + + Arguments: + req_id -- Unique for this Als object instance request ID string (e.g. + returned by req_id()) + req -- Request message JSON dictionary + """ + if self._producer is None: + return + self._send( + topic=ALS_TOPIC, key=self._als_key(req_id), + value=self._als_value(data_type=ALS_DT_REQUEST, data=req)) + + def afc_response(self, req_id: str, resp: Dict[str, Any]) -> None: + """ Send AFC Response + + Arguments: + req_id -- Unique for this AlS object instance request ID string (e.g. + returned by req_id()) + resp -- Response message as JSON dictionary + """ + if self._producer is None: + return + self._flush_afc_configs(req_id) + self._send( + topic=ALS_TOPIC, key=self._als_key(req_id), + value=self._als_value(data_type=ALS_DT_RESPONSE, data=resp)) + + def afc_config(self, req_id: str, config_text: str, customer: str, + geo_data_version: str, uls_id: str, + req_indices: Optional[list[int]] = None) -> None: + """ Send or cache AFC Config + + Arguments: + req_id -- Unique for this AlS object instance request ID + string (e.g. returned by req_id()) + config_text -- Config file contents + customer -- Customer name + geo_data_version -- Version of Geodetic data + uls_id -- ID of ULS data + req_indices -- List of 0-based indices of individual requests + within message to which this information is + related. None if to all + """ + if self._producer is None: + return + if (self._config_cache is None) or (req_indices is None): + self._send_afc_config( + req_id=req_id, config_text=config_text, customer=customer, + geo_data_version=geo_data_version, uls_id=uls_id, + req_indices=req_indices) + else: + with self._lock: + indices_for_config = \ + self._config_cache.setdefault(req_id, {}).\ + setdefault( + (config_text, customer, geo_data_version, uls_id), []) + indices_for_config += req_indices + + def json_log(self, topic: str, record: Any) -> None: + """ Send JSON log record + + Arguments + topic -- Log type (message format) identifier - name of table to + which this log will be placed + record -- JSON dictionary with record data + """ + if self._producer is None: + return + assert topic != ALS_TOPIC + assert TOPIC_NAME_REGEX.match(topic) + json_dict = \ + collections.OrderedDict( + [(JSON_LOG_FIELD_VERSION, JSON_LOG_VERSION), + (JSON_LOG_FIELD_SERVER_ID, self._server_id), + (JSON_LOG_FIELD_TIME, timetag()), + (JSON_LOG_FIELD_DATA, json.dumps(record))]) + self._send(topic=topic, value=to_bytes(json.dumps(json_dict))) + + def flush(self, timeout_sec: float) -> bool: + """ Flush pending messages (if any) + + Arguments: + timeout_sec -- timeout in seconds, None to wait for completion + Returns True on success, False on timeout + """ + if self._producer is None: + return True + return self._producer.flush(timeout=timeout_sec) == 0 + + def _flush_afc_configs(self, req_id: str) -> None: + """ Send all AFC Config records, collected for given request + + Arguments: + req_id -- Unique for this Als object instance request ID string (e.g. + returned by req_id()) + """ + if self._config_cache is None: + return + with self._lock: + configs = self._config_cache.get(req_id) + if configs is None: + return + del self._config_cache[req_id] + for (config_text, customer, geo_data_version, uls_id), indices \ + in configs.items(): + self._send_afc_config( + req_id=req_id, config_text=config_text, customer=customer, + geo_data_version=geo_data_version, uls_id=uls_id, + req_indices=indices if len(configs) != 1 else None) + + def _send_afc_config(self, req_id: str, config_text: str, customer: str, + geo_data_version: str, uls_id: str, + req_indices: Optional[List[int]]) -> None: + """ Actual AFC Config sending + + Arguments: + req_id -- Unique (on at least server level) request ID string + config_text -- Config file contents + customer -- Customer name + geo_data_version -- Version of Geodetic data + uls_id -- ID of ULS data + req_indices -- List of 0-based indices of individual requests + within message to which this information is + related. None if to all + """ + extra_fields: Dict[str, Any] = \ + collections.OrderedDict( + [(ALS_FIELD_CUSTOMER, customer), + (ALS_FIELD_GEO_DATA, geo_data_version), + (ALS_FIELD_ULS_ID, uls_id)]) + if req_indices: + extra_fields[ALS_FIELD_REQ_INDICES] = req_indices + self._send( + topic=ALS_TOPIC, key=self._als_key(req_id), + value=self._als_value(data_type=ALS_DT_CONFIG, data=config_text, + extra_fields=extra_fields)) + + def _send(self, topic: str, key: Optional[bytes] = None, + value: Optional[bytes] = None) -> None: + """ KafkaProducer's send() method with exceptions caught """ + assert self._producer is not None + self._send_count += 1 + if (self._send_count % POLL_PERIOD) == 0: + try: + self._producer.poll(0) + except confluent_kafka.KafkaException: + pass + try: + self._producer.produce(topic=topic, key=key, value=value) + except confluent_kafka.KafkaException as ex: + LOGGER.error( + "Error sending to topic '%s': %s", topic, + repr(ex.args[0])) + + def _als_key(self, req_id: str) -> bytes: + """ ALS record key for given request ID """ + return cast(bytes, to_bytes(f"{self._server_id}|{req_id}")) + + def _als_value(self, data_type: str, data: Union[str, Dict[str, Any]], + extra_fields: Optional[Dict[str, Any]] = None): + """ ALS record value + + Arguments: + data_type -- Data type string + data -- Data - string or JSON dictionary + extra_fields -- None or dictionary of message-type-specific fields + """ + json_dict = \ + collections.OrderedDict( + [(ALS_FIELD_VERSION, ALS_FORMAT_VERSION), + (ALS_FIELD_SERVER_ID, self._server_id), + (ALS_FIELD_TIME, timetag()), + (ALS_FIELD_DATA_TYPE, data_type), + (ALS_FIELD_DATA, + to_str(data) if isinstance(data, (str, bytes)) + else json.dumps(data))]) + for k, v in (extra_fields or {}).items(): + json_dict[k] = v + return to_bytes(json.dumps(json_dict)) + + +# STATIC INTERFACE FOR THIS MODULE FUNCTIONALITY + +# Als instance creation lock +_als_instance_lock: threading.Lock = threading.Lock() +# Als instance, built after first initialization +_als_instance: Optional[Als] = None + + +def als_initialize(client_id: Optional[str] = None, + consolidate_configs: bool = True) -> None: + """ Initialization + May be called several times, but all nonfirst calls are ignored + + Arguments: + client_id -- Client identity string. Better be unique, but this + is not necessary (as unique random sugffix is + apended anyway). If not specified, value of + ALS_KAFKA_CLIENT_ID is used. If neither specified, + "Unknown" is assumed + consolidate_configs -- False to send configs as they arrive, True to + collect them and then send consolidated + """ + global _als_instance + global _als_instance_lock + if _als_instance is not None: + return + with _als_instance_lock: + if _als_instance is None: + _als_instance = Als(client_id=client_id, + consolidate_configs=consolidate_configs) + + +def als_is_initialized() -> bool: + """ True if ALS was successfully initialized """ + return (_als_instance is not None) and _als_instance.initialized + + +def als_afc_req_id() -> Optional[str]: + """ Returns Als-instance unique request ID """ + return _als_instance.afc_req_id() if _als_instance is not None else None + + +def als_afc_request(req_id: str, req: Dict[str, Any]) -> None: + """ Send AFC Request + + Arguments: + req_id -- Unique (on at least server level) request ID string + req -- Request message JSON dictionary + """ + if (_als_instance is not None) and (req_id is not None): + _als_instance.afc_request(req_id, req) + + +def als_afc_response(req_id: str, resp: Dict[str, Any]) -> None: + """ Send AFC Response + + Arguments: + req_id -- Unique (on at least server level) request ID string + req -- Response message JSON dictionary + """ + if (_als_instance is not None) and (req_id is not None): + _als_instance.afc_response(req_id, resp) + + +def als_afc_config(req_id: str, config_text: str, customer: str, + geo_data_version: str, uls_id: str, + req_indices: Optional[List[int]] = None) -> None: + """ Send AFC Config + + Arguments: + req_id -- Unique (on at least server level) request ID string + config_text -- Config file contents + customer -- Customer name + geo_data_version -- Version of Geodetic data + uls_id -- ID of ULS data + req_indices -- List of 0-based indices of individual requests within + message to which this information is related. None if + to all + """ + if (_als_instance is not None) and (req_id is not None): + _als_instance.afc_config(req_id, config_text, customer, + geo_data_version, uls_id, req_indices) + + +def als_json_log(topic: str, record: Any): + """ Send JSON log record + + Arguments + topic -- Log type (message format) identifier - name of table to which + this log will be placed + record -- JSON dictionary with record data + """ + if _als_instance is not None: + _als_instance.json_log(topic, record) + + +def als_flush(timeout_sec: float = 2) -> bool: + """ Flush pending messages (if any) + + Usually it is not needed, but if last log ALS/JSON write was made + immediately before program exit, some records might become lost. + Hence it might be beneficial to call this function before script end + + Arguments: + timeout_sec -- timeout in seconds, None to wait for completion + Returns True on success, False on timeout + """ + if _als_instance is not None: + return _als_instance.flush(timeout_sec=timeout_sec) + return True diff --git a/src/afc-packages/als/setup.py b/src/afc-packages/als/setup.py new file mode 100644 index 0000000..e737c8c --- /dev/null +++ b/src/afc-packages/als/setup.py @@ -0,0 +1,26 @@ +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +from setuptools import setup, find_packages +from setuptools.command.install import install +import os + + +class InstallCmdWrapper(install): + def run(self): + install.run(self) + + +setup( + name='als', + # Label compatible with PEP 440 + version='0.1.0', + description='ALS Client module', + py_modules=["als"], + cmdclass={ + 'install': InstallCmdWrapper, + } +) diff --git a/src/afc-packages/appconfig/appcfg.py b/src/afc-packages/appconfig/appcfg.py new file mode 100644 index 0000000..2131556 --- /dev/null +++ b/src/afc-packages/appconfig/appcfg.py @@ -0,0 +1,315 @@ +# This Python file uses the following encoding: utf-8 +# +# Portions copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate +# affiliate that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy +# of which is included with this software program. +# + +''' Application configuration data. +''' +import abc +import os +import logging +import datetime + +#: The externally-visible script root path +APPLICATION_ROOT = '/fbrat' +#: Enable debug mode for flask +DEBUG = False +#: Enable detailed exception stack messages +PROPAGATE_EXCEPTIONS = False +#: Root logger filter +AFC_RATAPI_LOG_LEVEL = os.getenv("AFC_RATAPI_LOG_LEVEL", "WARNING") +#: Set of log handlers to use for root logger +LOG_HANDLERS = [ + logging.StreamHandler(), +] + +SECRET_KEY = None # Must be set in app config + +# Flask-SQLAlchemy settings +# postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...] +SQLALCHEMY_DATABASE_URI = None # Must be set in app config +SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoids SQLAlchemy warning + +# Flask-User settings +USER_APP_NAME = "AFC" # Shown in and email templates and page footers +USER_ENABLE_EMAIL = True # Enable email authentication +USER_ENABLE_CONFIRM_EMAIL = False # Disable email confirmation +USER_ENABLE_USERNAME = True # Enable username authentication +USER_EMAIL_SENDER_NAME = USER_APP_NAME +USER_EMAIL_SENDER_EMAIL = None +REMEMBER_COOKIE_DURATION = datetime.timedelta(days=30) # remember me timeout +USER_USER_SESSION_EXPIRATION = 3600 # One hour idle timeout +PERMANENT_SESSION_LIFETIME = datetime.timedelta( + seconds=USER_USER_SESSION_EXPIRATION) # idle session timeout +USER_LOGIN_TEMPLATE = 'login.html' +USER_REGISTER_TEMPLATE = 'register.html' + +#: API key used for google maps +GOOGLE_APIKEY = None +#: Dynamic system data (both model data and configuration) +STATE_ROOT_PATH = '/var/lib/fbrat' +#: Mount path +NFS_MOUNT_PATH = '/mnt/nfs' +#: Use random PAWS response flag +PAWS_RANDOM = False +#: History directory for log file storage +HISTORY_DIR = None +#: Task queue directory +TASK_QUEUE = '/var/spool/fbrat' + +#: Tracks if the daily uls parser ran today. Overwritten by the tasks that use it. +DAILY_ULS_RAN_TODAY = False + + +class InvalidEnvVar(Exception): + """Wrong/missing env var exception""" + pass + + +class BrokerConfigurator(object): + """Keep configuration for a broker""" + + def __init__(self) -> None: + self.BROKER_PROT = os.getenv('BROKER_PROT', 'amqp') + self.BROKER_USER = os.getenv('BROKER_USER', 'celery') + self.BROKER_PWD = os.getenv('BROKER_PWD', 'celery') + self.BROKER_FQDN = os.getenv('BROKER_FQDN', 'rmq') + self.BROKER_PORT = os.getenv('BROKER_PORT', '5672') + self.BROKER_VHOST = os.getenv('BROKER_VHOST', 'fbrat') + self.BROKER_URL = self.BROKER_PROT +\ + "://" +\ + self.BROKER_USER +\ + ":" +\ + self.BROKER_PWD +\ + "@" +\ + self.BROKER_FQDN +\ + ":" +\ + self.BROKER_PORT +\ + "/" +\ + self.BROKER_VHOST + self.BROKER_EXCH_DISPAT = os.getenv( + 'BROKER_EXCH_DISPAT', 'dispatcher_bcast') + + +class ObjstConfigBase(): + """Parent of configuration for objstorage""" + + def __init__(self): + self.AFC_OBJST_PORT = os.getenv("AFC_OBJST_PORT", "5000") + if not self.AFC_OBJST_PORT.isdigit(): + raise InvalidEnvVar("Invalid AFC_OBJST_PORT env var.") + self.AFC_OBJST_HIST_PORT = os.getenv("AFC_OBJST_HIST_PORT", "4999") + if not self.AFC_OBJST_HIST_PORT.isdigit(): + raise InvalidEnvVar("Invalid AFC_OBJST_HIST_PORT env var.") + + +class ObjstConfig(ObjstConfigBase): + """Filestorage external config""" + + def __init__(self): + ObjstConfigBase.__init__(self) + self.AFC_OBJST_HOST = os.getenv("AFC_OBJST_HOST") + + self.AFC_OBJST_SCHEME = None + if "AFC_OBJST_SCHEME" in os.environ: + self.AFC_OBJST_SCHEME = os.environ["AFC_OBJST_SCHEME"] + if self.AFC_OBJST_SCHEME not in ("HTTPS", "HTTP"): + raise InvalidEnvVar("Invalid AFC_OBJST_SCHEME env var.") + + +class SecretConfigurator(object): + + def __init__(self, secret_env, bool_attr, str_attr, int_attr): + attr = bool_attr + str_attr + int_attr + + # Initialize to false and empty + for k in bool_attr: + setattr(self, k, False) + + for k in str_attr: + setattr(self, k, "") + + for k in int_attr: + setattr(self, k, 0) + + # load priv config if available. + try: + from ratapi import priv_config + for k in attr: + val = getattr(priv_config, k, None) + if val is not None: + setattr(self, k, val) + except BaseException: + priv_config = None + + # override boolean config with environment variables + for k in bool_attr: + # Override with environment variables + ret = os.getenv(k) + if ret: + setattr(self, k, (ret.lower() == 'true')) + + # override string config with environment variables + for k in str_attr: + ret = os.getenv(k) + if ret: + setattr(self, k, ret) + + # override int config with environment variables + for k in int_attr: + ret = os.getenv(k) + if ret: + setattr(self, k, int(ret)) + + # Override values from config with secret file + secret_file = os.getenv(secret_env) + if secret_file: + import json + with open(secret_file) as secret_content: + data = json.load(secret_content) + for k in bool_attr: + if k in data: + setattr(self, k, data[k].lower() == 'true') + for k in str_attr: + if k in data: + setattr(self, k, data[k]) + for k in int_attr: + if k in data: + setattr(self, k, int(data[k])) + + +class OIDCConfigurator(SecretConfigurator): + def __init__(self): + oidc_bool_attr = ['OIDC_LOGIN'] + oidc_str_attr = ['OIDC_CLIENT_ID', + 'OIDC_CLIENT_SECRET', 'OIDC_DISCOVERY_URL'] + oidc_env = 'OIDC_ARG' + super().__init__(oidc_env, oidc_bool_attr, oidc_str_attr, []) + + +class RatApiConfigurator(SecretConfigurator): + def __init__(self): + ratapi_bool_attr = ['MAIL_USE_TLS', 'MAIL_USE_SSL', 'USE_CAPTCHA'] + ratapi_str_attr = [ + 'REGISTRATION_APPROVE_LINK', + 'REGISTRATION_DEST_EMAIL', + 'REGISTRATION_DEST_PDL_EMAIL', + 'REGISTRATION_SRC_EMAIL', + 'MAIL_PASSWORD', + 'MAIL_USERNAME', + 'MAIL_SERVER', + 'CAPTCHA_SECRET', + 'CAPTCHA_SITEKEY', + 'CAPTCHA_VERIFY', + 'USER_APP_NAME'] + ratapi_int_attr = ['MAIL_PORT'] + ratapi_env = 'RATAPI_ARG' + super().__init__(ratapi_env, ratapi_bool_attr, ratapi_str_attr, ratapi_int_attr) + + +# Msghnd configuration interfaces + +class MsghndConfiguration(abc.ABC): + @abc.abstractmethod + def get_name(self): + # AFC_MSGHND_NAME + pass + + @abc.abstractmethod + def get_port(self): + # AFC_MSGHND_PORT + pass + + @abc.abstractmethod + def get_bind(self): + # AFC_MSGHND_BIND + pass + + @abc.abstractmethod + def get_access_log(self): + # AFC_MSGHND_ACCESS_LOG + pass + + @abc.abstractmethod + def get_error_log(self): + # AFC_MSGHND_ERROR_LOG + pass + + @abc.abstractmethod + def get_workers(self): + # AFC_MSGHND_WORKERS + pass + + @abc.abstractmethod + def get_timeout(self): + # AFC_MSGHND_TIMEOUT + pass + + @abc.abstractmethod + def get_ratafc_tout(self): + # AFC_MSGHND_RATAFC_TOUT + pass + + +class HealthchecksMsghndCfgIface(MsghndConfiguration): + def __init__(self): + setattr(self, 'AFC_MSGHND_NAME', os.getenv('AFC_MSGHND_NAME')) + setattr(self, 'AFC_MSGHND_PORT', os.getenv('AFC_MSGHND_PORT')) + + def get_name(self): + return self.AFC_MSGHND_NAME + + def get_port(self): + return self.AFC_MSGHND_PORT + + def get_bind(self): + pass + + def get_access_log(self): + pass + + def get_error_log(self): + pass + + def get_workers(self): + pass + + def get_timeout(self): + pass + + def get_ratafc_tout(self): + pass + + +class RatafcMsghndCfgIface(MsghndConfiguration): + def __init__(self): + setattr(self, 'AFC_MSGHND_RATAFC_TOUT', + os.getenv('AFC_MSGHND_RATAFC_TOUT')) + + def get_name(self): + pass + + def get_port(self): + pass + + def get_bind(self): + pass + + def get_access_log(self): + pass + + def get_error_log(self): + pass + + def get_workers(self): + pass + + def get_timeout(self): + pass + + def get_ratafc_tout(self): + return int(self.AFC_MSGHND_RATAFC_TOUT) diff --git a/src/afc-packages/appconfig/setup.py b/src/afc-packages/appconfig/setup.py new file mode 100644 index 0000000..1b7fe99 --- /dev/null +++ b/src/afc-packages/appconfig/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup, find_packages +from setuptools.command.install import install +import os + + +class InstallCmdWrapper(install): + def run(self): + install.run(self) + + +setup( + name='appconfig', + # Label compatible with PEP 440 + version='0.1.0', + description='AFC packages', + py_modules=["appcfg"], + cmdclass={ + 'install': InstallCmdWrapper, + } +) diff --git a/src/afc-packages/defs/defs.py b/src/afc-packages/defs/defs.py new file mode 100644 index 0000000..07ce3ba --- /dev/null +++ b/src/afc-packages/defs/defs.py @@ -0,0 +1,21 @@ +# +# Copyright © 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# These individual bits corresponds to RUNTIME_OPT_... bits in AfcManager.cpp +# Please keep definitions synchronous +RNTM_OPT_DBG = 1 +RNTM_OPT_GUI = 2 +RNTM_OPT_AFCENGINE_HTTP_IO = 4 +RNTM_OPT_NOCACHE = 8 +RNTM_OPT_SLOW_DBG = 16 + +RNTM_OPT_NODBG_NOGUI = 0 +RNTM_OPT_DBG_NOGUI = RNTM_OPT_DBG +RNTM_OPT_NODBG_GUI = RNTM_OPT_GUI +RNTM_OPT_DBG_GUI = (RNTM_OPT_DBG | RNTM_OPT_GUI) + +RNTM_OPT_CERT_ID = 32 diff --git a/src/afc-packages/defs/setup.py b/src/afc-packages/defs/setup.py new file mode 100644 index 0000000..8efc643 --- /dev/null +++ b/src/afc-packages/defs/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup, find_packages +from setuptools.command.install import install +import os + + +class InstallCmdWrapper(install): + def run(self): + install.run(self) + + +setup( + name='defs', + # Label compatible with PEP 440 + version='0.1.0', + description='AFC packages', + py_modules=["defs"], + cmdclass={ + 'install': InstallCmdWrapper, + } +) diff --git a/src/afc-packages/fstorage/fst.py b/src/afc-packages/fstorage/fst.py new file mode 100644 index 0000000..afa5213 --- /dev/null +++ b/src/afc-packages/fstorage/fst.py @@ -0,0 +1,160 @@ +# coding=utf-8 + +# Copyright © 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +""" +Provides wrappers for RATAPI file operations +""" + +import abc +import os +import inspect +import logging +import requests +from appcfg import ObjstConfig + +app_log = logging.getLogger(__name__) +conf = ObjstConfig() + + +class DataInt: + """ Abstract class for data prot operations """ + __metaclass__ = abc.ABCMeta + + def __init__(self, file_name): + self._file_name = file_name + + @abc.abstractmethod + def write(self, data): + pass + + @abc.abstractmethod + def read(self): + pass + + @abc.abstractmethod + def head(self): + pass + + @abc.abstractmethod + def delete(self): + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + pass + + +class DataIntHttp(DataInt): + """ Data prot operations for the HTTP server prot """ + + def write(self, data): + """ write data to prot """ + app_log.debug("DataIntHttp.write({})".format(self._file_name)) + r = requests.post(self._file_name, data=data) + if not r.ok: + raise Exception("Cant post file") + + def read(self): + """ read data from prot """ + app_log.debug("DataIntHttp.read({})".format(self._file_name)) + r = requests.get(self._file_name, stream=True) + if r.ok: + r.raw.decode_content = False + return r.raw.read() + raise Exception("Cant get file") + + def head(self): + """ is data exist in prot """ + app_log.debug("DataIntHttp.exists({})".format(self._file_name)) + r = requests.head(self._file_name) + return r.ok + + def delete(self): + """ remove data from prot """ + app_log.debug("DataIntHttp.delete({})".format(self._file_name)) + requests.delete(self._file_name) + + +class DataIfBaseV1(): + """ Object storage access """ + HTTP = "HTTP" + HTTPS = "HTTPS" + + # HTTPS connection timeout before falling to HTTP + HTTPS_TIMEOUT = 0.5 + + def __init__(self): + assert self._host is not None, "Missing host" + assert self._port is not None, "Missing port" + assert self._prot in (self.HTTP, self.HTTPS), "Wrong or missing scheme" + self._pref = self._prot + "://" + \ + self._host + ":" + str(self._port) + "/" + + def open(self, r_name): + """ Create FileInt instance """ + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + app_log.debug("DataIfBaseV1.open({})".format(r_name)) + return DataIntHttp(r_name) + + def healthcheck(self): + """ Call healthcheck """ + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + app_log.debug("DataIfBaseV1.healthcheck()") + return requests.get(self._pref + '/healthy') + + @staticmethod + def httpsProbe(host, port): + if not host or not port: + raise Exception("Missing host:port") + url = "https://" + host + ":" + str(port) + "/" + try: + requests.head(url, timeout=self.HTTPS_TIMEOUT) + except requests.exceptions.ConnectionError: # fall to http + app_log.debug("httpsProbe() fall to HTTP") + return False + # use https + app_log.debug("httpsProbe() HTTPS ok") + return True + + +class DataIf(DataIfBaseV1): + """ Wrappers for RATAPI data operations """ + + def __init__(self, prot=None, host=None, port=None): + # Assign default args from env vars + self._host = host + if self._host is None: + self._host = conf.AFC_OBJST_HOST + + self._port = port + if self._port is None: + self._port = conf.AFC_OBJST_PORT + + self._prot = prot + if self._prot is None: + self._prot = conf.AFC_OBJST_SCHEME + + DataIfBaseV1.__init__(self) + + app_log.debug("DataIf.__init__: prot={} host={} port={} _pref={}" + .format(self._prot, self._host, self._port, self._pref)) + + def rname(self, baseName): + """ Return remote file name by basename """ + return self._pref + baseName + + def open(self, baseName): + """ Create FileInt instance """ + return DataIfBaseV1.open(self, self.rname(baseName)) + + def getProtocol(self): + return self._prot, self._host, self._port + +# vim: sw=4:et:tw=80:cc=+1 diff --git a/src/afc-packages/fstorage/setup.py b/src/afc-packages/fstorage/setup.py new file mode 100644 index 0000000..697fa82 --- /dev/null +++ b/src/afc-packages/fstorage/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup, find_packages +from setuptools.command.install import install +import os + + +class InstallCmdWrapper(install): + def run(self): + install.run(self) + + +setup( + name='fstorage', + # Label compatible with PEP 440 + version='0.1.0', + description='AFC packages', + py_modules=["fst"], + cmdclass={ + 'install': InstallCmdWrapper, + } +) diff --git a/src/afc-packages/healthchecks/hchecks.py b/src/afc-packages/healthchecks/hchecks.py new file mode 100755 index 0000000..d875819 --- /dev/null +++ b/src/afc-packages/healthchecks/hchecks.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +""" +Description + +test +""" + +import os +import inspect +import logging +import sys +import requests +import socket +from appcfg import HealthchecksMsghndCfgIface +from kombu import Connection + +app_log = logging.getLogger(__name__) + + +class BasicHealthcheck(): + def __init__(self) -> None: + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + self.url = '' + + def healthcheck(self) -> int: + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()" + f" url: {self.url}") + result = 0 + try: + rawresp = requests.get(self.url) + except Exception as e: + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()" + f" exception: {type(e).__name__}") + result = 1 + else: + if rawresp.status_code != 200: + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()" + f" result: {rawresp}") + result = 1 + return result + + +class ObjstHealthcheck(BasicHealthcheck): + """ Provide basic healthcheck for Objst """ + + def __init__(self, cfg) -> None: + app_log.debug(f"({os.getpid()}) {self.__class__.__name__}()") + self.url = 'http://' + cfg['AFC_OBJST_HOST'] + ':' + \ + cfg['AFC_OBJST_PORT'] + '/healthy' + + +class MsghndHealthcheck(BasicHealthcheck): + """ Provide basic healthcheck for Msghnd """ + + def __init__(self, cfg: HealthchecksMsghndCfgIface) -> None: + app_log.debug(f"({os.getpid()}) {self.__class__.__name__}()") + self.url = 'http://' + cfg.get_name() + ':' + \ + cfg.get_port() + '/fbrat/ap-afc/healthy' + + @classmethod + def from_hcheck_if(cls) -> None: + app_log.debug(f"({os.getpid()}) {cls.__name__}()") + return cls(HealthchecksMsghndCfgIface()) + + +class RmqHealthcheck(): + """ Make basic connection for healthcheck """ + + def __init__(self, broker_url) -> None: + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + self.connection = Connection(broker_url) + + def healthcheck(self) -> int: + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + conn = self.connection + res = False + try: + conn.connect() + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()" + f" ip: {socket.gethostbyname(socket.gethostname())}") + except Exception as e: + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()" + f" exception {type(e).__name__}") + return res + is_conn = conn.connection.connected + self.connection.close() + if is_conn: + return 0 + return 1 + + +# Local Variables: +# mode: Python +# indent-tabs-mode: nil +# python-indent: 4 +# End: +# +# vim: sw=4:et:tw=80:cc=+1 diff --git a/src/afc-packages/healthchecks/setup.py b/src/afc-packages/healthchecks/setup.py new file mode 100644 index 0000000..56c2f1a --- /dev/null +++ b/src/afc-packages/healthchecks/setup.py @@ -0,0 +1,22 @@ +from setuptools import setup, find_packages +from setuptools.command.install import install +import os +import inspect + + +class InstallCmdWrapper(install): + def run(self): + print(f"{inspect.stack()[0][3]}()") + install.run(self) + + +setup( + name='healthchecks', + # Label compatible with PEP 440 + version='0.1.0', + description='AFC packages', + py_modules=["hchecks"], + cmdclass={ + 'install': InstallCmdWrapper, + } +) diff --git a/src/afc-packages/netcli/ncli.py b/src/afc-packages/netcli/ncli.py new file mode 100755 index 0000000..ac8cee1 --- /dev/null +++ b/src/afc-packages/netcli/ncli.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +""" +Description + +test +""" + +import os +import inspect +import logging +import sys +import uuid +from kombu.mixins import ConsumerMixin +from kombu import Queue, Exchange, Connection, Producer + +app_log = logging.getLogger(__name__) + + +class MsgAcceptor(ConsumerMixin): + """ Accept messages from broadcast queue and handle them. """ + message_handler = None + handler_params = None + + def __init__(self, broker_url, broker_exch, + msg_handler=None, handler_params=None) -> None: + app_log.debug(f"({os.getpid()}) {self.__class__.__name__}()") + self.connection = Connection(broker_url) + if msg_handler is not None: + self.message_handler = msg_handler + self.handler_params = handler_params + self.exchange = Exchange( + broker_exch, + auto_delete=True, + type='fanout', durable=True) + self.queue = Queue('dispatcher_' + str(uuid.uuid4().int)[:7], + self.exchange, exclusive=True) + self.queue.no_ack = True + + def get_consumers(self, Consumer, channel): + return [Consumer(queues=self.queue, callbacks=[self.accept_message])] + + def accept_message(self, body, message): + if self.message_handler: + self.message_handler(self.handler_params, body) + + +class MsgPublisher(): + """ Connection maker """ + + def __init__(self, broker_url, broker_exch) -> None: + app_log.debug(f"({os.getpid()}) {self.__class__.__name__}()") + self.connection = Connection(broker_url) + self.exchange = Exchange( + broker_exch, + auto_delete=True, + type='fanout', durable=True) + self.channel = self.connection.channel() + self.producer = Producer(exchange=self.exchange, + channel=self.channel) + + def publish(self, msg): + self.producer.publish(msg) + + def close(self): + self.connection.release() + + +# Local Variables: +# mode: Python +# indent-tabs-mode: nil +# python-indent: 4 +# End: +# +# vim: sw=4:et:tw=80:cc=+1 diff --git a/src/afc-packages/netcli/setup.py b/src/afc-packages/netcli/setup.py new file mode 100644 index 0000000..2bccd03 --- /dev/null +++ b/src/afc-packages/netcli/setup.py @@ -0,0 +1,22 @@ +from setuptools import setup, find_packages +from setuptools.command.install import install +import os +import inspect + + +class InstallCmdWrapper(install): + def run(self): + print(f"{inspect.stack()[0][3]}()") + install.run(self) + + +setup( + name='netcli', + # Label compatible with PEP 440 + version='0.1.0', + description='AFC packages', + py_modules=["ncli"], + cmdclass={ + 'install': InstallCmdWrapper, + } +) diff --git a/src/afc-packages/pkgs.cert_db b/src/afc-packages/pkgs.cert_db new file mode 100644 index 0000000..6392695 --- /dev/null +++ b/src/afc-packages/pkgs.cert_db @@ -0,0 +1,10 @@ +/wd/afc-packages/fstorage +/wd/afc-packages/appconfig +/wd/afc-packages/afctask +/wd/afc-packages/defs +/wd/afc-packages/afcworker +/wd/afc-packages/netcli +/wd/afc-packages/afcmodels +/wd/afc-packages/als +/wd/afc-packages/rcache +/wd/afc-packages/healthchecks diff --git a/src/afc-packages/pkgs.dispatcher b/src/afc-packages/pkgs.dispatcher new file mode 100644 index 0000000..196f057 --- /dev/null +++ b/src/afc-packages/pkgs.dispatcher @@ -0,0 +1,4 @@ +/wd/afc-packages/fstorage +/wd/afc-packages/appconfig +/wd/afc-packages/netcli +/wd/afc-packages/healthchecks diff --git a/src/afc-packages/pkgs.msghnd b/src/afc-packages/pkgs.msghnd new file mode 100644 index 0000000..ed4dc9e --- /dev/null +++ b/src/afc-packages/pkgs.msghnd @@ -0,0 +1,11 @@ +/wd/afc-packages/fstorage +/wd/afc-packages/appconfig +/wd/afc-packages/afctask +/wd/afc-packages/defs +/wd/afc-packages/afcworker +/wd/afc-packages/netcli +/wd/afc-packages/afcmodels +/wd/afc-packages/rcache +/wd/afc-packages/healthchecks +/wd/afc-packages/als +/wd/afc-packages/prometheus_utils diff --git a/src/afc-packages/pkgs.objstorage b/src/afc-packages/pkgs.objstorage new file mode 100644 index 0000000..9914f0e --- /dev/null +++ b/src/afc-packages/pkgs.objstorage @@ -0,0 +1,2 @@ +/wd/afc-packages/appconfig +/wd/afc-packages/afcobjst diff --git a/src/afc-packages/pkgs.rat_server b/src/afc-packages/pkgs.rat_server new file mode 100644 index 0000000..ed4dc9e --- /dev/null +++ b/src/afc-packages/pkgs.rat_server @@ -0,0 +1,11 @@ +/wd/afc-packages/fstorage +/wd/afc-packages/appconfig +/wd/afc-packages/afctask +/wd/afc-packages/defs +/wd/afc-packages/afcworker +/wd/afc-packages/netcli +/wd/afc-packages/afcmodels +/wd/afc-packages/rcache +/wd/afc-packages/healthchecks +/wd/afc-packages/als +/wd/afc-packages/prometheus_utils diff --git a/src/afc-packages/pkgs.rcache b/src/afc-packages/pkgs.rcache new file mode 100644 index 0000000..4a9eac4 --- /dev/null +++ b/src/afc-packages/pkgs.rcache @@ -0,0 +1,6 @@ +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. +/wd/afc-packages/rcache diff --git a/src/afc-packages/pkgs.uls b/src/afc-packages/pkgs.uls new file mode 100644 index 0000000..4a9eac4 --- /dev/null +++ b/src/afc-packages/pkgs.uls @@ -0,0 +1,6 @@ +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. +/wd/afc-packages/rcache diff --git a/src/afc-packages/pkgs.worker b/src/afc-packages/pkgs.worker new file mode 100644 index 0000000..6b28799 --- /dev/null +++ b/src/afc-packages/pkgs.worker @@ -0,0 +1,8 @@ +/wd/afc-packages/fstorage +/wd/afc-packages/appconfig +/wd/afc-packages/afctask +/wd/afc-packages/defs +/wd/afc-packages/afcworker +/wd/afc-packages/netcli +/wd/afc-packages/rcache +/wd/afc-packages/als diff --git a/src/afc-packages/prometheus_utils/prometheus_utils.py b/src/afc-packages/prometheus_utils/prometheus_utils.py new file mode 100644 index 0000000..d99c293 --- /dev/null +++ b/src/afc-packages/prometheus_utils/prometheus_utils.py @@ -0,0 +1,284 @@ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +""" Prometheus client utility stuff for Flask + +This module defines a couple of wrappers around some stuff, defined in +prometheus_client Python module. Specifically: + +- PrometheusTimeBase class for time measurements +- multiprocess_prometheus_configured() and multiprocess_flask_metrics() for + defining Prometheus metrics endpoint in multiprocess Flask application (see + 'SETUP' section below) + +PrometheusTimeBase is a base class for defining Summary (mapped to Prometheus +'counter' data type) for measuring time spent inside code (at function level +one can use prometheus_client.Summary directly). + +PrometheusTimeBase is a base class. Each derived class defines a single metric +with following labels: +- type -- can be 'cpu' or 'wallclock' for measuring CPU and wallclock time + spent respectively. Any may be individually disabled if deemed + costly +- scope -- name of code span being measured +- segment -- name of segment within scope. This labes was introduced to avoid + moving code blocks right/left multiple times (see examples below) + +Metric derived class must have 'metric' static member. Here is how to define +metric 'foo': + class FooMetric(prometheus_utils.PrometheusTimeBase): + metric = \ + prometheus_utils.PrometheusTimeBase.make_metric( + "foo", "Description of 'foo'") + +Metric usage. +Context style: + import prometheus_utils + ... + def bar(): + ... + with FooMetric("bar_scope1") as foo_metric: + ... + foo_metric.seg_end("s1") + ... + foo_metric.seg_end("s2") + ... + ... + ... + ... + with FooMetric("bar_scope2"): + ... + ... + +Call style (more awkward, but does not require moving code left/right at all): + import prometheus_utils + ... + def bar(): + ... + foo_metric1 = FooMetric("bar_scope1") + foo_metric1.start() + ... + foo_metric.seg_end("s1") + ... + foo_metric.seg_end("s2") + ... + foo_metric1.stop() + ... + ... + ... + foo_metric2 = FooMetric("bar_scope2", start=True) + ... + foo_metric2.stop() + ... + +Code above will create following time series: +- Time spent in the whole 'bar_scope1': + - foo_sum{type="cpu", scope="bar_scope1", segment="entire"} + - foo_sum{type="wallclock", scope="bar_scope1", segment="entire"} +- Time spent in in first segment of 'bar_scope1': + - foo_sum{type="cpu", scope="bar_scope1", segment="s1"} + - foo_sum{type="wallclock", scope="bar_scope1", segment="s1"} +- Time spent in in second segment of 'bar_scope1': + - foo_sum{type="cpu", scope="bar_scope1", segment="s2"} + - foo_sum{type="wallclock", scope="bar_scope1", segment="s2"} +- Time spent in in last segment of 'bar_scope1': + - foo_sum{type="cpu", scope="bar_scope1", segment="tail"} + - foo_sum{type="wallclock", scope="bar_scope1", segment="tail"} +- Time spent in the whole 'bar_scope2': + - foo_sum{type="cpu", scope="bar_scope2", segment="entire"} + - foo_sum{type="wallclock", scope="bar_scope2", segment="entire"} +- Number of times correspondent codespans were executed: + - foo_count - same labels as above + + +Defining Flask metrics endpoint. Thsi example is, in fact, not good, as +/metrics endpoint better be defined at top level (not in blueprint), but as of +time of this writing I do not know how to achieve this and anyway achieving +this has nothing to do with Prometheus. So use '__metrics_path__' in Prometheus +target definition +... +module = flask.Blueprint(...) +... +class PrometheusMetrics(MethodView): + def get(self): + return prometheus_utils.multiprocess_flask_metrics() +... +if prometheus_utils.multiprocess prometheus_configured(): + module.add_url_rule( + '/metrics', view_func=PrometheusMetrics.as_view('PrometheusMetrics')) + + +SETUP + +For Prometheus client to work properly in multiprocess Flask applivation the +following things need to be done +- 'PROMETHEUS_MULTIPROC_DIR' environment variable must be defined and point to + empty directory +- Gunicorn should be given a setup file (--config in gunicorn command line) + with following stuff present: + ... + import prometheus_client.multiprocess + ... + def child_exit(server, worker): + ... + prometheus_client.multiprocess.mark_process_dead(worker.pid) + ... +""" + +# pylint: disable=too-many-instance-attributes, wrong-import-order +# pylint: disable=invalid-name + +import os +try: + import flask +except ImportError: + pass +import prometheus_client +import prometheus_client.multiprocess +import prometheus_client.core +import sys +import time +from typing import Optional + + +class PrometheusTimeBase: + """ Abstract base class for Summary-based metrics. + + Derived classes must define static 'metric' static field, initialized with + make_metric(). This field defines metric name + + Private attributes: + _metric -- Metric name + _scope -- Name of scope ('scope' label in generated + time series) + _no_cpu -- True to not generate 'type="cpu"' label + _no_wallclock -- True to not generate 'type="wallclock"' + label + _segmented -- True if there were segments since start (to + generate 'tail' segment time series) + _started -- True if measurement started and not stopped + _cpu_start_ns -- CPU NS counter at scope start + _wallclock_start_ns -- Wallclock NS counter at scope start + _segment_cpu_start_ns -- CPU NS counter at segment start + _segment_wallclock_start_ns -- Wallclock NS counter at segment start + """ + # Segment label value for last segment + TAIL_SEG_NAME = "tail" + # Segment label value for full scope + ENTIRE_SEG_NAME = "entire" + + def __init__(self, scope: str, no_cpu: bool = False, + no_wallclock: bool = False, start: bool = False) -> None: + """ Constructor + + Arguments: + scope -- Segment name + no_cpu -- True to not generate 'type="cpu"' label + no_wallclock -- True to not generate 'type="wallclock"' label + """ + # 'metric' static attribute must be defined in derived class + self._metric = self.__class__.metric # pylint: disable=no-member + self._scope = scope + self._no_cpu = no_cpu + self._no_wallclock = no_wallclock + self._segmented = False + self._started = False + self._cpu_start_ns = 0 + self._wallclock_start_ns = 0 + self._segment_cpu_start_ns = 0 + self._segment_wallclock_start_ns = 0 + if start: + self.start() + + def start(self) -> None: + """ Start scope measurement """ + assert not self._started + self._started = True + self._segmented = False + if not self._no_cpu: + self._cpu_start_ns = self._segment_cpu_start_ns = \ + time.process_time_ns() + if not self._no_wallclock: + self._wallclock_start_ns = self._segment_wallclock_start_ns = \ + time.perf_counter_ns() + + def stop(self) -> None: + """ Ens scope measurement """ + if not self._started: + return + self._started = False + if not self._no_cpu: + ns = time.process_time_ns() + if self._segmented: + self._metric.\ + labels("cpu", self._scope, self.TAIL_SEG_NAME).\ + observe((ns - self._segment_cpu_start_ns) * 1e-9) + self._metric.\ + labels("cpu", self._scope, self.ENTIRE_SEG_NAME).\ + observe((ns - self._cpu_start_ns) * 1e-9) + if not self._no_cpu: + ns = time.perf_counter_ns() + if self._segmented: + self._metric.\ + labels("wallclock", self._scope, self.TAIL_SEG_NAME).\ + observe((ns - self._segment_wallclock_start_ns) * 1e-9) + self._metric.\ + labels("wallclock", self._scope, self.ENTIRE_SEG_NAME).\ + observe((ns - self._wallclock_start_ns) * 1e-9) + + def seg_end(self, seg_name: str) -> None: + """ End of segment within scope + + Arguments: + seg_name -- Segment name + """ + if not self._started: + return + self._segmented = True + if not self._no_cpu: + ns = time.process_time_ns() + self._metric.\ + labels("cpu", self._scope, seg_name).\ + observe((ns - self._segment_cpu_start_ns) * 1e-9) + self._segment_cpu_start_ns = ns + if not self._no_cpu: + ns = time.perf_counter_ns() + self._metric.\ + labels("wallclock", self._scope, seg_name).\ + observe((ns - self._segment_wallclock_start_ns) * 1e-9) + self._segment_wallclock_start_ns = ns + + def __enter__(self) -> "PrometheusTimeBase": + """ 'with' start. Starts scope measurement """ + self.start() + return self + + def __exit__(self, exc_type, exc_value, traceback): + """ 'with' end. Ends scope (and last segment) measurement """ + self.stop() + + @classmethod + def make_metric(cls, name: str, dsc: Optional[str] = None) \ + -> prometheus_client.core.Summary: + """ Returns Summary metrics of given name and description """ + return prometheus_client.core.Summary(name, dsc or name, + ["type", "scope", "segment"]) + + +def multiprocess_prometheus_configured() -> bool: + """ True if multiprocess Priometheus client support was (hopefully) + configured, False if definitely not """ + return os.environ.get("PROMETHEUS_MULTIPROC_DIR") is not None + + +def multiprocess_flask_metrics() -> "flask.Response": + """ Returns return value for 'get()' on metrics endpoint """ + assert "flask" in sys.modules + registry = prometheus_client.CollectorRegistry() + prometheus_client.multiprocess.MultiProcessCollector(registry) + ret = flask.make_response(prometheus_client.generate_latest(registry)) + ret.mimetype = prometheus_client.CONTENT_TYPE_LATEST + return ret diff --git a/src/afc-packages/prometheus_utils/setup.py b/src/afc-packages/prometheus_utils/setup.py new file mode 100644 index 0000000..3a1463e --- /dev/null +++ b/src/afc-packages/prometheus_utils/setup.py @@ -0,0 +1,27 @@ +""" Prometheus cluient utility stuff for Flask """ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +from setuptools import setup +from setuptools.command.install import install + + +class InstallCmdWrapper(install): + def run(self): + install.run(self) + + +setup( + name='prometheus_utils', + # Label compatible with PEP 440 + version='0.1.0', + description='AFC packages', + py_modules=["prometheus_utils"], + cmdclass={ + 'install': InstallCmdWrapper, + } +) diff --git a/src/afc-packages/rcache/rcache_client.py b/src/afc-packages/rcache/rcache_client.py new file mode 100644 index 0000000..f96fee0 --- /dev/null +++ b/src/afc-packages/rcache/rcache_client.py @@ -0,0 +1,184 @@ +""" Synchronous part of AFC Request Cache database stuff """ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# pylint: disable=wrong-import-order, too-many-arguments, invalid-name + +import pydantic +import sys +from typing import Dict, List, Optional, Union + +from rcache_common import error, error_if, get_module_logger, \ + include_stack_to_error_log, set_error_exception + +try: + from rcache_db import RcacheDb +except ImportError: + pass + +try: + from rcache_models import AfcReqRespKey, LatLonRect, RcacheClientSettings + from rcache_rcache import RcacheRcache +except ImportError: + pass + +try: + from rcache_rmq import RcacheRmq, RcacheRmqConnection +except ImportError: + pass + + +__all__ = ["RcacheClient"] + +LOGGER = get_module_logger() + + +class RcacheClient: + """ Response Cache - related code that executes on other containers + + Private attributes: + _rcache_db -- Optional RcacheDb object - for making DB lookups (needed + on Message Handler and RatApi) + _rcache_rmq -- Optional RabbitMQ server AMQP URI (Needed on Message + Handler, RatApi, Worker) + _rmq_receiver -- True if for RabbitMQ receiver, False for transmitter, + None if irrelevant + _rcache_rcache -- Request Cache service base URL. Needed on a side that + makes cache update (i.e. either on Message Handler and + RatApi, or on Worker) or invalidate (FS Update for + spatial invalidate, some other entity for full/config + invalidate) + _update_on_send -- True to update cache on sender + """ + + def __init__(self, client_settings: RcacheClientSettings, + rmq_receiver: Optional[bool] = None) -> None: + """ Constructor + + Arguments: + client_settings -- Rcache client settings + rmq_receiver -- True if sends responses to RabbitMQ, False if + receives responses from RabbitMQ, None if neither + """ + set_error_exception(RuntimeError) + include_stack_to_error_log(True) + + self._rcache_db: Optional[RcacheDb] = None + if client_settings.enabled and \ + (client_settings.postgres_dsn is not None): + assert "rcache_db" in sys.modules + self._rcache_db = RcacheDb(client_settings.postgres_dsn) + + self._rcache_rmq: Optional[RcacheRmq] = None + if client_settings.enabled and \ + (client_settings.rmq_dsn is not None): + assert "rcache_rmq" in sys.modules + error_if(rmq_receiver is None, + "'rmq_receiver' parameter should be specified") + self._rcache_rmq = RcacheRmq(client_settings.rmq_dsn) + self._rmq_receiver = rmq_receiver + self._update_on_send = client_settings.update_on_send + + self._rcache_rcache: Optional[RcacheRcache] = None + if client_settings.enabled and \ + (client_settings.service_url is not None): + assert "rcache_rcache" in sys.modules + self._rcache_rcache = RcacheRcache(client_settings.service_url) + + def lookup_responses(self, req_cfg_digests: List[str]) -> Dict[str, str]: + """ Lookup responses in Postgres cache database + + Arguments: + req_cfg_digests -- List of lookup keys (Request/Config digests in + string form) + Returns dictionary of found responses, indexed by lookup keys + """ + assert self._rcache_db is not None + return self._rcache_db.lookup(req_cfg_digests, try_reconnect=True) + + def rmq_send_response(self, queue_name: str, req_cfg_digest: str, + request: str, response: Optional[str]) \ + -> None: + """ Send AFC response to RabbitMQ + + Arguments: + queue_name -- Name of RabbitMQ queue to send response to + req_cfg_digest -- Request/Config digest (request identifiers) + request -- Request as string + response -- Response as string. None on failure + """ + assert self._rcache_rmq is not None + with self._rcache_rmq.create_connection(tx_queue_name=queue_name) \ + as rmq_conn: + rmq_conn.send_response( + req_cfg_digest=req_cfg_digest, + request=None if self._update_on_send else request, + response=response) + if self._update_on_send and response: + assert self._rcache_rcache is not None + try: + self._rcache_rcache.update_cache( + [AfcReqRespKey(afc_req=request, afc_resp=response, + req_cfg_digest=req_cfg_digest)]) + except pydantic.ValidationError as ex: + error(f"Invalid arguments syntax: '{ex}'") + + def rmq_create_rx_connection(self) -> RcacheRmqConnection: + """ Creates RcacheRmqConnection. + + Must be called before opposite side starts transmitting. Object being + returned is a context manager (may be used with 'with', or should be + explicitly closed with its close() method + """ + assert self._rcache_rmq is not None + return self._rcache_rmq.create_connection() + + def rmq_receive_responses(self, rx_connection: RcacheRmqConnection, + req_cfg_digests: List[str], + timeout_sec: float) -> Dict[str, Optional[str]]: + """ Receiver ARC responses from RabbitMQ queue + + Arguments: + rx_connection -- Previously created + req_cfg_digests -- List of expected request/config digests + timeout_sec -- RX timeout in seconds + Returns dictionary of responses (as strings), indexed by request/config + digests. Failed responses represented by Nones + """ + assert self._rcache_rmq is not None + rrks = rx_connection.receive_responses(req_cfg_digests=req_cfg_digests, + timeout_sec=timeout_sec) + assert rrks is not None + to_update = \ + [AfcReqRespKey.from_orm(rrk) for rrk in rrks + if (rrk.afc_req is not None) and (rrk.afc_resp is not None)] + if to_update: + assert self._rcache_rcache is not None + self._rcache_rcache.update_cache(rrks=to_update) + return {rrk.req_cfg_digest: rrk.afc_resp for rrk in rrks} + + def rcache_invalidate( + self, ruleset_ids: Optional[Union[str, List[str]]] = None) -> None: + """ Invalidate cache - completely or for given configs + + Arguments: + ruleset_ids -- None for complete invalidation, ruleset_id or list + thereof for invalidation for given ruleset IDs + """ + assert self._rcache_rcache is not None + self._rcache_rcache.invalidate_cache(ruleset_ids=[ruleset_ids] + if isinstance(ruleset_ids, str) + else ruleset_ids) + + def rcache_spatial_invalidate(self, tiles: List["LatLonRect"]) -> None: + """ Spatial invalidation + + Arguments: + tiles -- List of latitude/longitude rectangles containing changed FSs + """ + assert self._rcache_rcache is not None + self._rcache_rcache.spatial_invalidate_cache(tiles=tiles) diff --git a/src/afc-packages/rcache/rcache_common.py b/src/afc-packages/rcache/rcache_common.py new file mode 100644 index 0000000..7b7d514 --- /dev/null +++ b/src/afc-packages/rcache/rcache_common.py @@ -0,0 +1,176 @@ +""" AFC Request cache common utility stuff """ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# pylint: disable=unnecessary-pass, logging-fstring-interpolation +# pylint: disable=global-statement + +import inspect +import logging +import os +import sys +import traceback +from typing import Any, Callable, Optional, Type +import urllib.parse + +__all__ = ["dp", "error", "error_if", "FailOnError", "get_module_logger", + "include_stack_to_error_log", "safe_dsn", "set_dp_printer", + "set_error_exception"] + +# Exception type to raise on error()/error_if() +_error_exception_type: type[BaseException] = SystemExit + +# True to include stack to log messages, created by error()/error_if() +_include_stack_to_error_log: bool = False + +# How to print debug messages +_dp_printer: Callable[[str], None] = lambda s: print(s, file=sys.stderr) + + +def dp(*args, **kwargs) -> None: # pylint: disable=invalid-name + """Print debug message + + Arguments: + args -- Format and positional arguments. If latter present - formatted + with % + kwargs -- Keyword arguments. If present formatted with format() + """ + msg = args[0] if args else "" + if len(args) > 1: + msg = msg % args[1:] + if args and kwargs: + msg = msg.format(**kwargs) + cur_frame = inspect.currentframe() + assert (cur_frame is not None) and (cur_frame.f_back is not None) + frameinfo = inspect.getframeinfo(cur_frame.f_back) + _dp_printer(f"DP {frameinfo.function}()@{frameinfo.lineno}: {msg}") + + +def get_module_logger(caller_level: int = 1) -> logging.Logger: + """ Returns logger object, named after caller module + + Arguments: + caller_level -- How far up on stack is the caller (1 - immediate caller, + etc.) + Returns logging.Logger object + """ + caller_frame = inspect.stack()[caller_level] + caller_module = \ + os.path.splitext(os.path.basename(caller_frame.filename))[0] + return logging.getLogger(caller_module) + + +def set_error_exception(exc_type: Type[BaseException]) -> Type[BaseException]: + """ Set Exception to raise on error()/error_if(). Returns previous one """ + global _error_exception_type + assert _error_exception_type is not None + ret = _error_exception_type + _error_exception_type = exc_type + return ret + + +def set_dp_printer(dp_printer: Callable[[str], None]) -> Callable[[str], None]: + """ Sets new dp() printing method, returns previous one """ + global _dp_printer + ret = _dp_printer + _dp_printer = dp_printer + return ret + + +def include_stack_to_error_log(state: bool) -> bool: + """ Sets if stack trace should be included into log message, generated by + error()/error_if(). Returns previous setting + """ + global _include_stack_to_error_log + ret = _include_stack_to_error_log + _include_stack_to_error_log = state + return ret + + +def error(msg: str, caller_level: int = 1) -> None: + """ Generates error exception and write it to log + + Arguments: + msg -- Message to put to exception and to include into log + caller_level -- How fur up stack is the caller (1 - immediate caller). + Used to retrieve logger for caller's module + """ + stack_trace = f"Most recent call last:\n" \ + f"{''.join(traceback.format_stack()[:-caller_level])}\n" \ + if _include_stack_to_error_log else "" + get_module_logger(caller_level + 1).error(f"{stack_trace}{msg}") + raise _error_exception_type(msg) + + +def error_if(cond: Any, msg: str) -> None: + """ If given condition met - raise and write to log an error with given + message """ + if cond: + error(msg=msg, caller_level=2) + + +class FailOnError: + """ Context that conditionally intercepts exceptions, raised by + error()/error_if() + + Private attributes: + _prev_error_exception_type -- Previously used error exception type if + errors are intercepted, None if not + intercepted + """ + class _IntermediateErrorException(Exception): + """ Exception used for interception """ + pass + + def __init__(self, fail_on_error: bool) -> None: + """ Constructor + + fail_on_error -- False to intercept error exception + """ + self._prev_error_exception_type: Optional[Type[BaseException]] = \ + None if fail_on_error \ + else set_error_exception(FailOnError._IntermediateErrorException) + + def __enter__(self) -> None: + """ Context entry """ + pass + + def __exit__(self, exc_type: Any, exc_value: Any, exc_tb: Any) -> bool: + """ Context exit + + Arguments: + exc_type -- Type of exception that caused context leave, None for + normal leave + exc_value -- Value of exception that caused context leave. None for + normal leave + exc_tb -- Traceback of exception that caused context leave, None for + normal leave + Returns True if exception was processed, False if it should be + propagated + """ + if (self._prev_error_exception_type is not None) and \ + (exc_type == FailOnError._IntermediateErrorException): + set_error_exception(self._prev_error_exception_type) + return True + return False + + +def safe_dsn(dsn: Optional[str]) -> Optional[str]: + """ Returns DSN without password (if there was any) """ + if not dsn: + return dsn + try: + parsed = urllib.parse.urlparse(dsn) + if not parsed.password: + return dsn + return \ + urllib.parse.urlunparse( + parsed._replace( + netloc=parsed.netloc.replace(":" + parsed.password, + ":"))) + except Exception: + return dsn diff --git a/src/afc-packages/rcache/rcache_db.py b/src/afc-packages/rcache/rcache_db.py new file mode 100644 index 0000000..302651a --- /dev/null +++ b/src/afc-packages/rcache/rcache_db.py @@ -0,0 +1,350 @@ +""" Synchronous part of AFC Request Cache database stuff """ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# pylint: disable=wrong-import-order, invalid-name, too-many-statements, +# pylint: disable=too-many-branches + +import sqlalchemy as sa +from typing import Any, cast, Dict, List, Optional, Tuple +import urllib.parse + +from rcache_common import dp, error, error_if, FailOnError, \ + get_module_logger, safe_dsn +from rcache_models import ApDbRespState, ApDbRecord + +__all__ = ["RcacheDb"] + +# Logger for this module +LOGGER = get_module_logger() + + +class RcacheDb: + """ Base/synchronous part of cache pPostgres database handling + + Public attributes: + metadata -- Database metadata. Default after construction, actual + after connection + rcache_db_dsn -- Database connection string. May be None e.g. if this + object is used for Alembic + db_name -- Database name. None before connect() + ap_table -- Table object for AP table. None before connect() + ap_pk_columns -- Tuple of primary key's names. None when 'table' is None + + Private attributes: + _engine -- SqlAlchemy Engine object. None before connect() + """ + + class _RootDb: + """ Context manager that encapsulates everexisting Postgres root + database + + Public attributes: + dsn -- Root database connection string + conn -- Root database connection + + Private attributes: + _engine -- Root database engine + """ + # Name of root database (used for database creation + ROOT_DB_NAME = "postgres" + + def __init__(self, dsn: str) -> None: + """ Constructor + + Arguments: + dsn -- Connection string to some (nonroot) database on same server + """ + self.dsn = \ + urllib.parse.urlunsplit( + urllib.parse.urlsplit(dsn). + _replace(path=f"/{self.ROOT_DB_NAME}")) + self._engine: Any = None + self.conn: Any = None + try: + self._engine = sa.create_engine(self.dsn) + self.conn = self._engine.connect() + except sa.exc.SQLAlchemyError as ex: + error( + f"Can't connect to root database '{safe_dsn(self.dsn)}': " + f"{ex}") + finally: + if (self.conn is None) and (self._engine is not None): + # Connection failed + self._engine.dispose() + + def __enter__(self) -> "RcacheDb._RootDb": + """ Context entry """ + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """ Context exit """ + if self.conn is not None: + self.conn.close() + if self._engine is not None: + self._engine.dispose() + + # Maximum number of fields in one UPDATE + _MAX_UPDATE_FIELDS = 32767 + + # Name of request cache table in database + AP_TABLE_NAME = "aps" + + # Name of enable/disable switches table in database + SWITCHES_TABLE_NAME = "switches" + + # All table names + ALL_TABLE_NAMES = [AP_TABLE_NAME, SWITCHES_TABLE_NAME] + + def __init__(self, rcache_db_dsn: Optional[str] = None) -> None: + """ Constructor + + Arguments: + rcache_db_dsn -- Database connection string. May be None for Alembic + use + """ + self.metadata = sa.MetaData() + # This declaration must be kept in sync with rcache_models.ApDbRecord + sa.Table( + self.AP_TABLE_NAME, + self.metadata, + sa.Column("serial_number", sa.String(), nullable=False, + primary_key=True), + sa.Column("rulesets", sa.String(), nullable=False, + primary_key=True), + sa.Column("cert_ids", sa.String(), nullable=False, + primary_key=True), + sa.Column("state", sa.Enum(ApDbRespState), nullable=False, + index=True), + sa.Column("config_ruleset", sa.String(), nullable=False, + index=True), + sa.Column("lat_deg", sa.Float(), nullable=False, index=True), + sa.Column("lon_deg", sa.Float(), nullable=False, index=True), + sa.Column("last_update", sa.DateTime(), nullable=False, + index=True), + sa.Column("req_cfg_digest", sa.String(), nullable=False, + index=True, unique=True), + sa.Column("validity_period_sec", sa.Float(), nullable=True), + sa.Column("request", sa.String(), nullable=False), + sa.Column("response", sa.String(), nullable=False)) + sa.Table( + self.SWITCHES_TABLE_NAME, + self.metadata, + sa.Column("name", sa.String(), nullable=False, primary_key=True), + sa.Column("state", sa.Boolean(), nullable=False)) + self.rcache_db_dsn = rcache_db_dsn + self.db_name: Optional[str] = \ + urllib.parse.urlsplit(self.rcache_db_dsn).path.strip("/") \ + if self.rcache_db_dsn else None + self._engine: Any = None + self.ap_table: Optional[sa.Table] = None + self.ap_pk_columns: Optional[Tuple[str, ...]] = None + + def max_update_records(self) -> int: + """ Maximum number of records in one update """ + return self._MAX_UPDATE_FIELDS // \ + len(self.metadata.tables[self.AP_TABLE_NAME].c) + + def check_server(self) -> bool: + """ True if database server can be connected """ + error_if(not self.rcache_db_dsn, + "AFC Response Cache URL was not specified") + assert self.rcache_db_dsn is not None + with FailOnError(False), self._RootDb(self.rcache_db_dsn) as rdb: + rdb.conn.execute("SELECT 1") + return True + return False + + def create_db(self, recreate_db=False, recreate_tables=False, + fail_on_error=True) -> bool: + """ Creates database if absent, optionally adjust if present + + Arguments: + recreate_db -- Recreate database if it exists + recreate_tables -- Recreate known database tables if database exists + fail_on_error -- True to fail on error, False to return success + status + Returns True on success, Fail on failure (if fail_on_error is False) + """ + engine: Any = None + with FailOnError(fail_on_error): + try: + if self._engine: + self._engine.dispose() + self._engine = None + error_if(not self.rcache_db_dsn, + "AFC Response Cache URL was not specified") + assert self.rcache_db_dsn is not None + engine = self._create_sync_engine(self.rcache_db_dsn) + if recreate_db: + with self._RootDb(self.rcache_db_dsn) as rdb: + try: + rdb.conn.execute("COMMIT") + rdb.conn.execute( + f'DROP DATABASE IF EXISTS "{self.db_name}"') + except sa.exc.SQLAlchemyError as ex: + error(f"Unable to drop database '{self.db_name}': " + f"{ex}") + try: + with engine.connect(): + pass + except sa.exc.SQLAlchemyError: + with self._RootDb(self.rcache_db_dsn) as rdb: + try: + rdb.conn.execute("COMMIT") + rdb.conn.execute( + f'CREATE DATABASE "{self.db_name}"') + with engine.connect(): + pass + except sa.exc.SQLAlchemyError as ex1: + error(f"Unable to create target database: {ex1}") + try: + if recreate_tables: + with engine.connect() as conn: + conn.execute("COMMIT") + for table_name in self.ALL_TABLE_NAMES: + conn.execute( + f'DROP TABLE IF EXISTS "{table_name}"') + self.metadata.create_all(engine) + self._read_metadata() + except sa.exc.SQLAlchemyError as ex: + error(f"Unable to (re)create tables in the database " + f"'{self.db_name}': {ex}") + self._update_ap_table() + self._engine = engine + engine = None + return True + finally: + if engine: + engine.dispose() + return False + + def connect(self, fail_on_error=True) -> bool: + """ Connect to database, that is assumed to be existing + + Arguments: + fail_on_error -- True to fail on error, False to return success + status + Returns True on success, Fail on failure (if fail_on_error is False) + """ + if self._engine: + return True + engine: Any = None + with FailOnError(fail_on_error): + try: + error_if(not self.rcache_db_dsn, + "AFC Response Cache URL was not specified") + engine = self._create_engine(self.rcache_db_dsn) + dsn_parts = urllib.parse.urlsplit(self.rcache_db_dsn) + self.db_name = cast(str, dsn_parts.path).strip("/") + self._read_metadata() + with engine.connect(): + pass + self._engine = engine + engine = None + return True + finally: + if engine: + engine.dispose() + return False + + def disconnect(self) -> None: + """ Disconnect database """ + if self._engine: + self._engine.dispose() + self._engine = None + + def lookup(self, req_cfg_digests: List[str], + try_reconnect=False) -> Dict[str, str]: + """ Request cache lookup + + Arguments: + req_cfg_digests -- List of request/config digests + try_reconnect -- On failure try reconnect + Returns Dictionary of found requests, indexed by request/config digests + """ + retry = False + while True: + if try_reconnect and (self._engine is None): + self.connect() + assert (self._engine is not None) and (self.ap_table is not None) + s = sa.select([self.ap_table]).\ + where((self.ap_table.c.req_cfg_digest.in_(req_cfg_digests)) & + (self.ap_table.c.state == ApDbRespState.Valid.name)) + try: + with self._engine.connect() as conn: + rp = conn.execute(s) + return {rec.req_cfg_digest: + ApDbRecord.parse_obj(rec).get_patched_response() + for rec in rp} + except sa.exc.SQLAlchemyError as ex: + if retry or (not try_reconnect): + error(f"Error querying '{self.db_name}: {ex}") + retry = True + try: + self.disconnect() + except sa.exc.SQLAlchemyError: + self._engine = None + assert self._engine is None + return {} # Will never happen, appeasing pylint + + def _read_metadata(self) -> None: + """ Reads-in metadata (fill in self.metadata, self.ap_table) from an + existing database """ + engine: Any = None + try: + engine = self._create_sync_engine(self.rcache_db_dsn) + with engine.connect(): + pass + metadata = sa.MetaData() + metadata.reflect(bind=engine) + for table_name in self.ALL_TABLE_NAMES: + error_if( + table_name not in metadata.tables, + f"Table '{table_name}' not present in the database " + f"'{safe_dsn(self.rcache_db_dsn)}'") + self.metadata = metadata + self._update_ap_table() + except sa.exc.SQLAlchemyError as ex: + error(f"Can't connect to database " + f"'{safe_dsn(self.rcache_db_dsn)}': {ex}") + finally: + if engine is not None: + engine.dispose() + + def get_ap_pk(self, row: Dict[str, Any]) -> Tuple: + """ Return primary key tuple for given AP row dictionary """ + assert self.ap_pk_columns is not None + return tuple(row[c] for c in self.ap_pk_columns) + + def _update_ap_table(self) -> None: + """ Reads 'table' and index columns from current metadata """ + self.ap_table = self.metadata.tables[self.AP_TABLE_NAME] + self.ap_pk_columns = \ + tuple(c.name for c in self.ap_table.c if c.primary_key) + + def _create_engine(self, dsn) -> Any: + """ Creates SqlAlchemy engine + + Overloaded in RcacheDbAsync to create asynchronous engine + + Returns Engine object + """ + return self._create_sync_engine(dsn) + + def _create_sync_engine(self, dsn) -> Any: + """ Creates synchronous SqlAlchemy engine + + Overloaded in RcacheDbAsync to create asynchronous engine + + Returns Engine object + """ + try: + return sa.create_engine(dsn) + except sa.exc.SQLAlchemyError as ex: + error(f"Invalid database DSN: '{safe_dsn(dsn)}': {ex}") + return None # Will never happen, appeasing pylint diff --git a/src/afc-packages/rcache/rcache_models.py b/src/afc-packages/rcache/rcache_models.py new file mode 100644 index 0000000..bc01dda --- /dev/null +++ b/src/afc-packages/rcache/rcache_models.py @@ -0,0 +1,566 @@ +""" AFC Request Cache external interface structures """ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# pylint: disable=too-few-public-methods, invalid-name, wrong-import-order +# pylint: disable=too-many-boolean-expressions +# pylint: disable=no-member + +import datetime +import enum +import json +import pydantic +from typing import Any, Dict, List, Optional + +from rcache_common import dp + +__all__ = ["AfcReqRespKey", "ApDbPk", "ApDbRecord", "ApDbRespState", + "FuncSwitch", "IfDbExists", "LatLonRect", "RatapiAfcConfig", + "RatapiRulesetIds", "RcacheClientSettings", "RcacheInvalidateReq", + "RcacheServiceSettings", "RcacheSpatialInvalidateReq", + "RcacheStatus", "RcacheUpdateReq", "RmqReqRespKey"] + +# Format of response expiration time +RESP_EXPIRATION_FORMAT = "%Y-%m-%dT%H:%M:%SZ" + + +class IfDbExists(enum.Enum): + """ What to do if cache database exists on RCache service startup """ + # Leave as is (default) + leave = "leave" + # Completely recreate (extra stuff, like alembic, will be removed) + recreate = "recreate" + # Clean cache table, leaving others intact + clean = "clean" + + +class RcacheServiceSettings(pydantic.BaseSettings): + """ Rcache service parameters, passed via environment """ + + class Config: + """ Metainformation """ + # Prefix of environment variables + env_prefix = "RCACHE_" + + enabled: bool = \ + pydantic.Field( + True, title="Rcache enabled (False for legacy file-based cache") + + port: int = pydantic.Field(..., title="Port this service listens on", + env="RCACHE_CLIENT_PORT") + postgres_dsn: pydantic.PostgresDsn = \ + pydantic.Field( + ..., + title="Postgres DSN: " + "postgresql://[user[:password]]@host[:port]/database[?...]") + if_db_exists: IfDbExists = \ + pydantic.Field( + IfDbExists.leave, + title="What to do if cache database already exists: 'leave' " + "(leave as is - default), 'recreate' (completely recreate - e.g. " + "removing alembic, if any), 'clean' (only clean the cache table)") + precompute_quota: int = \ + pydantic.Field( + 10, title="Number of simultaneous precomputing requests in flight") + afc_req_url: Optional[pydantic.AnyHttpUrl] = \ + pydantic.Field( + None, + title="RestAPI URL to send AFC Requests for precomputation") + rulesets_url: Optional[pydantic.AnyHttpUrl] = \ + pydantic.Field( + None, + title="RestAPI URL to retrieve list of active Ruleset IDs") + config_retrieval_url: Optional[pydantic.AnyHttpUrl] = \ + pydantic.Field( + None, + title="RestAPI URL to retrieve AFC Config for given Ruleset ID") + + @classmethod + @pydantic.root_validator(pre=True) + def _remove_empty(cls, values: Dict[str, Any]) -> Dict[str, Any]: + """ Removes empty entries """ + for key in list(values.keys()): + if values[key] == "": + del values[key] + return values + + +class RcacheClientSettings(pydantic.BaseSettings): + """ Parameters of Rcache clients in various services, passed via + environment """ + class Config: + """ Metainformation """ + # Prefix of environment variables + env_prefix = "RCACHE_" + + enabled: bool = \ + pydantic.Field(True, + title="Rcache enabled (False for legacy file-based " + "cache. Default is enabled") + postgres_dsn: Optional[pydantic.PostgresDsn] = \ + pydantic.Field( + None, + title="Postgres DSN: " + "postgresql://[user[:password]]@host[:port]/database[?...]") + service_url: Optional[pydantic.AnyHttpUrl] = \ + pydantic.Field(None, title="Rcache server base RestAPI URL") + rmq_dsn: Optional[pydantic.AmqpDsn] = \ + pydantic.Field( + None, + title="RabbitMQ AMQP DSN: amqp://[user[:password]]@host[:port]") + update_on_send: bool = \ + pydantic.Field( + True, + title="True to update cache from worker (on sending response), " + "False to update cache on msghnd (on receiving response)") + + @classmethod + @pydantic.root_validator(pre=True) + def _remove_empty(cls, values: Dict[str, Any]) -> Dict[str, Any]: + """ Removes empty entries """ + for key in list(values.keys()): + if values[key] == "": + del values[key] + return values + + def validate_for(self, db: bool = False, rmq: bool = False, + rcache: bool = False) -> None: + """ Generates exception if Rcache is enabled, but parameter(s) for + some its required aspect are not set + + Arguments: + db -- Check parameters for Postgres DB connection + rmq -- Check parameters for RabbitMQ connection + rcache -- Check parameters for Rcache service connection + """ + if not self.enabled: + return + for predicate, attr in [(db, "postgres_dsn"), (rmq, "rmq_dsn"), + (rcache, "service_url")]: + if (not predicate) or getattr(self, attr): + continue + raise ValueError( + f"RcacheClientSettings.{attr} Rcache client configuration " + f"parameter neither set explicitly nor via " + f"{self.Config.env_prefix}{attr.upper()} environment " + f"variable") + + +class LatLonRect(pydantic.BaseModel): + """ Latitude/longitude rectangle, used in spatial cache invalidation """ + min_lat: float = \ + pydantic.Field(..., title="Minimum latitude in north-positive degrees") + max_lat: float = \ + pydantic.Field(..., title="Maximum latitude in north-positive degrees") + min_lon: float = \ + pydantic.Field(..., title="Minimum longitude in east-positive degrees") + max_lon: float = \ + pydantic.Field(..., title="Maximum longitude in east-positive degrees") + + def short_str(self) -> str: + """ Condensed string representation """ + parts: List[str] = [] + for min_val, max_val, pos_semi, neg_semi in \ + [(self.min_lat, self.max_lat, "N", "S"), + (self.min_lon, self.max_lon, "E", "W")]: + if (min_val * max_val) >= 0: + # Same hemisphere + parts.append( + f"[{min(abs(min_val), abs(max_val))}-" + f"{max(abs(min_val), abs(max_val))}]" + f"{pos_semi if (min_val + max_val) >= 0 else neg_semi}") + else: + # Different hemispheres + parts.append( + f"[{abs(min_val)}{neg_semi}-" + f"{abs(max_val)}{pos_semi}]") + return ", ".join(parts) + + +class RcacheInvalidateReq(pydantic.BaseModel): + """ RCache REST API cache invalidation request """ + ruleset_ids: Optional[List[str]] = \ + pydantic.Field(None, + title="Optional list of ruleset IDs to invalidate. By " + "default invalidates everything") + + +class RcacheSpatialInvalidateReq(pydantic.BaseModel): + """ RCache REST API spatial invalidation request """ + tiles: List[LatLonRect] = \ + pydantic.Field(..., title="List of rectangles, containing changed FSs") + + +class AfcReqRespKey(pydantic.BaseModel): + """ Information about single computed result, used in request cache update + request """ + afc_req: str = pydantic.Field(..., title="AFC Request as string") + afc_resp: str = pydantic.Field(..., title="AFC Response as string") + req_cfg_digest: str = \ + pydantic.Field( + ..., title="Request/Config hash (cache lookup key) as string") + + class Config: + """ Metadata """ + # May be constructed from RmqReqRespKey + orm_mode = True + + +class RcacheUpdateReq(pydantic.BaseModel): + """ RCache REST API cache update request """ + req_resp_keys: List[AfcReqRespKey] = \ + pydantic.Field(..., title="Computation results to add to cache") + + +class RcacheStatus(pydantic.BaseModel): + """ Rcache service status information """ + up_time: datetime.timedelta = pydantic.Field(..., title="Service up time") + db_connected: bool = \ + pydantic.Field(..., title="Database successfully connected") + all_tasks_running: bool = \ + pydantic.Field(..., title="All tasks running (none crashed)") + invalidation_enabled: bool = \ + pydantic.Field(..., title="Invalidation enabled") + update_enabled: bool = pydantic.Field(..., title="Update enabled") + precomputation_enabled: bool = \ + pydantic.Field(..., title="Precomputation enabled") + precomputation_quota: int = \ + pydantic.Field(..., + title="Maximum number of simultaneous precomputations") + num_valid_entries: int = \ + pydantic.Field(..., title="Number of valid entries in cache") + num_invalid_entries: int = \ + pydantic.Field( + ..., + title="Number of invalidated (awaiting precomputation) entries in " + "cache") + update_queue_len: int = \ + pydantic.Field(..., title="Number of pending records in update queue") + update_count: int = \ + pydantic.Field(..., title="Number of updates written to database") + avg_update_write_rate: float = \ + pydantic.Field(..., title="Average number of update writes per second") + avg_update_queue_len: float = \ + pydantic.Field(..., title="Average number of pending update records") + num_precomputed: int = \ + pydantic.Field(..., title="Number of initiated precomputations") + active_precomputations: int = \ + pydantic.Field(..., title="Number of active precomputations") + avg_precomputation_rate: float = \ + pydantic.Field( + ..., + title="Average number precomputations initiated per per second") + avg_schedule_lag: float = \ + pydantic.Field( + ..., + title="Average scheduling delay in seconds (measure of service " + "process load)") + + +class AfcReqCertificationId(pydantic.BaseModel): + """ Interesting part of AFC Request's CertificationId structure """ + rulesetId: str = \ + pydantic.Field( + ..., title="Regulatory ruleset for which certificate was given") + id: str = \ + pydantic.Field(..., title="Certification ID for given ruleset") + + +class AfcReqDeviceDescriptor(pydantic.BaseModel): + """ Interesting part of AFC Request's DeviceDescriptor structure """ + serialNumber: str = \ + pydantic.Field(..., title="Device serial number") + certificationId: List[AfcReqCertificationId] = \ + pydantic.Field(..., min_items=1, title="Device certifications") + + +class AFcReqPoint(pydantic.BaseModel): + """ Interesting part of AFC Request's Point structure """ + longitude: float = \ + pydantic.Field(..., title="Longitude in east-positive degrees") + latitude: float = \ + pydantic.Field(..., title="Latitude in north-positive degrees") + + +class AfcReqEllipse(pydantic.BaseModel): + """ Interesting part of AFC Request's Ellipse structure """ + center: AFcReqPoint = pydantic.Field(..., title="Ellipse center") + + +class AfcReqLinearPolygon(pydantic.BaseModel): + """ Interesting part of AFC Request's LinearPolygon structure """ + outerBoundary: List[AFcReqPoint] = \ + pydantic.Field(..., title="List of vertices", min_items=1) + + +class AfcReqRadialPolygon(pydantic.BaseModel): + """ Interesting part of AFC Request's RadialPolygon structure """ + center: AFcReqPoint = pydantic.Field(..., title="Polygon center") + + +class AfcReqLocation(pydantic.BaseModel): + """ Interesting part of AFC Request's Location structure """ + ellipse: Optional[AfcReqEllipse] = \ + pydantic.Field(None, title="Optional ellipse descriptor") + linearPolygon: Optional[AfcReqLinearPolygon] = \ + pydantic.Field(None, title="Optional linear polygon descriptor") + radialPolygon: Optional[AfcReqRadialPolygon] = \ + pydantic.Field(None, title="Optional radial polygon descriptor") + + @classmethod + @pydantic.root_validator() + def one_definition(cls, v: Dict[str, Any]) -> Dict[str, Any]: + """ Verifies that exactly one type of AP location is specified """ + if ((0 if v.get("ellipse") is None else 1) + + (0 if v.get("linearPolygon") is None else 1) + + (0 if v.get("radialPolygon") is None else 1)) != 1: + raise ValueError( + "Not exactly one AFC Request location definition found") + return v + + def center(self) -> AFcReqPoint: + """ Returns location center """ + if self.ellipse is not None: + return self.ellipse.center + if self.radialPolygon is not None: + return self.radialPolygon.center + assert self.linearPolygon is not None + lat = sum(b.latitude for b in self.linearPolygon.outerBoundary) / \ + len(self.linearPolygon.outerBoundary) + lon: float = 0. + lon0 = self.linearPolygon.outerBoundary[0].longitude + for b in self.linearPolygon.outerBoundary: + blon = b.longitude + while blon > (lon0 + 360): + blon -= 360 + while blon <= (lon0 - 360): + blon += 360 + lon += blon + lon /= len(self.linearPolygon.outerBoundary) + return AFcReqPoint(latitude=lat, longitude=lon) + + +class AfcReqAvailableSpectrumInquiryRequest(pydantic.BaseModel): + """ Interesting part of AFC Request's AvailableSpectrumInquiryRequest + structure """ + deviceDescriptor: AfcReqDeviceDescriptor = \ + pydantic.Field(..., title="Device descriptor") + location: AfcReqLocation = \ + pydantic.Field(..., title="Device location") + + +class AfcReqAvailableSpectrumInquiryRequestMessage(pydantic.BaseModel): + """ Interesting part of AFC Request's + AvailableSpectrumInquiryRequestMessage structure """ + availableSpectrumInquiryRequests: \ + List[AfcReqAvailableSpectrumInquiryRequest] = \ + pydantic.Field(..., min_items=1, max_items=1, + title="Single element list of requests") + + +class AfcRespResponse(pydantic.BaseModel): + """ Interesting part of AFC Responses' status data """ + responseCode: int = pydantic.Field(..., title="Response code") + + +class AfcRespAvailableSpectrumInquiryResponse(pydantic.BaseModel): + """ Interesting part of AFC Response's AvailableSpectrumInquiryResponse + structure """ + rulesetId: str = \ + pydantic.Field(..., title="ID of ruleset used for computation") + availabilityExpireTime: Optional[str] = \ + pydantic.Field( + None, + title="UTC expiration time in YYY-MM-DDThh:mm:ssZ format. Absent " + "if response unsuccessful") + response: AfcRespResponse = pydantic.Field(None, title="Response status") + + @classmethod + @pydantic.validator('availabilityExpireTime') + def check_expiration_time_format(cls, v: Optional[str]) -> Optional[str]: + """ Checks validity of 'availabilityExpireTime' """ + if v is not None: + datetime.datetime.strptime(v, RESP_EXPIRATION_FORMAT) + return v + + +class AfcRespAvailableSpectrumInquiryResponseMessage(pydantic.BaseModel): + """ Interesting part of AFC Response's + AvailableSpectrumInquiryResponseMessage structure """ + availableSpectrumInquiryResponses: \ + List[AfcRespAvailableSpectrumInquiryResponse] = \ + pydantic.Field(..., min_items=1, max_items=1, + title="Single-element list of responses") + + +class RmqReqRespKey(pydantic.BaseModel): + """ Request/Response/Digest structure, used in RabbitMQ communication """ + afc_req: Optional[str] = \ + pydantic.Field( + ..., + title="AFC Request as string. None if cache update being made on " + "sender (Worker) side") + afc_resp: Optional[str] = \ + pydantic.Field(..., title="AFC Response as string. None on failure") + req_cfg_digest: str = \ + pydantic.Field( + ..., title="Request/Config hash (cache lookup key) as string") + + +# Cache database row state (reflects status of response) +ApDbRespState = enum.Enum("ApDbRespState", ["Valid", "Invalid", "Precomp"]) + + +class ApDbPk(pydantic.BaseModel): + """ Fields that comprise database primary key in AP table """ + serial_number: str = pydantic.Field(..., title="Device serial number") + rulesets: str = pydantic.Field(..., title="Concatenated ruleset IDs") + cert_ids: str = pydantic.Field(..., title="Concatenated certification IDs") + + @classmethod + def from_req( + cls, + req_pydantic: + Optional[AfcReqAvailableSpectrumInquiryRequestMessage] = None, + req_str: Optional[str] = None) -> "ApDbPk": + """ Create self from either string or pydantic request message """ + if req_pydantic is None: + assert req_str is not None + req_pydantic = \ + AfcReqAvailableSpectrumInquiryRequestMessage.parse_raw(req_str) + first_request = req_pydantic.availableSpectrumInquiryRequests[0] + return \ + ApDbPk( + serial_number=first_request.deviceDescriptor.serialNumber, + rulesets="|".join(cert.rulesetId for cert in + first_request.deviceDescriptor. + certificationId), + cert_ids="|".join(cert.id for cert in + first_request.deviceDescriptor. + certificationId)) + + +class ApDbRecord(pydantic.BaseModel): + """ Database AP record in Pydantic representation + + Structure must be kept in sync with one, defined in rcache_db.RcacheDb() + """ + serial_number: str = pydantic.Field(..., title="Device serial number") + rulesets: str = pydantic.Field(..., title="Concatenated ruleset IDs") + cert_ids: str = pydantic.Field(..., title="Concatenated certification IDs") + state: str = pydantic.Field(..., title="Response state") + config_ruleset: str = \ + pydantic.Field(..., title="Ruleset used for computation") + lat_deg: float = \ + pydantic.Field(..., title="North positive latitude in degrees") + lon_deg: float = \ + pydantic.Field(..., title="East positive longitude in degrees") + last_update: datetime.datetime = \ + pydantic.Field(..., title="Time of last update") + req_cfg_digest: str = \ + pydantic.Field(..., title="Request/Config digest (cache lookup key") + validity_period_sec: Optional[float] = \ + pydantic.Field(..., title="Response validity period in seconds") + request: str = pydantic.Field(..., title="Request message as string") + response: str = pydantic.Field(..., title="Response message as string") + + @classmethod + def from_req_resp_key(cls, rrk: AfcReqRespKey) -> Optional["ApDbRecord"]: + """ Construct from Request/Response/digest data + Returns None if data not deserved to be in database + """ + resp = \ + AfcRespAvailableSpectrumInquiryResponseMessage.parse_raw( + rrk.afc_resp).availableSpectrumInquiryResponses[0] + if resp.response.responseCode != 0: + return None + req_pydantic = \ + AfcReqAvailableSpectrumInquiryRequestMessage.parse_raw( + rrk.afc_req) + pk = ApDbPk.from_req(req_pydantic=req_pydantic) + center = \ + req_pydantic.availableSpectrumInquiryRequests[0].location.center() + return \ + ApDbRecord( + **pk.dict(), + state=ApDbRespState.Valid.name, + config_ruleset=resp.rulesetId, + lat_deg=center.latitude, + lon_deg=center.longitude, + last_update=datetime.datetime.now(), + req_cfg_digest=rrk.req_cfg_digest, + validity_period_sec=None + if resp.availabilityExpireTime is None + else (datetime.datetime.strptime( + resp.availabilityExpireTime, RESP_EXPIRATION_FORMAT) - + datetime.datetime.utcnow()).total_seconds(), + request=rrk.afc_req, + response=rrk.afc_resp) + + @classmethod + def check_db_table(cls, table: Any) -> Optional[str]: + """ Checks that structure is the same as given SqlAlchemy tab + Returns None if all OK, error message otherwise + """ + class_name = cls.schema()['title'] + properties: Dict[str, Dict[str, str]] = cls.schema()["properties"] + if len(properties) != len(table.c): + return f"Database table and {class_name} have different numbers " \ + f"of fields" + for idx, (field_name, attrs) in enumerate(properties.items()): + if field_name != table.c[idx].name: + return f"{idx}'s field in database and in {class_name} have " \ + f"different names: {table.c[idx].name} and {field_name} " \ + f"respectively" + field_type = attrs.get("type") + column_type_name = str(table.c[idx].type) + if ((column_type_name == "INTEGER") and + (field_type != "integer")) or \ + ((column_type_name == "FLOAT") and + (field_type != "number")) or \ + ((column_type_name == "VARCHAR") and + (field_type not in ("string", None))) or \ + ((column_type_name == "DATETIME") and + (attrs.get("format") != "date-time")): + return f"Field '{field_name}' at index {idx} in the " \ + f"database and in the {class_name} have different " \ + f"types: {str(table.c[idx].type)} and {field_type} " \ + f"respectively" + if set(c.name for c in table.c if c.primary_key) != \ + set(ApDbPk.schema()["properties"].keys()): + return "Different primary key composition in database and ApDbPk" + return None + + def get_patched_response(self) -> str: + """ Returns response patched with known nonconstant fields """ + ret_dict = json.loads(self.response) + resp = ret_dict["availableSpectrumInquiryResponses"][0] + if self.validity_period_sec is not None: + resp["availabilityExpireTime"] = \ + datetime.datetime.strftime( + datetime.datetime.utcnow() + + datetime.timedelta(seconds=self.validity_period_sec), + RESP_EXPIRATION_FORMAT) + return json.dumps(ret_dict) + + +class RatapiRulesetIds(pydantic.BaseModel): + """ RatApi request to retrieve list of active ruleset IDs """ + rulesetId: List[str] = \ + pydantic.Field( + ..., title="List of active ruleset IDs (AFC Config identifiers)") + + +class RatapiAfcConfig(pydantic.BaseModel): + """ Interesting parts of AFC Config, returned by RapAPI REST request """ + maxLinkDistance: float = \ + pydantic.Field(..., + title="Maximum distance between AP and affected FS RX") + + +# Rcache functionality enable/disable switches +FuncSwitch = enum.Enum("FuncSwitch", ["Update", "Invalidate", "Precompute"]) diff --git a/src/afc-packages/rcache/rcache_rcache.py b/src/afc-packages/rcache/rcache_rcache.py new file mode 100644 index 0000000..ee945d9 --- /dev/null +++ b/src/afc-packages/rcache/rcache_rcache.py @@ -0,0 +1,98 @@ +""" Sending requests to Rcache service """ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# pylint: disable=wrong-import-order + +import pydantic +import requests +from typing import Any, Dict, List, Optional + +from rcache_common import dp, error, FailOnError +from rcache_models import AfcReqRespKey, RcacheUpdateReq, \ + RcacheInvalidateReq, LatLonRect, RcacheSpatialInvalidateReq + + +class RcacheRcache: + """ Communicates with Request cache service + + Private attributes: + _rcache_server_url -- Request cache service URL + """ + + def __init__(self, rcache_server_url: str) -> None: + self._rcache_server_url = rcache_server_url.rstrip("/") + + def update_cache(self, rrks: List[AfcReqRespKey], + fail_on_error: bool = True) -> bool: + """ Update Request cache + + Arguments: + rrks -- List of request/response/digest triplets + fail_on_error -- True to fail on error, False to return False + Returns True on success, False on known fail if fail_on_error is False + """ + with FailOnError(fail_on_error): + try: + self._post(command="update", + json=RcacheUpdateReq(req_resp_keys=rrks).dict()) + except pydantic.ValidationError as ex: + error(f"Invalid argument format: {ex}") + return True + return False + + def invalidate_cache(self, ruleset_ids: Optional[List[str]] = None, + fail_on_error: bool = True) -> bool: + """ Invalidate request cache (completely of for config) + + Arguments: + ruleset_ids -- None for complete invalidation, list of ruleset IDs + for configs to invalidate for config-based + invalidation + fail_on_error -- True to fail on error, False to return False + Returns True on success, False on known fail if fail_on_error is False + """ + with FailOnError(fail_on_error): + try: + self._post( + command="invalidate", + json=RcacheInvalidateReq(ruleset_ids=ruleset_ids).dict()) + except pydantic.ValidationError as ex: + error(f"Invalid argument format: {ex}") + return True + return False + + def spatial_invalidate_cache(self, tiles: List[LatLonRect], + fail_on_error: bool = True) -> bool: + """ Spatial invalidation of request cache + + Arguments: + tiles -- List of tiles, containing changed FSs + fail_on_error -- True to fail on error, False to return False + Returns True on success, False on known fail if fail_on_error is False + """ + with FailOnError(fail_on_error): + try: + self._post(command="spatial_invalidate", + json=RcacheSpatialInvalidateReq(tiles=tiles).dict()) + except pydantic.ValidationError as ex: + error(f"Invalid argument format: {ex}") + return True + return False + + def _post(self, command: str, json: Dict[str, Any]) -> None: + """ Do the POST request to Request cache service + + Arguments: + command -- Command (last part of URL) to invoke + json -- Command parameters in JSON format + """ + try: + requests.post(f"{self._rcache_server_url}/{command}", json=json) + except requests.RequestException as ex: + error(f"Error sending '{command}' post to Request cache Server: " + f"{ex}") diff --git a/src/afc-packages/rcache/rcache_rmq.py b/src/afc-packages/rcache/rcache_rmq.py new file mode 100644 index 0000000..8228c1d --- /dev/null +++ b/src/afc-packages/rcache/rcache_rmq.py @@ -0,0 +1,200 @@ +""" RabbitMQ sender and receiver """ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# pylint: disable=wrong-import-order, logging-fstring-interpolation +# pylint: disable=too-many-arguments, too-many-branches, too-many-nested-blocks +# pylint: disable=too-few-public-methods + +import pika +import pydantic +import random +import string +from typing import cast, List, Optional, Set + +from rcache_common import error, get_module_logger, safe_dsn +from rcache_models import RmqReqRespKey + +__all__ = ["RcacheRmq", "RcacheRmqConnection"] + +LOGGER = get_module_logger() + +# Exchange name +EXCHANGE_NAME = "RcacheExchange" + + +class RcacheRmqConnection: + """ Context manager to handle single read or write operation. + + Durable RabbitMQ connections are problematic, as they require responding to + RMQ server's heartbeats and logistics of this without a separate thread is + unclear. So single-shot connection and channel are created every time. + + Private attributes: + _connection -- Pika connection adapter (corresponds to TCP connection to + RMQ server) + _channel -- Pika channel (corresponds to logical data stream) + _for_rx -- True for RX connection, False tot TX connection + _queue_name -- Queue name + """ + + def __init__(self, url_params: pika.URLParameters, + tx_queue_name: Optional[str] = None) -> None: + """ Constructor + + Arguments: + url_params -- RabbitMQ connection parameters, retrieved from URL + tx_queue_name -- Queue name for TX connection, None for RX connection + """ + self._connection: Optional[pika.BlockingConnection] = None + self._channel: \ + Optional[pika.adapters.blocking_connection.BlockingChannel] = None + self._connection = pika.BlockingConnection(url_params) + self._channel = self._connection.channel() + self._channel.exchange_declare(exchange=EXCHANGE_NAME, + exchange_type="direct") + self._for_rx = tx_queue_name is None + if self._for_rx: + self._queue_name = \ + "afc_response_queue_" + \ + "".join(random.choices(string.ascii_uppercase + string.digits, + k=10)) + self._channel.queue_declare(queue=self._queue_name, exclusive=True) + self._channel.queue_bind(queue=self._queue_name, + exchange=EXCHANGE_NAME) + else: + self._queue_name = cast(str, tx_queue_name) + + def rx_queue_name(self) -> str: + """ Returns quyeue name for RX connection """ + assert self._for_rx + return self._queue_name + + def send_response(self, req_cfg_digest: str, request: Optional[str], + response: Optional[str]) -> None: + """ Send computed AFC Response + + Arguments: + req_cfg_digest -- Request/config digest that identifies request + request -- Request as a string. None if cache update performed + by sender + response -- Response as a string. None on failure + """ + assert not self._for_rx + assert self._channel is not None + ex: Exception + try: + self._channel.tx_select() + self._channel.basic_publish( + exchange=EXCHANGE_NAME, routing_key=self._queue_name, + body=RmqReqRespKey( + afc_req=request, afc_resp=response, + req_cfg_digest=req_cfg_digest).json(), + properties=pika.BasicProperties( + content_type="application/json", + delivery_mode=pika.DeliveryMode.Transient), + mandatory=False) + self._channel.tx_commit() + except pydantic.ValidationError as ex: + error(f"Invalid arguments: {repr(ex)}") + except pika.exceptions.AMQPError as ex: + error(f"RabbitMQ send failed: {repr(ex)}") + + def receive_responses(self, req_cfg_digests: List[str], + timeout_sec: float) -> List[RmqReqRespKey]: + """ Receive AFC responses + + Arguments: + req_cfg_digests -- Request/config digests of expected responses + timeout_sec -- Timeout in seconds + Returns list of request(optional)/response/digest triplets + """ + assert self._for_rx + assert self._connection is not None + assert self._channel is not None + remaining_responses: Set[str] = set(req_cfg_digests) + ret: List[RmqReqRespKey] = [] + timer_id: Optional[int] = None + ex: Exception + try: + if timeout_sec: + timer_id = \ + self._connection.call_later(timeout_sec, + self._channel.cancel) + body: bytes + for _, _, body in \ + self._channel.consume( + queue=self._queue_name, auto_ack=True, exclusive=True): + try: + rrk = RmqReqRespKey.parse_raw(body) + except pydantic.ValidationError as ex: + LOGGER.error(f"Decode error on AFC Response Info " + f"arrived from Worker: {ex}") + continue + if rrk.req_cfg_digest in remaining_responses: + remaining_responses.remove(rrk.req_cfg_digest) + ret.append(rrk) + if not remaining_responses: + self._channel.cancel() + self._connection.remove_timeout(timer_id) + return ret + except pika.exceptions.AMQPError as ex: + error(f"RabbitMQ receive failed: {repr(ex)}") + return [] # Will never happen, appeasing pylint + + def close(self) -> None: + """ Closing RabbitMQ connection """ + try: + if self._for_rx and (self._channel is not None): + self._channel.queue_delete(self._queue_name) + except pika.exceptions.AMQPError: + pass + try: + if self._connection is not None: + self._connection.close() + self._connection = None + self._channel = None + except pika.exceptions.AMQPError: + pass + + def __enter__(self) -> "RcacheRmqConnection": + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + self.close() + + +class RcacheRmq: + """ RabbitMQ synchronous sender/receiver + + Public attributes: + rmq_dsn -- RabbitMQ AMQP URI + + Private attributes: + _url_params -- Connection parameters + """ + + def __init__(self, rmq_dsn: str) -> None: + """ Constructor + + Arguments: + rmq_dsn -- RabbitMQ AMQP URI + as_receiver -- True if will be used for RX, false if for TX + """ + self.rmq_dsn = rmq_dsn + try: + self._url_params = pika.URLParameters(self.rmq_dsn) + except pika.exceptions.AMQPError as ex: + error(f"RabbitMQ URL '{safe_dsn(self.rmq_dsn)}' has invalid " + f"syntax: {ex}") + + def create_connection(self, tx_queue_name: Optional[str] = None) \ + -> RcacheRmqConnection: + """ Creates and returns connection context manager """ + return \ + RcacheRmqConnection( + url_params=self._url_params, tx_queue_name=tx_queue_name) diff --git a/src/afc-packages/rcache/setup.py b/src/afc-packages/rcache/setup.py new file mode 100644 index 0000000..d734f1e --- /dev/null +++ b/src/afc-packages/rcache/setup.py @@ -0,0 +1,26 @@ +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +from setuptools import setup +from setuptools.command.install import install + + +class InstallCmdWrapper(install): + def run(self): + install.run(self) + + +setup( + name='rcache', + # Label compatible with PEP 440 + version='0.1.0', + description='AFC packages', + py_modules=["rcache_client", "rcache_common", "rcache_db", "rcache_models", + "rcache_rcache", "rcache_rmq"], + cmdclass={ + 'install': InstallCmdWrapper, + } +) diff --git a/src/afclogging/CMakeLists.txt b/src/afclogging/CMakeLists.txt new file mode 100644 index 0000000..c22f8cf --- /dev/null +++ b/src/afclogging/CMakeLists.txt @@ -0,0 +1,11 @@ +# All source files to same target +set(TGT_NAME "afclogging") + +file(GLOB ALL_CPP "*.cpp") +file(GLOB ALL_HEADER "*.h") +add_dist_library(TARGET ${TGT_NAME} SOURCES ${ALL_CPP} HEADERS ${ALL_HEADER} EXPORTNAME fbratTargets) +set(TARGET_LIBS ${TARGET_LIBS} ${TGT_NAME} PARENT_SCOPE) + +target_link_libraries(${TGT_NAME} PUBLIC Qt5::Core) +target_link_libraries(${TGT_NAME} PUBLIC Boost::log) +target_link_libraries(${TGT_NAME} PUBLIC Boost::regex) diff --git a/src/afclogging/ErrStream.cpp b/src/afclogging/ErrStream.cpp new file mode 100644 index 0000000..3ef46eb --- /dev/null +++ b/src/afclogging/ErrStream.cpp @@ -0,0 +1,12 @@ +// + +#include "ErrStream.h" + +ErrStream::ErrStream() +{ +} + +ErrStream::operator QString() const +{ + return QString::fromStdString(_str.str()); +} diff --git a/src/afclogging/ErrStream.h b/src/afclogging/ErrStream.h new file mode 100644 index 0000000..b2747a0 --- /dev/null +++ b/src/afclogging/ErrStream.h @@ -0,0 +1,78 @@ +// + +#ifndef SRC_AFCLOGGING_ERRSTREAM_H_ +#define SRC_AFCLOGGING_ERRSTREAM_H_ + +#include "afclogging_export.h" +#include "QtStream.h" +#include + +class QString; + +/** A helper class to define exception text in-line with the object + * constructor. + * + * Example use is: + * @code + * throw std::logic_error(ErrStream() << "some message: " << errCode); + * @endcode + */ +class AFCLOGGING_EXPORT ErrStream +{ + public: + /** Initialize to an empty string. + */ + ErrStream(); + + /** Implicitly convert to std::string for exceptions. + * @return The string representation. + */ + operator std::string() const + { + return _str.str(); + } + + /** Implicitly convert to QString for processing. + * @return The text representation. + */ + operator QString() const; + + /** Append to stream and keep this class type for final conversion. + * + * @tparam The type to append. + * @param val The value to append. + * @return This updated stream. + */ + template + ErrStream &operator<<(const T &val) + { + _str << val; + return *this; + } + + ///@{ + /** Handle stream manipulation functions. + * + * @tparam The stream type to manipulate. + * @param func The manipulator to apply. + * @return This updated stream. + */ + template + ErrStream &operator<<(T &(*func)(T &)) + { + _str << func; + return *this; + } + ErrStream &operator<<(std::ostream &(*func)(std::ostream &)) + { + _str << func; + return *this; + } + ///@} + + private: + /// string storage + std::ostringstream _str; +}; + +#endif /* SRC_AFCLOGGING_ERRSTREAM_H_ */ diff --git a/src/afclogging/Logging.cpp b/src/afclogging/Logging.cpp new file mode 100644 index 0000000..7b15409 --- /dev/null +++ b/src/afclogging/Logging.cpp @@ -0,0 +1,23 @@ +// + +#include "Logging.h" + +namespace logging = boost::log; + +namespace +{ +/// Logger for messages related to Logging namespace +LOGGER_DEFINE_GLOBAL(logger, "Logging"); + +} + +Logging::logger_mt &Logging::getLoggerInstance() +{ + return logger::get(); +} + +void Logging::flush() +{ + boost::shared_ptr core = logging::core::get(); + core->flush(); +} diff --git a/src/afclogging/Logging.h b/src/afclogging/Logging.h new file mode 100644 index 0000000..e4e5d60 --- /dev/null +++ b/src/afclogging/Logging.h @@ -0,0 +1,151 @@ +// + +#ifndef LOGGING_H_ +#define LOGGING_H_ + +#include "LoggingSeverityLevel.h" +#include "QtStream.h" +#include +#include +#include +#include +#include +#include +#include +#include + +/** Utility functions for interacting with boost::log library. + * + * Typical thread-safe use of this library is in the example: + * @code +namespace { +/// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "logname") +} +... +void func(){ + LOGGER_INFO(logger) << "some message " << with_data; +} + * @endcode + */ +namespace Logging +{ +/// the type of the channel name +using channel_name_type = std::string; + +/// Convert severity to text +std::ostream &operator<<(std::ostream &stream, const severity_level &val); + +/// Convenience name for single-thread logger class +typedef boost::log::sources::severity_channel_logger< + severity_level, // the type of the severity level + channel_name_type // the type of the channel name + > + logger_st; + +/// Convenience name for multi-thread-safe logger class +typedef boost::log::sources::severity_channel_logger_mt< + severity_level, // the type of the severity level + channel_name_type // the type of the channel name + > + logger_mt; + +/** Access the root logger, which always passes the record filter. + * + * @return The root logger instance with channel name "Logging". + */ +logger_mt &getLoggerInstance(); + +/** Flush all current logging sinks. + * @note Logging is inhibited during the flush. + */ +void flush(); + +/** An RAII class to call Logging::flush() in its destructor. + */ +class Flusher +{ + public: + /// Do nothing + Flusher() + { + } + /// Flush the logs + ~Flusher() + { + Logging::flush(); + } +}; + +} // End namespace + +/** Define a global thread-safe logger instance. + * + * @param tag_name The object name used to identify the logger. + * @param chan_name The channel name to include with log messages. + * @sa LOG_SEV, LOGGER_DEBUG, LOGGER_INFO, LOGGER_WARN, LOGGER_ERROR + */ +#define LOGGER_DEFINE_GLOBAL(tag_name, chan_name) \ + BOOST_LOG_INLINE_GLOBAL_LOGGER_INIT(tag_name, Logging::logger_mt) \ + { \ + namespace keywords = boost::log::keywords; \ + return Logging::logger_mt(keywords::channel = chan_name); \ + } + +/** Log at Logging::LOG_DEBUG level. + * @param inst The logger instance to write to. + */ +#define LOGINST_DEBUG(inst) BOOST_LOG_SEV(inst, Logging::LOG_DEBUG) + +/** Log at Logging::LOG_INFO level. + * @param inst The logger instance to write to. + */ +#define LOGINST_INFO(inst) BOOST_LOG_SEV(inst, Logging::LOG_INFO) + +/** Log at Logging::LOG_WARN level. + * @param inst The logger instance to write to. + */ +#define LOGINST_WARN(inst) BOOST_LOG_SEV(inst, Logging::LOG_WARN) + +/** Log at Logging::ERROR level. + * @param inst The logger instance to write to. + */ +#define LOGINST_ERROR(inst) BOOST_LOG_SEV(inst, Logging::LOG_ERROR) + +/** Log at Logging::LOG_CRIT level. + * @param inst The logger instance to write to. + */ +#define LOGINST_CRIT(inst) BOOST_LOG_SEV(inst, Logging::LOG_CRIT) + +/** Log at arbitrary severity level. + * @param tag_name The object name used to identify the logger. + * @param severity The specific severity level. + */ +#define LOG_SEV(tag_name, severity) BOOST_LOG_SEV(tag_name::get(), severity) + +/** Log at Logging::DEBUG level. + * @param tag_name The object name used to identify the logger. + */ +#define LOGGER_DEBUG(tag_name) LOG_SEV(tag_name, Logging::LOG_DEBUG) + +/** Log at Logging::INFO level. + * @param tag_name The object name used to identify the logger. + */ +#define LOGGER_INFO(tag_name) LOG_SEV(tag_name, Logging::LOG_INFO) + +/** Log at Logging::WARN level. + * @param tag_name The object name used to identify the logger. + */ +#define LOGGER_WARN(tag_name) LOG_SEV(tag_name, Logging::LOG_WARN) + +/** Log at Logging::ERROR level. + * @param tag_name The object name used to identify the logger. + */ +#define LOGGER_ERROR(tag_name) LOG_SEV(tag_name, Logging::LOG_ERROR) + +/** Log at Logging::CRIT level. + * @param tag_name The object name used to identify the logger. + */ +#define LOGGER_CRIT(tag_name) LOG_SEV(tag_name, Logging::LOG_CRIT) + +#endif /* LOGGING_H_ */ diff --git a/src/afclogging/LoggingConfig.cpp b/src/afclogging/LoggingConfig.cpp new file mode 100644 index 0000000..b25648c --- /dev/null +++ b/src/afclogging/LoggingConfig.cpp @@ -0,0 +1,319 @@ +// + +#include "LoggingConfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// allows for backwards compatability with older version of Boost. +#if BOOST_VERSION >= 105600 + #include + #define BOOST_NULL_DELETER boost::null_deleter() +#else + #include + #define BOOST_NULL_DELETER boost::log::empty_deleter() +#endif + +namespace logging = boost::log; +namespace attrs = boost::log::attributes; +namespace src = boost::log::sources; +namespace sinks = boost::log::sinks; +namespace keywords = boost::log::keywords; +namespace expr = boost::log::expressions; + +namespace +{ +typedef std::pair LevelName; +typedef std::map LevelMap; + +/// Fixed list of names for all severities +const LevelMap levelNames({ + {Logging::LOG_DEBUG, "debug"}, + {Logging::LOG_INFO, "info"}, + {Logging::LOG_WARN, "warning"}, + {Logging::LOG_ERROR, "error"}, + {Logging::LOG_CRIT, "critical"}, +}); + +/** Compare strings for prefix subset. + * @param ref The long string to compare. + * @param pre The short string to check for. + * @return True if the @c ref string begins with @c pre string. + */ +bool startswith(const std::string &ref, const std::string &pre) +{ + if (pre.size() > ref.size()) { + return false; + } + // true if there is at least one matching character + return (std::mismatch(pre.begin(), pre.end(), ref.begin()).second != ref.begin()); +} + +/** A combination of filter conditions to be checked for each message. + */ +class AttrFilterSet +{ + public: + /// Boost intrinsic functional type + using Filter = boost::log::aux::light_function; + + AttrFilterSet() + { + _rootChannel = Logging::getLoggerInstance().channel(); + } + + /** Apply the filtering to a message. + * + * @param attrs The message attributes. + * @return True if all conditions are true. + */ + bool operator()(boost::log::attribute_value_set const &attrs) const + { + { // show the root logger channel unconditionally + const auto chanIt = attrs.find(Logging::channel_type::get_name()); + if (chanIt != attrs.end()) { + if (chanIt->second.extract() == _rootChannel) { + return true; + } + } + } + for (const auto &part : conditions) { + if (!part(attrs)) { + return false; + } + } + return true; + } + + /// The actual filter conditions to check + std::vector conditions; + + private: + std::string _rootChannel; +}; + +/// Unchanging text formatter +const boost::log::formatter textFormatter = (expr::stream + // Include a timestamp for console output + << expr::format_date_time( + Logging::utctimestamp_type::get_name(), + "%Y-%m-%d %H:%M:%S.%fZ") + << " " + << "TH:" << Logging::thread_id << " " + << "<" << Logging::severity << "> " << Logging::channel + << ": " << Logging::message); +/// Initial #curConfig has been set +bool hasConfig = false; +/// Copy of running configuration +Logging::Config curConfig; +/// Boost version of #curConfig.useStream +boost::shared_ptr extRef; +} + +std::ostream &Logging::operator<<(std::ostream &stream, const severity_level &val) +{ + stream << levelNames.at(val); + return stream; +} + +Logging::Filter::NameError::NameError(const std::string &msg) : logic_error(msg) +{ +} + +Logging::Filter::Filter() : leastLevel(Logging::LOG_DEBUG) +{ +} + +void Logging::Filter::setLevel(const std::string &val) +{ + LevelMap found = levelNames; + for (auto it = found.begin(); it != found.end();) { + // first-byte mismatch indicates no-match + if (!startswith(it->second, val)) { + it = found.erase(it); + } else { + ++it; + } + } + + if (found.empty()) { + throw NameError("Invalid log filter \"" + val + "\""); + } + if (found.size() > 1) { + throw NameError("Non-unique log filter \"" + val + "\""); + } + leastLevel = found.begin()->first; +} + +#if !defined(BOOST_LOG_WITHOUT_SYSLOG) +Logging::SyslogConfig::SyslogConfig() : facility(sinks::syslog::user) +{ +} +#endif + +#if !defined(BOOST_LOG_WITHOUT_EVENT_LOG) +Logging::WinlogConfig::WinlogConfig() +{ +} +#endif + +Logging::Config::Config() : useStdOut(false), useStdErr(true) +{ +#if !defined(BOOST_LOG_WITHOUT_SYSLOG) + useSyslog = false; +#endif +#if !defined(BOOST_LOG_WITHOUT_EVENT_LOG) + useWinlog = false; +#endif +} + +const boost::log::formatter &Logging::getTextFormatter() +{ + return textFormatter; +} + +Logging::Config Logging::currentConfig() +{ + return curConfig; +} + +void Logging::initialize(const Logging::Config &config) +{ + boost::shared_ptr core = logging::core::get(); + // clear initial state + core->remove_all_sinks(); + int sinkCount = 0; + + // Fixed attributes + core->add_global_attribute(Logging::utctimestamp_type::get_name(), attrs::utc_clock()); + core->add_global_attribute(Logging::thread_id_type::get_name(), attrs::current_thread_id()); + + // Overall message filter for all sinks + { + AttrFilterSet filter; + filter.conditions.push_back(Logging::severity >= config.filter.leastLevel); + + for (const auto &pat : config.filter.channelInclude) { + filter.conditions.push_back(expr::matches(Logging::channel, pat)); + } + for (const auto &pat : config.filter.channelExclude) { + filter.conditions.push_back(!expr::matches(Logging::channel, pat)); + } + + core->set_filter(filter); + } + + if (config.useStdOut || config.useStdErr) { + typedef sinks::text_ostream_backend backend_t; + typedef sinks::synchronous_sink sink_t; + + auto backend = boost::make_shared(); + + // Using empty_deleter to avoid destroying global stream objects + if (config.useStdOut) { + boost::shared_ptr stream(&std::cout, BOOST_NULL_DELETER); + backend->add_stream(stream); + } + if (config.useStdErr) { + boost::shared_ptr stream(&std::cerr, BOOST_NULL_DELETER); + backend->add_stream(stream); + } + + auto sink = boost::make_shared(backend); + sink->set_formatter(textFormatter); + + core->add_sink(sink); + ++sinkCount; + } +#if !defined(BOOST_LOG_WITHOUT_SYSLOG) + if (config.useSyslog && config.syslogConfig) { + typedef sinks::syslog_backend backend_t; + typedef sinks::synchronous_sink sink_t; + + auto backend = boost::make_shared( + keywords::facility = config.syslogConfig->facility, + keywords::ident = config.syslogConfig->identity); + + { + sinks::syslog::custom_severity_mapping mapper("Severity"); + mapper[Logging::LOG_DEBUG] = sinks::syslog::debug; + mapper[Logging::LOG_INFO] = sinks::syslog::info; + mapper[Logging::LOG_WARN] = sinks::syslog::warning; + mapper[Logging::LOG_ERROR] = sinks::syslog::error; + mapper[Logging::LOG_CRIT] = sinks::syslog::critical; + backend->set_severity_mapper(mapper); + } + + auto sink = boost::make_shared(backend); + // syslog includes its own time stamp + core->add_sink(sink); + ++sinkCount; + } +#endif +#if !defined(BOOST_LOG_WITHOUT_EVENT_LOG) + if (config.useWinlog && config.winlogConfig) { + typedef sinks::simple_event_log_backend backend_t; + typedef sinks::synchronous_sink sink_t; + + auto backend = boost::make_shared( + keywords::log_source = config.winlogConfig->identity); + { + sinks::event_log::custom_event_type_mapping mapping("Severi" + "ty"); + mapping[Logging::LOG_DEBUG] = sinks::event_log::success; + mapping[Logging::LOG_INFO] = sinks::event_log::info; + mapping[Logging::LOG_WARN] = sinks::event_log::warning; + mapping[Logging::LOG_ERROR] = sinks::event_log::error; + mapping[Logging::LOG_CRIT] = sinks::event_log::error; + backend->set_event_type_mapper(mapping); + } + + auto sink = boost::make_shared(backend); + sink->set_formatter(textFormatter); + + core->add_sink(sink); + ++sinkCount; + } +#endif + if (config.useStream) { + typedef sinks::text_ostream_backend backend_t; + typedef sinks::synchronous_sink sink_t; + + // keep the reference in #extStream but do not manage memory + extRef.reset(config.useStream->stream.get(), BOOST_NULL_DELETER); + auto backend = boost::make_shared(); + backend->add_stream(extRef); + backend->auto_flush(config.useStream->autoFlush); + + auto sink = boost::make_shared(backend); + sink->set_formatter(textFormatter); + + core->add_sink(sink); + ++sinkCount; + } else { + extRef.reset(); + } + + // Zero sinks triggers default logging behavior, need to disable here + core->set_logging_enabled(sinkCount > 0); + + if (!hasConfig || (config.filter.leastLevel != curConfig.filter.leastLevel)) { + LOGINST_INFO(getLoggerInstance()) + << "Logging at level " << config.filter.leastLevel; + } + curConfig = config; + hasConfig = true; +} diff --git a/src/afclogging/LoggingConfig.h b/src/afclogging/LoggingConfig.h new file mode 100644 index 0000000..1740c4d --- /dev/null +++ b/src/afclogging/LoggingConfig.h @@ -0,0 +1,165 @@ +// + +#ifndef LOGGING_CONFIG_H_ +#define LOGGING_CONFIG_H_ + +#include "Logging.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Logging +{ + +/// Register timestamp attribute +BOOST_LOG_ATTRIBUTE_KEYWORD(utctimestamp, + "UtcTimeStamp", + boost::log::attributes::utc_time_traits::time_type) +/// Register severity attribute +BOOST_LOG_ATTRIBUTE_KEYWORD(severity, + boost::log::aux::default_attribute_names::severity(), + severity_level) +/// Register channel type attribute +BOOST_LOG_ATTRIBUTE_KEYWORD(channel, + boost::log::aux::default_attribute_names::channel(), + channel_name_type) +/// Register thread ID type attribute +BOOST_LOG_ATTRIBUTE_KEYWORD(thread_id, + boost::log::aux::default_attribute_names::thread_id(), + boost::log::attributes::current_thread_id::value_type) +/// Register message-text attribute type +using message_type = boost::log::expressions::smessage_type; +/// Register message-text attribute +const message_type message = {}; + +/** Encapsulate configuration of a log-level filter value. + */ +class Filter +{ + public: + struct NameError : public std::logic_error { + NameError(const std::string &msg); + }; + + /// Default filter is at minimum level (i.e. allow all messages) + Filter(); + + /** Extract filter level from a configuration string. + * @param val The value to take from. + * This is either a Net-SNMP-style level string prefix: + * - critical + * - error + * - warning + * - info + * - debug + * For example, the values "E", "ERR", and "ERROR" all match error level. + * @throw NameError if the value is not a log level specifier. + */ + void setLevel(const std::string &val); + + /// The least-severe level allowed by the filter + severity_level leastLevel; + /// Included patterns for channel names + std::list channelInclude; + /// Excluded patterns for channel names + std::list channelExclude; +}; + +/// Individual out-stream configuration +struct OStreamConfig { + /// Default configuration + OStreamConfig() + { + } + + /// Original file name associated with #stream (if applicable) + std::string fileName; + /// The stream to write to + std::shared_ptr stream; + /// True if the stream is flushed after each log record + bool autoFlush = false; +}; + +#if !defined(BOOST_LOG_WITHOUT_SYSLOG) +/// Syslog-specific configuration +struct SyslogConfig { + /// Default configuration + SyslogConfig(); + + /// The particular 'facility' to log as + boost::log::sinks::syslog::facility facility; + /// The local identity to use for logging + std::string identity; +}; +#endif + +#if !defined(BOOST_LOG_WITHOUT_EVENT_LOG) +/// Windows event log-specific configuration +struct WinlogConfig { + /// Default configuration + WinlogConfig(); + + /// The local identity to use for logging + std::string identity; +}; +#endif + +/// Logging configuration details +struct Config { + /** Default configuration. + * Logging is sent to @c stderr only. + */ + Config(); + + /// If true, output will be sent to @c stdout. + bool useStdOut; + /// If true, output will be sent to @c stderr. + bool useStdErr; +#if !defined(BOOST_LOG_WITHOUT_SYSLOG) + /// If true and #syslogConfig is set, output will be sent to @c syslog facility. + bool useSyslog; + /// Configuration to use iff #useSyslog is true + std::shared_ptr syslogConfig; +#endif +#if !defined(BOOST_LOG_WITHOUT_EVENT_LOG) + /// If true and #winlogConfig is set, output will be sent to windows event log. + bool useWinlog; + /// Configuration to use iff #useWinlog is true + std::shared_ptr winlogConfig; +#endif + /// If non-null, output will be appended to this stream + std::shared_ptr useStream; + + /// Filter for log events + Filter filter; +}; + +/** Get the record formatter used for text sinks. + * + * @return The formatter object + */ +const boost::log::formatter &getTextFormatter(); + +/** Get the current running configuration. + * + * @return Current config content.s + */ +Config currentConfig(); + +/** Initialize the default appenders. + * @param config The new logging configuration. + */ +void initialize(const Config &config = Config()); + +} // End namespace + +#endif /* LOGGING_CONFIG_H_ */ diff --git a/src/afclogging/LoggingScopedSink.cpp b/src/afclogging/LoggingScopedSink.cpp new file mode 100644 index 0000000..06bc81f --- /dev/null +++ b/src/afclogging/LoggingScopedSink.cpp @@ -0,0 +1,16 @@ +#include "LoggingScopedSink.h" +#include + +Logging::ScopedSink::ScopedSink(const boost::shared_ptr &sinkVal) : + sink(sinkVal) +{ + boost::shared_ptr core = boost::log::core::get(); + core->set_logging_enabled(true); + core->add_sink(sink); +} + +Logging::ScopedSink::~ScopedSink() +{ + boost::shared_ptr core = boost::log::core::get(); + core->remove_sink(sink); +} diff --git a/src/afclogging/LoggingScopedSink.h b/src/afclogging/LoggingScopedSink.h new file mode 100644 index 0000000..b711ea6 --- /dev/null +++ b/src/afclogging/LoggingScopedSink.h @@ -0,0 +1,32 @@ + +#ifndef SRC_AFCLOGGING_LOGGINGSCOPEDSINK_H_ +#define SRC_AFCLOGGING_LOGGINGSCOPEDSINK_H_ + +#include +#include + +namespace Logging +{ + +/** An RAII class to manage the lifetime of a boost::log sink. + */ +class ScopedSink +{ + public: + /** Attach the sink. + * + * @param sink The sink to manage: + */ + ScopedSink(const boost::shared_ptr &sink); + + /** Detach the sink. + */ + ~ScopedSink(); + + /// The managed sink + boost::shared_ptr sink; +}; + +} // End namespace + +#endif /* SRC_AFCLOGGING_LOGGINGSCOPEDSINK_H_ */ diff --git a/src/afclogging/LoggingSeverityLevel.h b/src/afclogging/LoggingSeverityLevel.h new file mode 100644 index 0000000..72046a2 --- /dev/null +++ b/src/afclogging/LoggingSeverityLevel.h @@ -0,0 +1,22 @@ +// + +#ifndef LOGGING_SEVERITY_LEVEL_H +#define LOGGING_SEVERITY_LEVEL_H + +namespace Logging +{ + +/** Levels of severity for logging messages. + * Associated numbers increase with increasing severity. + */ +enum severity_level { + LOG_DEBUG = 0, //!< DEBUG + LOG_INFO = 10, //!< INFO + LOG_WARN = 20, //!< WARN + LOG_ERROR = 30, //!< ERROR + LOG_CRIT = 40 //!< CRIT +}; + +} // End namespace Logging + +#endif /* LOGGING_SEVERITY_LEVEL_H */ diff --git a/src/afclogging/QtStream.cpp b/src/afclogging/QtStream.cpp new file mode 100644 index 0000000..b119365 --- /dev/null +++ b/src/afclogging/QtStream.cpp @@ -0,0 +1,168 @@ +// + +#include "QtStream.h" +#include "Logging.h" +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +/// Redirection logger for Qt +LOGGER_DEFINE_GLOBAL(logger, "Qt") + +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) +/** Redirect Qt logging. + * + * @param type Severity level of the message. + * @param context + * @param msg The message text. + * @sa qt_logger + */ +void qtRedirect(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + Logging::severity_level lvl; + switch (type) { + case QtDebugMsg: + lvl = Logging::LOG_DEBUG; + break; + case QtInfoMsg: + lvl = Logging::LOG_INFO; + break; + case QtWarningMsg: + lvl = Logging::LOG_WARN; + break; + default: + lvl = Logging::LOG_ERROR; + break; + } + LOG_SEV(logger, lvl) << msg.toStdString(); +} +#endif /* QT_VERSION */ + +} + +void QtStream::installLogHandler() +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + qInstallMessageHandler(qtRedirect); +#endif +} + +std::ostream &operator<<(std::ostream &stream, const QObject *val) +{ + if (val) { + const QMetaObject *const meta = val->metaObject(); + stream << meta->className() << "(" << reinterpret_cast(val) << ")"; + } else { + stream << "QObject(null)"; + } + return stream; +} + +std::ostream &operator<<(std::ostream &stream, const QString &val) +{ + const bool extra = stream.flags() & std::ios_base::showbase; + if (extra) { + stream << "QString("; + if (val.isNull()) { + stream << "null"; + } else { + stream << '"' << val.toUtf8().constData() << '"'; + } + stream << ")"; + } else { + stream << val.toUtf8().constData(); + } + return stream; +} + +std::ostream &operator<<(std::ostream &stream, const QLatin1String &val) +{ + const bool extra = stream.flags() & std::ios_base::showbase; + if (extra) { + stream << "QLatin1String("; + if (val.data() == nullptr) { + stream << "null"; + } else { + stream << '"' << val.data() << '"'; + } + stream << ")"; + } else { + stream << val.data(); + } + return stream; +} + +std::ostream &operator<<(std::ostream &stream, const QStringRef &val) +{ + const bool extra = stream.flags() & std::ios_base::showbase; + if (extra) { + stream << "QStringRef(\""; + } + stream << val.toUtf8().constData(); + if (extra) { + stream << "\")"; + } + return stream; +} + +std::ostream &operator<<(std::ostream &stream, const QByteArray &val) +{ + const bool inhex = stream.flags() & std::ios_base::hex; + QByteArray show; + if (inhex) { + show = val.toHex(); + } else { + show = val; // Qt reference copy + } + + const bool extra = stream.flags() & std::ios_base::showbase; + if (extra) { + stream << "QByteArray("; + if (val.isNull()) { + stream << "null"; + } else { + if (!inhex) { + stream << '"'; + } + stream << show.constData(); + if (!inhex) { + stream << '"'; + } + } + stream << ")"; + } else { + stream << show.constData(); + } + return stream; +} + +std::ostream &operator<<(std::ostream &stream, const QUuid &val) +{ + stream << val.toString(); + return stream; +} + +std::ostream &operator<<(std::ostream &stream, const QJsonValue &val) +{ + return QtStream::out_qt(stream, val); +} +std::ostream &operator<<(std::ostream &stream, const QJsonArray &val) +{ + return QtStream::out_qt(stream, val); +} +std::ostream &operator<<(std::ostream &stream, const QJsonObject &val) +{ + return QtStream::out_qt(stream, val); +} + +std::ostream &operator<<(std::ostream &stream, const QVersionNumber &val) +{ + return QtStream::out_qt(stream, val); +} diff --git a/src/afclogging/QtStream.h b/src/afclogging/QtStream.h new file mode 100644 index 0000000..3afeb2b --- /dev/null +++ b/src/afclogging/QtStream.h @@ -0,0 +1,119 @@ +// + +#ifndef QTSTREAM_H_ +#define QTSTREAM_H_ + +#include +#include + +class QObject; +class QString; +class QUuid; +class QDateTime; +class QVariant; +class QJsonValue; +class QJsonArray; +class QJsonObject; +template +class QList; +template +class QMap; +typedef QMap QVariantMap; +class QVersionNumber; + +namespace QtStream +{ + +/** Install a local log handler. + * The log channel name is "Qt". + */ +void installLogHandler(); + +/** Write a Qt value to an @c ostream via a QDebug stream. + * + * @param stream The stream to write. + * @param value The value to output. + * @return The updated stream. + */ +template +::std::ostream &out_qt(::std::ostream &stream, const Typ &value) +{ + QString str; + QDebug(&str).nospace() << value; + stream << str.toStdString(); + return stream; +} + +/** Convenience for logging stl-style containers. + * + * @param stream The stream to write to. + * @param val The value to write. + * @param className The class identifier name. + * @return The updated stream. + */ +template +::std::ostream &out_stl_container(::std::ostream &stream, + const Container &val, + const ::std::string &className) +{ + stream << className << "[size=" << val.size() << "]("; + for (auto it = val.begin(); it != val.end(); ++it) { + if (it != val.begin()) { + stream << ","; + } + stream << *it; + } + stream << ")"; + return stream; +} + +} + +QT_BEGIN_NAMESPACE + +///@{ +/** Convenience for logging Qt values. + * + * @param stream The stream to write to. + * @param val The value to write. + * @return The updated stream. + */ +::std::ostream &operator<<(::std::ostream &stream, const QObject *val); +::std::ostream &operator<<(::std::ostream &stream, const QString &val); +::std::ostream &operator<<(::std::ostream &stream, const QLatin1String &val); +::std::ostream &operator<<(::std::ostream &stream, const QStringRef &val); +::std::ostream &operator<<(::std::ostream &stream, const QByteArray &val); +::std::ostream &operator<<(::std::ostream &stream, const QUuid &val); +::std::ostream &operator<<(::std::ostream &stream, const QJsonValue &val); +::std::ostream &operator<<(::std::ostream &stream, const QJsonArray &val); +::std::ostream &operator<<(::std::ostream &stream, const QJsonObject &val); +::std::ostream &operator<<(::std::ostream &stream, const QVersionNumber &val); +template +::std::ostream &operator<<(::std::ostream &stream, const QList &val) +{ + return QtStream::out_stl_container(stream, val, "QList"); +} +template +::std::ostream &operator<<(::std::ostream &stream, const QVector &val) +{ + return QtStream::out_stl_container(stream, val, "QVector"); +} +template +::std::ostream &operator<<(::std::ostream &stream, const QSet &val) +{ + return QtStream::out_stl_container(stream, val, "QSet"); +} +///@} + +#define LOG_STREAM_WRAP_QT(QtType) \ + inline ::std::ostream &operator<<(::std::ostream &stream, const QtType &val) \ + { \ + return QtStream::out_qt(stream, val); \ + } +LOG_STREAM_WRAP_QT(QDateTime) +LOG_STREAM_WRAP_QT(QVariant) +LOG_STREAM_WRAP_QT(QVariantMap) + +QT_END_NAMESPACE + +#endif /* QTSTREAM_H_ */ diff --git a/src/afclogging/test/CMakeLists.txt b/src/afclogging/test/CMakeLists.txt new file mode 100644 index 0000000..2e0b131 --- /dev/null +++ b/src/afclogging/test/CMakeLists.txt @@ -0,0 +1,5 @@ +# All source files to same target +file(GLOB ALL_CPP "*.cpp") +add_gtest_executable(${TGT_NAME}-test ${ALL_CPP}) +target_link_libraries(${TGT_NAME}-test PRIVATE ${TGT_NAME}) +target_link_libraries(${TGT_NAME}-test PRIVATE gtest_main) diff --git a/src/afclogging/test/TestErrStream.cpp b/src/afclogging/test/TestErrStream.cpp new file mode 100644 index 0000000..be8e643 --- /dev/null +++ b/src/afclogging/test/TestErrStream.cpp @@ -0,0 +1,40 @@ +// + +#include "../ErrStream.h" +#include + +TEST(TestErrStream, testSimpleOutput) +{ + ErrStream str; + str << "test"; + + ASSERT_EQ(std::string("test"), std::string(str)); + ASSERT_EQ(QString("test"), QString(str)); +} + +TEST(TestErrStream, testFormattedOutput) +{ + ErrStream str; + str << double(3.45); + + ASSERT_EQ(std::string("3.45"), std::string(str)); + ASSERT_EQ(QString("3.45"), QString(str)); +} + +TEST(TestErrStream, testStreamFunc) +{ + ErrStream str; + str << "test" << std::flush; + + ASSERT_EQ(std::string("test"), std::string(str)); + ASSERT_EQ(QString("test"), QString(str)); +} + +TEST(TestErrStream, testIoManip) +{ + ErrStream str; + str << std::setprecision(2) << double(3.45); + + ASSERT_EQ(std::string("3.5"), std::string(str)); + ASSERT_EQ(QString("3.5"), QString(str)); +} diff --git a/src/afclogging/test/TestLogging.cpp b/src/afclogging/test/TestLogging.cpp new file mode 100644 index 0000000..6f68ef3 --- /dev/null +++ b/src/afclogging/test/TestLogging.cpp @@ -0,0 +1,202 @@ +// + +#include "../Logging.h" +#include "../LoggingConfig.h" +#include +#include + +namespace +{ +std::istream &operator>>(std::istream &stream, std::string &dest) +{ + std::string buf; + std::getline(stream, dest); + return stream; +} + +std::shared_ptr toStream(const std::shared_ptr &stream) +{ + auto config = std::make_shared(); + config->stream = stream; + config->autoFlush = true; + return config; +} + +const auto failmask = std::ios_base::failbit | std::ios_base::badbit | std::ios_base::eofbit; + +/// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(loggerA, "TestLoggingA") +LOGGER_DEFINE_GLOBAL(loggerB, "TestLoggingB") + +} + +class TestLogging : public testing::Test +{ + public: +}; + +TEST_F(TestLogging, testFilterDef) +{ + Logging::Filter filt; + filt.setLevel("deb"); + ASSERT_EQ(filt.leastLevel, Logging::LOG_DEBUG); + + filt.setLevel("w"); + ASSERT_EQ(filt.leastLevel, Logging::LOG_WARN); +} + +TEST_F(TestLogging, testTargets) +{ + Logging::Config conf; + conf.useStdOut = false; + conf.useStdErr = false; + conf.useSyslog = false; + + Logging::initialize(conf); + + { + Logging::Config other(conf); + other.useStdOut = true; + Logging::initialize(other); + } + { + Logging::Config other(conf); + other.useStdErr = true; + Logging::initialize(other); + } + { + Logging::Config other(conf); + other.useSyslog = true; + Logging::initialize(other); + } +} + +TEST_F(TestLogging, testIntercept) +{ + auto outbuf = std::make_shared(); + outbuf->exceptions(failmask); + + Logging::Config conf; + conf.useStdOut = false; + conf.useStdErr = false; + conf.useStream = toStream(outbuf); + Logging::initialize(conf); + + // Clear any initialize logging + boost::log::core::get()->flush(); + outbuf->str(std::string()); + + conf.filter.setLevel("error"); + Logging::initialize(conf); + + { + boost::log::core::get()->flush(); + std::string line; + *outbuf >> line; + ASSERT_TRUE(boost::regex_match(line, + boost::regex(".* Logging: Logging at level " + "error"))) + << "Line:" << line; + + ASSERT_THROW(*outbuf >> line, std::exception); + outbuf->str(std::string()); + } +} + +TEST_F(TestLogging, testFilterLevel) +{ + auto outbuf = std::make_shared(); + outbuf->exceptions(failmask); + + Logging::Config conf; + conf.filter.setLevel("warning"); + conf.useStdOut = false; + conf.useStdErr = false; + conf.useStream = toStream(outbuf); + Logging::initialize(conf); + + // Clear any initialize logging + boost::log::core::get()->flush(); + outbuf->str(std::string()); + + LOGGER_INFO(loggerA) << "hi there"; + { + boost::log::core::get()->flush(); + ASSERT_EQ(std::string(), outbuf->str()); + } + + LOGGER_WARN(loggerA) << "hi there"; + { + boost::log::core::get()->flush(); + std::string line; + *outbuf >> line; + ASSERT_TRUE(boost::regex_match(line, + boost::regex(".* TestLoggingA: hi there"))) + << "Line:" << line; + + ASSERT_THROW(*outbuf >> line, std::exception); + outbuf->str(std::string()); + } +} + +TEST_F(TestLogging, testFilterChannelInclude) +{ + auto outbuf = std::make_shared(); + outbuf->exceptions(failmask); + + Logging::Config conf; + conf.filter.channelInclude.push_back(boost::regex("TestLoggingA")); + conf.useStdOut = false; + conf.useStdErr = false; + conf.useStream = toStream(outbuf); + Logging::initialize(conf); + + // Clear any initialize logging + boost::log::core::get()->flush(); + outbuf->str(std::string()); + + LOGGER_WARN(loggerA) << "hi there"; + LOGGER_WARN(loggerB) << "oh hi"; + { + boost::log::core::get()->flush(); + std::string line; + *outbuf >> line; + ASSERT_TRUE(boost::regex_match(line, + boost::regex(".* TestLoggingA: hi there"))) + << "Line:" << line; + + ASSERT_THROW(*outbuf >> line, std::exception); + outbuf->str(std::string()); + } +} + +TEST_F(TestLogging, testFilterChannelExclude) +{ + auto outbuf = std::make_shared(); + outbuf->exceptions(failmask); + + Logging::Config conf; + conf.filter.channelExclude.push_back(boost::regex(".*LoggingA")); + conf.useStdOut = false; + conf.useStdErr = false; + conf.useStream = toStream(outbuf); + Logging::initialize(conf); + + // Clear any initialize logging + boost::log::core::get()->flush(); + outbuf->str(std::string()); + + LOGGER_WARN(loggerA) << "hi there"; + LOGGER_WARN(loggerB) << "oh hi"; + { + boost::log::core::get()->flush(); + std::string line; + *outbuf >> line; + ASSERT_TRUE( + boost::regex_match(line, boost::regex(".* TestLoggingB: oh hi"))) + << "Line:" << line; + + ASSERT_THROW(*outbuf >> line, std::exception); + outbuf->str(std::string()); + } +} diff --git a/src/afcsql/CMakeLists.txt b/src/afcsql/CMakeLists.txt new file mode 100644 index 0000000..6d14d08 --- /dev/null +++ b/src/afcsql/CMakeLists.txt @@ -0,0 +1,13 @@ +# All source files to same target +set(TGT_NAME "afcsql") + +file(GLOB ALL_CPP "*.cpp") +file(GLOB ALL_HEADER "*.h") +add_dist_library(TARGET ${TGT_NAME} SOURCES ${ALL_CPP} HEADERS ${ALL_HEADER} EXPORTNAME fbratTargets) +set(TARGET_LIBS ${TARGET_LIBS} ${TGT_NAME} PARENT_SCOPE) + +target_link_libraries(${TGT_NAME} PUBLIC Qt5::Core) +target_link_libraries(${TGT_NAME} PUBLIC Qt5::Sql) + +target_link_libraries(${TGT_NAME} PUBLIC afclogging) + diff --git a/src/afcsql/SqlConnectionDefinition.cpp b/src/afcsql/SqlConnectionDefinition.cpp new file mode 100644 index 0000000..2c5c8c8 --- /dev/null +++ b/src/afcsql/SqlConnectionDefinition.cpp @@ -0,0 +1,55 @@ +// + +#include +#include +#include "SqlConnectionDefinition.h" +#include "ratcommon/TextHelpers.h" +#include "afclogging/Logging.h" +#include "SqlError.h" + +namespace +{ +/// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "SqlConnectionDefinition") + +/// Number of hexadecimal digits to make the random name +const int dbNameDigits = 10; +} + +SqlConnectionDefinition::SqlConnectionDefinition() : hostName("127.0.0.1"), port(0) +{ +} + +void SqlConnectionDefinition::configureDb(QSqlDatabase &db) const +{ + db.setHostName(hostName); + if (port > 0) { + db.setPort(port); + } + db.setDatabaseName(dbName); + db.setUserName(userName); + db.setPassword(password); + db.setConnectOptions(options); +} + +QSqlDatabase SqlConnectionDefinition::newConnection() const +{ + if (!QSqlDatabase::isDriverAvailable(driverName)) { + throw SqlError(QString("SQL driver not available \"%1\"").arg(driverName)); + } + + // Search for an unused random connection name + QString connName; + do { + connName = TextHelpers::randomHexDigits(dbNameDigits); + } while (QSqlDatabase::contains(connName)); + + QSqlDatabase db = QSqlDatabase::addDatabase(driverName, connName); + configureDb(db); + if (!db.isValid()) { + throw SqlError("Bad SQL configuration", db.lastError()); + } + LOGGER_DEBUG(logger) << "newConnection " << connName << " to " << driverName << "://" + << userName << "@" << hostName << ":" << port << "/" << dbName; + return db; +} diff --git a/src/afcsql/SqlConnectionDefinition.h b/src/afcsql/SqlConnectionDefinition.h new file mode 100644 index 0000000..470cd97 --- /dev/null +++ b/src/afcsql/SqlConnectionDefinition.h @@ -0,0 +1,48 @@ +// + +#ifndef SQL_CONNECTION_DEFINITION_H +#define SQL_CONNECTION_DEFINITION_H + +#include + +class QSqlDatabase; + +/** Configuration for a single server. + * This class provides convenience behavior for packing and unpacking + * definitions into common "host:port" strings. + */ +struct SqlConnectionDefinition { + /** Initialization of invalid definition. + */ + SqlConnectionDefinition(); + + /** Load the definition into a DB connection object. + * + * @param db The database to configure with current settings. + */ + void configureDb(QSqlDatabase &db) const; + + /** Create a new DB connection with a random Qt DB name. + * + * @return The new thread-local connection. + */ + QSqlDatabase newConnection() const; + + /// Qt SQL driver name (e.g. QMYSQL, QODBC) + QString driverName; + /// Host name (defaults to local IP4 address + QString hostName; + /// TCP port number to connect to (if zero, it is invalid) + quint16 port; + /// Extra options for QSqlDatabase:: + QString options; + + /// Name of the DB schema (not the host name) + QString dbName; + /// User name for the connection + QString userName; + /// Password for the connection + QString password; +}; + +#endif /* SQL_CONNECTION_DEFINITION_H */ diff --git a/src/afcsql/SqlDelete.cpp b/src/afcsql/SqlDelete.cpp new file mode 100644 index 0000000..214da28 --- /dev/null +++ b/src/afcsql/SqlDelete.cpp @@ -0,0 +1,84 @@ +// + +#include +#include "SqlDelete.h" +#include "SqlHelpers.h" + +SqlDelete::SqlDelete(const QSqlDatabase &db, const QString &tableName) : + _db(db), _table(tableName), _tableExpr(tableName) +{ +} + +SqlDelete::SqlDelete(const QSqlDatabase &db, const QString &tableName, const SqlTable &data) : + _db(db), _table(tableName), _tableExpr(data.expression()) +{ +} + +SqlDelete &SqlDelete::whereNull(const QString &col) +{ + return where(QString("(%1 IS NULL)").arg(col)); +} + +SqlDelete &SqlDelete::whereEqualPlaceholder(const QString &col) +{ + return where(QString("(%1 = ?)").arg(col)); +} + +SqlDelete &SqlDelete::whereEqual(const QString &col, const QVariant &value) +{ + const QString valEnc = SqlHelpers::quoted(_db.driver(), value); + const QString op = (value.isNull() ? "IS" : "="); + return where(QString("(%1 %2 %3)").arg(col, op, valEnc)); +} + +SqlDelete &SqlDelete::whereInList(const QString &col, const QVariantList &values) +{ + const QSqlDriver *const drv = _db.driver(); + QStringList listEnc; + foreach(const QVariant &val, values) + { + listEnc.append(SqlHelpers::quoted(drv, val)); + } + + return where(QString("(%1 IN (%2))").arg(col, listEnc.join(","))); +} + +SqlDelete &SqlDelete::whereInExpr(const QString &col, const QString &expr) +{ + return where(QString("(%1 IN (%2))").arg(col, expr)); +} + +SqlDelete &SqlDelete::whereCompare(const QString &col, const QString &op, const QVariant &value) +{ + _whereExprs << QString("(%1 %2 %3)") + .arg(col) + .arg(op) + .arg(SqlHelpers::quoted(_db.driver(), value)); + return *this; +} + +QString SqlDelete::query() const +{ + QString queryStr = "DELETE"; + switch (_db.driver()->dbmsType()) { + case QSqlDriver::MySqlServer: + queryStr += " " + _table; + break; + default: + break; + } + + queryStr += " FROM " + _tableExpr; + + // Add list of WHERE clauses + if (!_whereExprs.isEmpty()) { + queryStr += " WHERE " + _whereExprs.join(" AND "); + } + + return queryStr; +} + +QSqlQuery SqlDelete::run() const +{ + return SqlHelpers::exec(_db, query()); +} diff --git a/src/afcsql/SqlDelete.h b/src/afcsql/SqlDelete.h new file mode 100644 index 0000000..f46c914 --- /dev/null +++ b/src/afcsql/SqlDelete.h @@ -0,0 +1,127 @@ +// + +#ifndef SQL_DELETE_H +#define SQL_DELETE_H + +#include +#include +#include "SqlTable.h" + +/** An interface specifically for the particular needs of the UPDATE query. + */ +class SqlDelete +{ + public: + /** Create an invalid object which can be assigned-to later. + */ + SqlDelete() + { + } + + /** Create a new DELETE query on a given database and table. + * @param db The database to query. + * @param tableName The definition of the table to delete from. + */ + SqlDelete(const QSqlDatabase &db, const QString &tableName); + + /** Create a complex DELETE based on a joined table. + * @param db The database to query. + * @param tableName The name of the table to delete from. + * @param data The data table to query with FROM clauses. + */ + SqlDelete(const QSqlDatabase &db, const QString &tableName, const SqlTable &data); + + /** Get the underlying database object. + * @param The database to query. + */ + const QSqlDatabase &database() const + { + return _db; + } + + /** Add an arbitrary WHERE clause to the DELETE. + * Each call to this function adds a new clause which will be joined with + * the "AND" operator. + * + * @param expr The WHERE expression. + * @return The updated query object. + */ + SqlDelete &where(const QString &expr) + { + _whereExprs << expr; + return *this; + } + + /** Add a WHERE clause to the DELETE for a single column. + * @param col The name of the column to filter, including only @c null values. + * @return The updated query object. + */ + SqlDelete &whereNull(const QString &col); + + /** Add a WHERE clause to the DELETE for a single column. + * This is intended to be used in prepared queries, where particular + * values are bound later. + * @param col The name of the column to filter. + * @return The updated query object. + * @sa PreparedQuery + */ + SqlDelete &whereEqualPlaceholder(const QString &col); + + /** Add a WHERE clause to the DELETE for a single column. + * @param col The name of the column to filter. + * @param value The value which must be in the column. + * @return The updated query object. + */ + SqlDelete &whereEqual(const QString &col, const QVariant &value); + + /** Add a WHERE clause to the DELETE for a single column. + * @param col The name of the column to filter. + * @param values The list of values to match the column. + * @return The updated query object. + */ + SqlDelete &whereInList(const QString &col, const QVariantList &values); + + /** Add a WHERE clause to the DELETE for a single column. + * @param col The name of the column to filter. + * @param expr The SQL expression to match equality to. + * @return The updated query object. + */ + SqlDelete &whereInExpr(const QString &col, const QString &expr); + + /** Add a WHERE clause to the DELETE for a single column. + * @param col The name of the column to filter. + * @param op The comparison operator for the column. + * The column is on the left of the operator and the value on right. + * @param value The value which must be in the column. + * @return The updated query object. + */ + SqlDelete &whereCompare(const QString &col, + const QString &op, + const QVariant &value); + + /** Get the SQL query string which would be executed by run(). + * @return The query string to be executed. + * The order of the query is defined by @xref{SQL-92} with DB-specific + * clauses at end. + */ + QString query() const; + + /** Build and execute the query and return the QSqlQuery result. + * @return The successful result of a query execution. + * @throw SqlError if the query fails to execute. + */ + QSqlQuery run() const; + + private: + /// Underlying database + QSqlDatabase _db; + /// The table to delete rows from + QString _table; + /// Fully formed table expression + QString _tableExpr; + /// List of WHERE clauses to be ANDed together, with unbound values + /// The clauses should be parenthesized to avoid error + QStringList _whereExprs; +}; + +#endif /* SQL_DELETE_H */ diff --git a/src/afcsql/SqlError.cpp b/src/afcsql/SqlError.cpp new file mode 100644 index 0000000..b2c7d38 --- /dev/null +++ b/src/afcsql/SqlError.cpp @@ -0,0 +1,19 @@ +// + +#include "SqlError.h" + +SqlError::SqlError(const QString &msg) : runtime_error(msg.toStdString()) +{ +} + +// Convert to Latin1 representation to ensure bad characters +SqlError::SqlError(const QString &msg, const QSqlError &err) : + runtime_error(QString("%1: (%2, \"%3\", \"%4\")") + .arg(msg) + .arg(err.number()) + .arg(err.driverText()) + .arg(err.databaseText()) + .toStdString()), + _err(err) +{ +} diff --git a/src/afcsql/SqlError.h b/src/afcsql/SqlError.h new file mode 100644 index 0000000..7b4aa8c --- /dev/null +++ b/src/afcsql/SqlError.h @@ -0,0 +1,53 @@ +// + +#ifndef SQL_ERROR_H +#define SQL_ERROR_H + +#include +#include + +/// Type used for errors in database access +class SqlError : public std::runtime_error +{ + public: + /** General error condition. + * + * @param msg The error message. + */ + SqlError(const QString &msg); + + /** Error associated with an underlying QSqlError. + * + * @param msg The context for the error. + * @param err The error itself. + * The parts of the @c err object are extracted into this object message. + */ + SqlError(const QString &msg, const QSqlError &err); + + /// Required no-throw specification + ~SqlError() throw() + { + } + + /** Get the original QSqlError (if one exists). + * @return The source error info, or an invalid QSqlError. + */ + const QSqlError &dbError() const + { + return _err; + } + + /** Get an associated QSqlError error number (if one exists). + * @return An error number, if applicable, or -1. + */ + int errNum() const + { + return _err.number(); + } + + private: + /// The SQL error struct + QSqlError _err; +}; + +#endif /* SQL_ERROR_H */ diff --git a/src/afcsql/SqlExceptionDb.cpp b/src/afcsql/SqlExceptionDb.cpp new file mode 100644 index 0000000..968890f --- /dev/null +++ b/src/afcsql/SqlExceptionDb.cpp @@ -0,0 +1,86 @@ +// + +#include +#include "SqlExceptionDb.h" +#include "SqlError.h" +#include "SqlHelpers.h" + +using namespace SqlHelpers; + +void SqlExceptionDb::setRequiredFeatures(const FeatureList &features) +{ + _feats = features; +} + +void SqlExceptionDb::ensureDriverValid() const +{ + if (!isValid()) { + throw SqlError("ensureDriverValid invalid connection", lastError()); + } + const QSqlDriver *const drv = driver(); + foreach(QSqlDriver::DriverFeature feature, _feats) + { + ensureFeature(*drv, feature); + } +} + +SqlExceptionDb &SqlExceptionDb::operator=(const QSqlDatabase &db) +{ + if (!db.isValid()) { + throw SqlError("Bad SQL driver", db.lastError()); + } + QSqlDatabase::operator=(db); + return *this; +} + +void SqlExceptionDb::ensureOpen() const +{ + if (!isOpen()) { + throw SqlError("Database connection not open"); + } +} + +void SqlExceptionDb::tryOpen() +{ + // Verify with active query first + if (isOpen()) { + QSqlQuery test = exec("SELECT 1"); + if (!test.isActive()) { + const QString name = QString("%1://%3@%2/%4") + .arg(driverName(), + hostName(), + userName(), + databaseName()); + qWarning().nospace() + << "SqlExceptionDb closing supposedly open connection to " << name; + close(); + } + } + if (!isOpen()) { + if (!open()) { + throw SqlError("Database connection failed", lastError()); + } + } + ensureDriverValid(); +} + +void SqlExceptionDb::transaction() +{ + if (!QSqlDatabase::transaction()) { + throw SqlError("Failed to start transaction", lastError()); + } +} + +void SqlExceptionDb::commit() +{ + if (!QSqlDatabase::commit()) { + throw SqlError("Failed to commit transaction", lastError()); + } +} + +void SqlExceptionDb::rollback() +{ + if (!QSqlDatabase::rollback()) { + throw SqlError("Failed to roll-back transaction", lastError()); + } +} diff --git a/src/afcsql/SqlExceptionDb.h b/src/afcsql/SqlExceptionDb.h new file mode 100644 index 0000000..4d00b7d --- /dev/null +++ b/src/afcsql/SqlExceptionDb.h @@ -0,0 +1,102 @@ +// + +#ifndef SQL_EXCEPTION_DB_H +#define SQL_EXCEPTION_DB_H + +#include +#include + +/** Extend the basic database class to provide overloads which raise exceptions + * upon common errors. + */ +class SqlExceptionDb : public QSqlDatabase +{ + public: + /// Convenience definition + typedef QList FeatureList; + + /// Default constructor with invalid state + SqlExceptionDb() + { + } + + /** Construct from an existing database connection. + * + * @param db The existing DB. + * @throw SqlError With any issues in the source database. + */ + SqlExceptionDb(const QSqlDatabase &db) + { + operator=(db); + } + + /** Copy constructor. + * Overload to also copy required features. + * + * @param db The existing DB. + */ + SqlExceptionDb(const SqlExceptionDb &db) : QSqlDatabase(db) + { + _feats = db._feats; + } + + /** Wrap an existing database connection. + * + * @param db The existing DB. + * @throw SqlError With any issues in the source database. + */ + SqlExceptionDb &operator=(const QSqlDatabase &db); + + /** Set the features which are required of this database. + * + * This should be done before ensureDriverValid(). + * @param features + */ + void setRequiredFeatures(const FeatureList &features); + + /** Ensure that this DB connection has all required features. + * + * Features are required by setRequiredFeatures(). + * @pre The DB connection is open. + * @throw SqlError If any required features are not present. + */ + void ensureDriverValid() const; + + /** Ensure that the connection is open. + * @throw SqlError If the connection is not open. + */ + void ensureOpen() const; + + /** Attempt to force the connection to be open. + * If the connection is already valid then this is no-op. + * + * @note This should be called if an existing connection has been kept + * open but idle for a long time (hours) to ensure that the server + * has not reset the connection during the idle time. + * + * @throw SqlError If there is a problem connecting. + * @post The connection will be open if no exception is raised. + */ + void tryOpen(); + + /** Overload to throw exception. + * @throw SqlError if the operation fails. + */ + void transaction(); + + /** Overload to throw exception. + * @throw SqlError if the operation fails. + */ + void commit(); + + /** Overload to throw exception. + * @throw SqlError if the operation fails. + */ + void rollback(); + + private: + /// Required features + FeatureList _feats; +}; + +#endif /* SQL_EXCEPTION_DB_H */ diff --git a/src/afcsql/SqlHelpers.cpp b/src/afcsql/SqlHelpers.cpp new file mode 100644 index 0000000..ec9cd6b --- /dev/null +++ b/src/afcsql/SqlHelpers.cpp @@ -0,0 +1,211 @@ +// + +#define SQL_DEBUG + +#include "SqlHelpers.h" +#include "SqlError.h" +#include "afclogging/Logging.h" +#include +#include +#include +#include +#include + +LOG_STREAM_WRAP_QT(QSqlError) + +#define FEATURE(Name) \ + case QSqlDriver::Name: \ + return #Name; + +namespace +{ +/** Name a particular feature enumeration. + * + * @param id The enum value. + * @return The name of the value. + */ +QString featureName(QSqlDriver::DriverFeature id) +{ + switch (id) { + FEATURE(Transactions) + FEATURE(QuerySize) + FEATURE(BLOB) + FEATURE(Unicode) + FEATURE(PreparedQueries) + FEATURE(NamedPlaceholders) + FEATURE(PositionalPlaceholders) + FEATURE(LastInsertId) + FEATURE(BatchOperations) + FEATURE(SimpleLocking) + FEATURE(LowPrecisionNumbers) + FEATURE(EventNotifications) + FEATURE(FinishQuery) + FEATURE(MultipleResultSets) + FEATURE(CancelQuery) + default: + return "other"; + } +}; + +/** Check for the special error case of QTBUG-223. + * Documented at https://bugreports.qt-project.org/browse/QTBUG-223. + * @post Close the connection if MySQL client reports "server gone away" + */ +void checkMysql(const QSqlDatabase &db, const QSqlError &err) +{ + if ((err.number() == 2006) && (db.driverName() == "QMYSQL")) { + // Not really a copy, since both use same connection + QSqlDatabase dbRef(db); + dbRef.close(); + } +} + +/// Expand a QDateTime as full-resolution form (fixed time zone) +const QString fullDtSpec("yyyy-MM-ddTHH:mm:ss.zzzZ"); + +/// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "SqlHelpers") +} + +EnvironmentFlag SqlHelpers::doDebug("CPO_SQL_DEBUG"); + +void SqlHelpers::ensureFeature(const QSqlDriver &drv, QSqlDriver::DriverFeature feature) +{ + if (!drv.hasFeature(feature)) { + throw SqlError(QString("SQL driver is missing %1").arg(featureName(feature))); + } +} + +QVariant SqlHelpers::encode(const QVariant &value) +{ + switch (value.type()) { + case QVariant::DateTime: { + const QDateTime valDt = value.toDateTime(); + if (valDt.isNull()) { + return QVariant(); + } + if (valDt.timeSpec() != Qt::UTC) { + throw SqlError("All DateTime values must be in UTC"); + } + return valDt.toString(fullDtSpec); + } + + default: + return value; + } +} + +QString SqlHelpers::quoted(const QSqlDriver *driver, const QVariant &value) +{ + const QVariant val = SqlHelpers::encode(value); + QSqlField field(QString(), val.type()); + field.setValue(val); + return driver->formatValue(field); +} + +QStringList SqlHelpers::prefixCols(const QString &prefix, const QStringList &cols) +{ + QStringList result; + result.reserve(cols.count()); + foreach(const QString &col, cols) + { + result.append(QString("%1.%2").arg(prefix, col)); + } + return result; +} + +QSqlQuery SqlHelpers::prepare(const QSqlDatabase &db, const QString &query) +{ + if (doDebug()) { + LOGGER_DEBUG(logger) << "prepare " << query; + } + + QSqlQuery qObj(db); + if (!qObj.prepare(query)) { + const QSqlError sqlErr = qObj.lastError(); + checkMysql(db, sqlErr); + throw SqlError(QString("Failed prepare for \"%1\"").arg(query), sqlErr); + } + return qObj; +} + +void SqlHelpers::execPrepared(const QSqlDatabase &db, QSqlQuery &qObj) +{ + std::unique_ptr timer; + if (doDebug()) { + LOGGER_DEBUG(logger) << "execPrepared RUN " << qObj.lastQuery() << " WITH (" + << boundList(qObj).join(", ") << ")"; + timer.reset(new QElapsedTimer); + timer->start(); + } + + const bool success = qObj.exec(); + if (doDebug()) { + const qint64 elapsed = timer->elapsed(); + LOGGER_DEBUG(logger) << "execPrepared TIME " << elapsed << " SIZE " << qObj.size() + << " ERR " << qObj.lastError(); + } + if (!success) { + const QSqlError sqlErr = qObj.lastError(); + checkMysql(db, sqlErr); + throw SqlError(QString("Failed exec for \"%1\" with values (%2)") + .arg(qObj.lastQuery()) + .arg(boundList(qObj).join(",")), + sqlErr); + } +} + +QStringList SqlHelpers::boundList(const QSqlQuery &query) +{ + // work-around for QTBUG-12186 + const int boundCount = query.boundValues().count(); + QStringList boundStrs; + for (int valI = 0; valI < boundCount; ++valI) { + const QVariant &var = query.boundValue(valI); + QString val; + switch (var.type()) { + case QVariant::Invalid: + val = "NULL"; + break; + case QVariant::ByteArray: + val = "h'" + var.toByteArray().toHex() + "'"; + break; + case QVariant::DateTime: + val = var.toDateTime().toString(fullDtSpec); + break; + case QVariant::String: + val = "'" + var.toString() + "'"; + break; + default: + val = var.toString(); + break; + } + boundStrs << val; + } + return boundStrs; +} + +QSqlQuery SqlHelpers::exec(const QSqlDatabase &db, const QString &query) +{ + std::unique_ptr timer; + if (doDebug()) { + LOGGER_DEBUG(logger) << "exec RUN " << query; + timer.reset(new QElapsedTimer); + timer->start(); + } + QSqlQuery qObj(db); + + // INSERT queries on ODBC may have qObj.isActive() false but have no error + const bool success = qObj.exec(query); + if (doDebug()) { + const qint64 elapsed = timer->elapsed(); + LOGGER_DEBUG(logger) << "exec TIME " << elapsed << " SIZE " << qObj.size() + << " ERR " << qObj.lastError(); + } + if (!success) { + const QSqlError sqlErr = qObj.lastError(); + checkMysql(db, sqlErr); + throw SqlError(QString("Failed exec for \"%1\"").arg(query), sqlErr); + } + return qObj; +} diff --git a/src/afcsql/SqlHelpers.h b/src/afcsql/SqlHelpers.h new file mode 100644 index 0000000..d54aaf8 --- /dev/null +++ b/src/afcsql/SqlHelpers.h @@ -0,0 +1,96 @@ +// + +#ifndef SQL_HELPERS_H +#define SQL_HELPERS_H + +//#include "cposql_export.h" +#include "ratcommon/EnvironmentFlag.h" +#include +#include + +class QVariant; + +namespace SqlHelpers +{ +/** Extract environment variable "CPO_SQL_DEBUG" one time and cache. + * This function is thread safe. + * @return True if debugging is enabled. + */ +// CPOSQL_EXPORT +extern EnvironmentFlag doDebug; + +/** Guarantee that an SQL driver has a required feature. + * + * @param drv The driver to check. + * @param feature The feature required. + * @throw SqlError If the feature is not present. + */ +void ensureFeature(const QSqlDriver &drv, QSqlDriver::DriverFeature feature); + +/** Override Qt encoding rules for SQL types. + * For MySQL and PostgreSQL, Qt does not properly format datetime values. + * + * @param value The value to encode. + * @return The same value, or a string-encoded represenation. + */ +QVariant encode(const QVariant &value); + +/** Get a quoted representation of a given value. + * This uses the underlying database driver QSqlDriver::formatValue(). + * + * @param db The database used to get driver info from. + * @param value The type of the value is used to determine the quoting and + * the string representation is used to give the output. + * @return The quoted value representation. + */ +QString quoted(const QSqlDriver *driver, const QVariant &value); + +/** Apply a table namespace prefix to a list of column names. + * + * @param prefix The namespace to prepend. + * @param cols The individual column names. + * @return The prefixed column names. + */ +QStringList prefixCols(const QString &prefix, const QStringList &cols); + +/** Attempt to prepare a specific SQL query. + * + * @param db The database to execute in. + * @param query The full query string. + * @return The prepared query object. + * @throw SqlError If the query failed. + * @post If the DB driver is MySQL and the error is a failed connection, + * then the DB connection is closed. + */ +QSqlQuery prepare(const QSqlDatabase &db, const QString &query); + +/** Attempt to execute a prepared SQL query. + * + * @param db The database to execute in. + * @param qObj The prepared query object. + * @throw SqlError If the query failed. + * @post If the DB driver is MySQL and the error is a failed connection, + * then the DB connection is closed. + */ +void execPrepared(const QSqlDatabase &db, QSqlQuery &qObj); + +/** Get the list of bound positional placeholdervalues. + * + * @param query The query which is bound to. + * @return The list of bound values. + */ +QStringList boundList(const QSqlQuery &query); + +/** Attempt to execute a specific SQL query. + * + * @param db The database to execute in. + * @param query The full query string. + * @return The active, valid query result. + * @throw SqlError If the query failed. + * @post If the DB driver is MySQL and the error is a failed connection, + * then the DB connection is closed. + */ +QSqlQuery exec(const QSqlDatabase &db, const QString &query); +} + +#endif /* SQL_HELPERS_H */ diff --git a/src/afcsql/SqlInsert.cpp b/src/afcsql/SqlInsert.cpp new file mode 100644 index 0000000..973f515 --- /dev/null +++ b/src/afcsql/SqlInsert.cpp @@ -0,0 +1,54 @@ +// + +#include +#include +#include +#include "SqlInsert.h" +#include "SqlError.h" +#include "SqlHelpers.h" +#include "SqlPreparedQuery.h" + +namespace +{ +const QChar comma(','); +} + +SqlInsert &SqlInsert::cols(const QStringList &colsVal) +{ + if (colsVal.toSet().count() != colsVal.count()) { + throw SqlError("Duplicate column name"); + } + + _cols = colsVal; + return *this; +} + +QString SqlInsert::query(const QString &expr) const +{ + const QString colPart = _cols.join(comma); + + return QString("INSERT INTO %1 (%2) %3").arg(_table).arg(colPart).arg(expr); +} + +QString SqlInsert::prepared() const +{ + const QString valPart = SqlPreparedQuery::qMark(_cols.count()); + return query(QString("VALUES (%1)").arg(valPart)); +} + +QSqlQuery SqlInsert::run(const QVariantList &values) +{ + const QSqlDriver *const drv = _db.driver(); + QStringList valStrs; + foreach(const QVariant &val, values) + { + valStrs.append(SqlHelpers::quoted(drv, val)); + } + + return run(QString("VALUES (%1)").arg(valStrs.join(comma))); +} + +QSqlQuery SqlInsert::run(const QString &expr) +{ + return SqlHelpers::exec(_db, query(expr)); +} diff --git a/src/afcsql/SqlInsert.h b/src/afcsql/SqlInsert.h new file mode 100644 index 0000000..08444e9 --- /dev/null +++ b/src/afcsql/SqlInsert.h @@ -0,0 +1,87 @@ +// + +#ifndef SQL_INSERT_H +#define SQL_INSERT_H + +#include +#include +#include + +/** An interface specifically for the particular needs of the INSERT query. + */ +class SqlInsert +{ + public: + /** Create a new SELECT query on a given database and table. + * @param db The database to query. + * @param table The name of the table to query. + */ + SqlInsert(const QSqlDatabase &db, const QString &table) : _db(db), _table(table) + { + } + + /** Get the underlying database object. + * @param The database to query. + */ + const QSqlDatabase &database() const + { + return _db; + } + + /** Get the number of columns currently defined for the query. + * + * @return The number of columns. + */ + int colCount() const + { + return _cols.count(); + } + + /** Set the columns used for the result of the SELECT. + * @param cols A list of column names. + * @return The updated query object. + */ + SqlInsert &cols(const QStringList &cols); + + /** Get the SQL query string which would be executed by run() or filled + * by prepared(). + * + * @param expr The value expression to insert. This expression must have + * the same number of values as the insert columns. + * @return The query string to be executed. + */ + QString query(const QString &expr) const; + + /** Get the SQL query string with positional placeholders. + * + * @param The query string to be executed. + */ + QString prepared() const; + + /** Build and execute the query and return the QSqlQuery result. + * + * @param values The values to insert. + * @return The successful result of a query execution. + * @throw SqlError if the query fails to execute. + */ + QSqlQuery run(const QVariantList &values); + + /** Build and execute the query and return the QSqlQuery result. + * + * @param expr The value expression to insert. This expression must have + * the same number of values as the insert columns. + * @return The successful result of a query execution. + * @throw SqlError if the query fails to execute. + */ + QSqlQuery run(const QString &expr); + + protected: + /// Underlying database + QSqlDatabase _db; + /// Unquoted name of the table + QString _table; + /// List of quoted column names to retrieve + QStringList _cols; +}; + +#endif /* SQL_INSERT_H */ diff --git a/src/afcsql/SqlPreparedQuery.cpp b/src/afcsql/SqlPreparedQuery.cpp new file mode 100644 index 0000000..912b11b --- /dev/null +++ b/src/afcsql/SqlPreparedQuery.cpp @@ -0,0 +1,70 @@ +// + +#include +#include +#include +#include +#include "SqlSelect.h" +#include "SqlInsert.h" +#include "SqlUpdate.h" +#include "SqlDelete.h" +#include "SqlHelpers.h" +#include "SqlPreparedQuery.h" + +QString SqlPreparedQuery::qMark(int number) +{ + QStringList parts; + for (int ix = 0; ix < number; ++ix) { + parts += "?"; + } + return parts.join(","); +} + +SqlPreparedQuery::SqlPreparedQuery(const SqlSelect &query) +{ + _db = query.database(); + QSqlQuery::operator=(SqlHelpers::prepare(_db, query.query())); +} + +SqlPreparedQuery::SqlPreparedQuery(const SqlInsert &query) +{ + _db = query.database(); + QSqlQuery::operator=(SqlHelpers::prepare(_db, query.prepared())); +} + +SqlPreparedQuery::SqlPreparedQuery(const SqlUpdate &query) +{ + _db = query.database(); + QSqlQuery::operator=(SqlHelpers::prepare(_db, query.query())); +} + +SqlPreparedQuery::SqlPreparedQuery(const SqlDelete &query) +{ + _db = query.database(); + QSqlQuery::operator=(SqlHelpers::prepare(_db, query.query())); +} + +SqlPreparedQuery::SqlPreparedQuery(const QSqlDatabase &db, const QString &query) : _db(db) +{ + QSqlQuery::operator=(SqlHelpers::prepare(_db, query)); +} + +SqlPreparedQuery &SqlPreparedQuery::bind(const QVariant ¶m) +{ + addBindValue(param); + return *this; +} + +SqlPreparedQuery &SqlPreparedQuery::bindList(const QVariantList ¶ms) +{ + for (int ix = 0; ix < params.count(); ++ix) { + bindValue(ix, params.at(ix)); + } + return *this; +} + +QSqlQuery &SqlPreparedQuery::run() +{ + SqlHelpers::execPrepared(_db, *this); + return *this; +} diff --git a/src/afcsql/SqlPreparedQuery.h b/src/afcsql/SqlPreparedQuery.h new file mode 100644 index 0000000..f05375e --- /dev/null +++ b/src/afcsql/SqlPreparedQuery.h @@ -0,0 +1,91 @@ +// + +#ifndef PREPARED_QUERY_H +#define PREPARED_QUERY_H + +#include +#include +#include + +class SqlSelect; +class SqlInsert; +class SqlUpdate; +class SqlDelete; + +/** Wrapper class around QSqlQuery to provide a binding interface similar + * to QString::arg() and to throw SqlError upon failure. + * + * This is intended to be used as: + * @code + * result = PreparedQuery("SELECT * FROM table WHERE col = ?").bind("value").run(); + * @endcode + */ +class SqlPreparedQuery : public QSqlQuery +{ + public: + /** Generate a binding list of question marks. + * @param number The number of parameters-to-be-bound. + * @return A string containing the desired number of question marks. + */ + static QString qMark(int number); + + /// Default constructor does nothing + SqlPreparedQuery() + { + } + + /** Construct from SELECT expression. + * + * @param query The query object to take the prepared expression from. + */ + SqlPreparedQuery(const SqlSelect &query); + + /** Construct from INSERT expression. + * + * @param query The query object to take the prepared expression from. + */ + SqlPreparedQuery(const SqlInsert &query); + + /** Construct from UPDATE expression. + * + * @param query The query object to take the expression from. + */ + SqlPreparedQuery(const SqlUpdate &query); + + /** Construct from DELETE expression. + * + * @param query The query object to take the expression from. + */ + SqlPreparedQuery(const SqlDelete &query); + + /** Prepare (but do not execute) a given query. + * @param db The database in which the query is to be run. + * @param query The query string, with possible unbound values. + * @throw SqlError if the query fails to prepare. + */ + SqlPreparedQuery(const QSqlDatabase &db, const QString &query); + + /** Bind a single parameter to a prepared query. + * @param param The parameter value to bind. + * @return The modified query. + */ + SqlPreparedQuery &bind(const QVariant ¶m); + + /** Bind multiple parameters to a prepared query. + * @param params The parameters value to bind. + * @return The modified query. + */ + SqlPreparedQuery &bindList(const QVariantList ¶ms); + + /** Execute the query and return the QSqlQuery result. + * @return The successful result of a query execution. + * @throw SqlError if the query fails to execute. + */ + QSqlQuery &run(); + + private: + /// Database connection. This is not accessible by QSqlQuery. + QSqlDatabase _db; +}; + +#endif /* PREPARED_QUERY_H */ diff --git a/src/afcsql/SqlScopedConnection.cpp b/src/afcsql/SqlScopedConnection.cpp new file mode 100644 index 0000000..e029a74 --- /dev/null +++ b/src/afcsql/SqlScopedConnection.cpp @@ -0,0 +1,13 @@ +// + +#include "SqlScopedConnection.h" + +void SqlScopedConnectionCloser::cleanup(QSqlDatabase *pointer) +{ + if (!pointer) { + return; + } + const QString cName = pointer->connectionName(); + delete pointer; + QSqlDatabase::removeDatabase(cName); +} diff --git a/src/afcsql/SqlScopedConnection.h b/src/afcsql/SqlScopedConnection.h new file mode 100644 index 0000000..27c7866 --- /dev/null +++ b/src/afcsql/SqlScopedConnection.h @@ -0,0 +1,50 @@ +// + +#ifndef CPOBG_SRC_CPOSQL_SQLSCOPEDCONNECTION_H_ +#define CPOBG_SRC_CPOSQL_SQLSCOPEDCONNECTION_H_ + +//#include "cposql_export.h" +#include +#include + +/** Deleter for a database object. + * Uses QSqlDatabase::removeDatabase() for DB connection cleanup. + */ +class /*CPOSQL_EXPORT*/ SqlScopedConnectionCloser +{ + public: + /// Interface for QScopedPointerDeleter + static void cleanup(QSqlDatabase *pointer); +}; + +/** A scoped pointer which closes a DB connection when it deletes the + * DB object. + * @tparam DbName The specific class to scope. + * It must be either QSqlDatabase or a derived class. + */ +template +class SqlScopedConnection : public QScopedPointer +{ + typedef QScopedPointer Parent; + + public: + /** Construct a new default instance. + * @post The DB instance is ready for connection assignment. + */ + SqlScopedConnection() + { + Parent::reset(new DbType()); + } + + /** Take ownership of an existing instance. + * + * @param db The database instance start with. + * Ownership is taken from the caller. + */ + SqlScopedConnection(DbType *db) + { + Parent::reset(db); + } +}; + +#endif /* CPOBG_SRC_CPOSQL_SQLSCOPEDCONNECTION_H_ */ diff --git a/src/afcsql/SqlSelect.cpp b/src/afcsql/SqlSelect.cpp new file mode 100644 index 0000000..c6fd15c --- /dev/null +++ b/src/afcsql/SqlSelect.cpp @@ -0,0 +1,171 @@ +// + +#include +#include +#include "SqlSelect.h" +#include "SqlTable.h" +#include "SqlError.h" +#include "SqlHelpers.h" +#include + +namespace +{ +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "SqlSelect") + +} // end namespace + +SqlSelect::SqlSelect() : _rowLimit(-1) +{ +} + +SqlSelect::SqlSelect(const QSqlDatabase &db, const QString &table) : + _db(db), _table(table), _rowLimit(-1) +{ +} + +SqlSelect::SqlSelect(const QSqlDatabase &db, const SqlTable &table) : + _db(db), _table(table.expression()), _rowLimit(-1) +{ +} + +SqlSelect &SqlSelect::col(const QString &name) +{ + return cols(QStringList(name)); +} + +SqlSelect &SqlSelect::group(const QStringList &colsVal) +{ + return group(colsVal.join(",")); +} + +SqlSelect &SqlSelect::whereNull(const QString &colVal) +{ + return where(QString("(%1 IS NULL)").arg(colVal)); +} + +SqlSelect &SqlSelect::whereEqualPlaceholder(const QString &colVal) +{ + return where(QString("(%1 = ?)").arg(colVal)); +} + +SqlSelect &SqlSelect::whereNonZero(const QString &colVal) +{ + return where(QString("(%1 <> 0)").arg(colVal)); +} + +SqlSelect &SqlSelect::whereEqual(const QString &colVal, const QVariant &value) +{ + const QString valEnc = SqlHelpers::quoted(_db.driver(), value); + const QString op = (value.isNull() ? "IS" : "="); + return where(QString("(%1 %2 %3)").arg(colVal, op, valEnc)); +} + +SqlSelect &SqlSelect::whereCompare(const QString &colVal, const QString &op, const QVariant &value) +{ + const QString valEnc = SqlHelpers::quoted(_db.driver(), value); + return where(QString("(%1 %2 %3)").arg(colVal, op, valEnc)); +} + +SqlSelect &SqlSelect::whereComparePlaceholder(const QString &colVal, const QString &op) +{ + return where(QString("(%1 %2 ?)").arg(colVal, op)); +} + +SqlSelect &SqlSelect::whereInExpr(const QString &colVal, const QString &expr) +{ + _whereExprs << QString("(%1 IN (%2))").arg(colVal).arg(expr); + return *this; +} + +SqlSelect &SqlSelect::whereInList(const QString &colVal, const QVariantList &values) +{ + const QSqlDriver *drv = _db.driver(); + QStringList parts; + foreach(const QVariant &val, values) + { + parts << SqlHelpers::quoted(drv, val); + } + return whereInExpr(colVal, parts.join(",")); +} + +SqlSelect &SqlSelect::whereBetween(const QString &colVal, + const QVariant &minInclusive, + const QVariant &maxInclusive) +{ + const QSqlDriver *drv = _db.driver(); + _whereExprs << QString("((%1 >= %2) AND (%1 <= %3))") + .arg(colVal) + .arg(SqlHelpers::quoted(drv, minInclusive)) + .arg(SqlHelpers::quoted(drv, maxInclusive)); + return *this; +} + +SqlSelect &SqlSelect::join(const QString &other, const QString &on, const QString &type) +{ + Join joinVal; + joinVal.whatClause = other; + joinVal.onClause = on; + joinVal.typeClause = type; + _joins.append(joinVal); + return *this; +} + +SqlSelect &SqlSelect::topmost(int count) +{ + _rowLimit = count; + return *this; +} + +QString SqlSelect::query() const +{ + QString queryStr = "SELECT "; + + if (!_prefix.isEmpty()) { + queryStr += _prefix + " "; + } + + queryStr += _selCols.join(QChar(',')) + " FROM " + _table; + + // Optional index force + if (!_index.isEmpty()) { + queryStr += " USE INDEX (" + _index + ")"; + } + + // Optional JOINs list + foreach(const Join &joinVal, _joins) + { + queryStr += QString(" %1 JOIN %2 ON (%3)") + .arg(joinVal.typeClause, joinVal.whatClause, joinVal.onClause); + } + + // Add list of WHERE clauses + if (!_whereExprs.isEmpty()) { + queryStr += " WHERE " + _whereExprs.join(" AND "); + } + // Add optional grouping + if (!_groupCols.isEmpty()) { + queryStr += " GROUP BY " + _groupCols; + } + // Add final filter + if (!_havingExpr.isEmpty()) { + queryStr += " HAVING " + _havingExpr; + } + // Add optional sorting + if (!_orderCols.isEmpty()) { + queryStr += " ORDER BY " + _orderCols; + } + // Add row restriction + if (_rowLimit >= 0) { + queryStr += QString(" LIMIT %1").arg(_rowLimit); + } + + return queryStr; +} + +QSqlQuery SqlSelect::run() const +{ + const QString queryStr = query(); + LOGGER_DEBUG(logger) << "Executing select query: " << queryStr; + return SqlHelpers::exec(_db, queryStr); +} diff --git a/src/afcsql/SqlSelect.h b/src/afcsql/SqlSelect.h new file mode 100644 index 0000000..5ba5f23 --- /dev/null +++ b/src/afcsql/SqlSelect.h @@ -0,0 +1,312 @@ +// + +#ifndef SQL_SELECT_H +#define SQL_SELECT_H + +#include +#include +#include + +class SqlTable; + +/** An interface specifically for the particular needs of the SELECT query. + */ +class SqlSelect +{ + public: + /** Create a invalid object which can be assigned-to later. + */ + SqlSelect(); + + /** Create a new SELECT query on a given database and table. + * @param db The database to query. + * @param table The name of the table to query. + */ + SqlSelect(const QSqlDatabase &db, const QString &table); + + /** Create a new SELECT query on a given database and table. + * @param db The database to query. + * @param table The definition of the table to query. + */ + SqlSelect(const QSqlDatabase &db, const SqlTable &table); + + /** Get the underlying database object. + * @param The database to query. + */ + const QSqlDatabase &database() const + { + return _db; + } + + /** Get the number of columns currently defined for the query. + * + * @return The number of columns. + */ + int colCount() const + { + return _selCols.count(); + } + + /** Set the columns used for the result of the SELECT. + * @param name A single column to fetch. + * @return The updated query object. + */ + SqlSelect &col(const QString &name); + + /** Set the columns used for the result of the SELECT. + * @param cols A list of column names. + * @return The updated query object. + */ + SqlSelect &cols(const QStringList &colsVal) + { + _selCols = colsVal; + return *this; + } + + /** Set query prefix options (such as DISTINCT). + * + * @param options The options expression to prefix. + * @return The updated query object. + */ + SqlSelect &prefix(const QString &options) + { + _prefix = options; + return *this; + } + + /** Set the columns used to group the result of the SELECT. + * @param cols A comma separated list of column names. + * @return The updated query object. + */ + SqlSelect &group(const QString &colsVal) + { + _groupCols = colsVal; + return *this; + } + /** Overload to combine multiple column names. + * + * @param cols Individual column names to group by in-order. + * @return The updated query object. + * @sa group(QString) + */ + SqlSelect &group(const QStringList &cols); + + /** Set the columns used to sort the result of the SELECT. + * @param cols A comma separated list of column names. + * @return The updated query object. + */ + SqlSelect &having(const QString &text) + { + _havingExpr = text; + return *this; + } + + /** Set the columns used to sort the result of the SELECT. + * @param cols A comma separated list of column names. + * @return The updated query object. + */ + SqlSelect &order(const QString &colsVal) + { + _orderCols = colsVal; + return *this; + } + + /** Add an arbitrary WHERE clause to the SELECT. + * Each call to this function adds a new clause which will be joined with + * the "AND" operator. + * + * @param expr The WHERE expression. + * @return The updated query object. + */ + SqlSelect &where(const QString &expr) + { + _whereExprs << expr; + return *this; + } + + /** Add a WHERE clause to the SELECT for a single column. + * @param col The name of the column to filter to include @c null values. + * @return The updated query object. + */ + SqlSelect &whereNull(const QString &col); + + /** Add a WHERE clause to the SELECT for a single column. + * This is intended to be used in prepared queries, where particular + * values are bound later. + * @param col The name of the column to filter. + * @return The updated query object. + * @sa PreparedQuery + */ + SqlSelect &whereEqualPlaceholder(const QString &col); + + /** Add a WHERE clause to the SELECT for a single column. + * @param col The name of the column to filter for non-zero values. + * @return The updated query object. + */ + SqlSelect &whereNonZero(const QString &col); + + /** Add a WHERE clause to the SELECT for a single column. + * @param col The name of the column to filter. + * @param value The value which must be in the column. + * @return The updated query object. + */ + SqlSelect &whereEqual(const QString &col, const QVariant &value); + + /** Add a WHERE clause to the SELECT for a single column. + * @param col The name of the column to filter. + * @param op The comparison operator for the column. + * The column is on the left of the operator and the value on right. + * @param value The value which must be in the column. + * @return The updated query object. + */ + SqlSelect &whereCompare(const QString &col, + const QString &op, + const QVariant &value); + + /** Add a WHERE clause to the SELECT for a single column. + * @param col The name of the column to filter. + * @param op The comparison operator for the column. + * The column is on the left of the operator and the placeholder on right. + * @return The updated query object. + */ + SqlSelect &whereComparePlaceholder(const QString &col, const QString &op); + + /** Add a WHERE clause to the SELECT for a single column. + * @param col The name of the column to filter. + * @param expr An SQL expression for ppossible values which must be in the column. + * @return The updated query object. + */ + SqlSelect &whereInExpr(const QString &col, const QString &expr); + + /** Add a WHERE clause to the SELECT for a single column. + * @param col The name of the column to filter. + * @param values A list of possible values which must be in the column. + * @return The updated query object. + */ + SqlSelect &whereInList(const QString &col, const QVariantList &values); + + /** Add a WHERE clause to the SELECT for a single column. + * @param col The name of the column to filter. + * @param values A list of possible values which must be in the column. + * @return The updated query object. + */ + template + SqlSelect &whereIn(const QString &colVal, const Container &values) + { + QVariantList parts; + foreach(const typename Container::value_type &val, values) + { + parts << val; + } + return whereInList(colVal, parts); + } + + /** Add a WHERE clause to the SELECT for a single column. + * @param col The name of the column to filter. + * @param minInclusive The minimum possible value allowed by the filter. + * @param maxInclusive The maximum possible value allowed by the filter. + * @return The updated query object. + */ + SqlSelect &whereBetween(const QString &col, + const QVariant &minInclusive, + const QVariant &maxInclusive); + + /** Join the current table with another. + * Multiple joins may be added in-sequence. + * + * @param other The other expression to join with. + * @param on The expression to join on. + * @param type The type of join to perform (INNER, LEFT, RIGHT, etc.) + * @return The updated query object. + */ + SqlSelect &join(const QString &other, const QString &on, const QString &type); + + /** Shortcut to perform a LEFT JOIN. + * + * @param other The other expression to join with. + * @param on The expression to join on. + * @return The updated query object. + */ + SqlSelect &leftJoin(const QString &other, const QString &on) + { + return join(other, on, "LEFT"); + } + + /** Shortcut to perform a INNER JOIN. + * + * @param other The other expression to join with. + * @param on The expression to join on. + * @return The updated query object. + */ + SqlSelect &innerJoin(const QString &other, const QString &on) + { + return join(other, on, "INNER"); + } + + /** Force a specific index to be used for selection. + * @param indexName The name of the underlying index. + * @return The updated query object. + */ + SqlSelect &index(const QString &indexName) + { + _index = indexName; + return *this; + } + + /** Add a row-limiting clause to the query. + * + * @param count The maximum number of rows to retrieve. + * @return The updated query object. + */ + SqlSelect &topmost(int count = 1); + + /** Get the SQL query string which would be executed by run(). + * @return The query string to be executed. + * The order of the query is defined by @xref{SQL-92} with DB-specific + * clauses at end. + */ + QString query() const; + + /** Build and execute the query and return the QSqlQuery result. + * @return The successful result of a query execution. + * @throw SqlError if the query fails to execute. + */ + QSqlQuery run() const; + + // protected: + /// Each JOIN in this select + struct Join { + /// Named type of join + QString typeClause; + /// Join right-hand side clause + QString whatClause; + /// Join-on clause + QString onClause; + }; + + /// Underlying database + QSqlDatabase _db; + /// Fully quoted name of the table + QString _table; + /// Joins defined + QList _joins; + /// Prefix options + QString _prefix; + /// List of quoted column names to retrieve + QStringList _selCols; + /// Comma-separated list of quoted column names for a GROUP BY clause + QString _groupCols; + /// Single expression used for HAVING clause + QString _havingExpr; + /// Comma-separated list of quoted column names for an ORDER BY by + QString _orderCols; + /// List of WHERE clauses to be ANDed together, with unbound values + /// The clauses should be parenthesized to avoid error + QStringList _whereExprs; + /// Optional comma-separated list of indices to use + QString _index; + /// Optional row limit + int _rowLimit; +}; + +#endif /* SQL_SELECT_H */ diff --git a/src/afcsql/SqlTable.cpp b/src/afcsql/SqlTable.cpp new file mode 100644 index 0000000..cf6af1e --- /dev/null +++ b/src/afcsql/SqlTable.cpp @@ -0,0 +1,39 @@ +// + +#include "SqlTable.h" + +SqlTable::SqlTable(const QString &tableExpr) +{ + Join joinVal; + joinVal.whatClause = tableExpr; + _joins.append(joinVal); +} + +SqlTable &SqlTable::join(const QString &tableExpr, const QString &on, const QString &type) +{ + Join joinVal; + joinVal.whatClause = tableExpr; + joinVal.onClause = on; + joinVal.typeClause = type; + _joins.append(joinVal); + return *this; +} + +QString SqlTable::expression() const +{ + QList::const_iterator it = _joins.begin(); + if (it == _joins.end()) { + return QString(); + } + + QString expr; + expr += it->whatClause; + ++it; + + for (; it != _joins.end(); ++it) { + expr += QString(" %1 JOIN %2 ON (%3)") + .arg(it->typeClause, it->whatClause, it->onClause); + } + + return expr; +} diff --git a/src/afcsql/SqlTable.h b/src/afcsql/SqlTable.h new file mode 100644 index 0000000..a012d7b --- /dev/null +++ b/src/afcsql/SqlTable.h @@ -0,0 +1,90 @@ +// + +#ifndef SQL_TABLE_H +#define SQL_TABLE_H + +#include +#include + +/** Define an SQL expression for a simple table or a combined JOIN of tables. + */ +class SqlTable +{ + public: + /// Initialize to an invalid definition + SqlTable() + { + } + + /** Initialize to a single existing table/view name. + * + * @param tableExpr The fully-quoted table expression. + */ + SqlTable(const QString &tableExpr); + + /** Join the current table with another. + * Multiple joins may be added in-sequence. + * + * @param tableExpr The other fully-quoted expression to join with. + * @param on The expression to join on. + * @param type The type of join to perform (INNER, LEFT, RIGHT, etc.) + * @return The updated query object. + */ + SqlTable &join(const QString &tableExpr, const QString &on, const QString &type); + + /** Shortcut to perform a LEFT JOIN. + * + * @param other The other expression to join with. + * @param on The expression to join on. + * @return The updated query object. + */ + SqlTable &leftJoin(const QString &other, const QString &on) + { + return join(other, on, "LEFT"); + } + + /** Shortcut to perform a RIGHT JOIN. + * + * @param other The other expression to join with. + * @param on The expression to join on. + * @return The updated query object. + */ + SqlTable &rightJoin(const QString &other, const QString &on) + { + return join(other, on, "RIGHT"); + } + + /** Shortcut to perform a INNER JOIN. + * + * @param other The other expression to join with. + * @param on The expression to join on. + * @return The updated query object. + */ + SqlTable &innerJoin(const QString &other, const QString &on) + { + return join(other, on, "INNER"); + } + + /** Get the combined expresison for this table(set). + * + * @return The SQL expression suitable for SELECT or UPDATE queries. + */ + QString expression() const; + + private: + /// Each part of a multi-table JOIN + struct Join { + /// Named type of join + QString typeClause; + /// Table name to join on + QString whatClause; + /// Join-on clause + QString onClause; + }; + + /// List of joined tables. + /// The first item in the list only uses the #whatClause value. + QList _joins; +}; + +#endif /* SQL_TABLE_H */ diff --git a/src/afcsql/SqlTransaction.cpp b/src/afcsql/SqlTransaction.cpp new file mode 100644 index 0000000..62fa834 --- /dev/null +++ b/src/afcsql/SqlTransaction.cpp @@ -0,0 +1,56 @@ +// +#include "SqlTransaction.h" +#include "SqlError.h" +#include "SqlHelpers.h" +#include "afclogging/Logging.h" +#include + +namespace +{ +/// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "SqlTransaction") +} + +SqlTransaction::SqlTransaction(QSqlDatabase &db) : _db(&db) +{ + const bool success = _db->transaction(); + if (SqlHelpers::doDebug()) { + LOGGER_DEBUG(logger) + << "Started on " << _db->connectionName() << " success=" << success; + } + if (!success) { + throw SqlError("Failed to start transaction", _db->lastError()); + } +} + +SqlTransaction::~SqlTransaction() +{ + if (!_db) { + return; + } + if (!_db->isOpen()) { + return; + } + const bool success = _db->rollback(); + if (SqlHelpers::doDebug()) { + LOGGER_DEBUG(logger) + << "Rollback on " << _db->connectionName() << " success=" << success; + } + // no chance to raise exception during destructor +} + +void SqlTransaction::commit() +{ + if (!_db) { + return; + } + const bool success = _db->commit(); + if (SqlHelpers::doDebug()) { + LOGGER_DEBUG(logger) + << "Commit on " << _db->connectionName() << " success=" << success; + } + if (!success) { + throw SqlError("Failed to commit transaction", _db->lastError()); + } + _db = nullptr; +} diff --git a/src/afcsql/SqlTransaction.h b/src/afcsql/SqlTransaction.h new file mode 100644 index 0000000..55fe1c5 --- /dev/null +++ b/src/afcsql/SqlTransaction.h @@ -0,0 +1,38 @@ +// +#ifndef CPOBG_SRC_CPOSQL_SQLTRANSACTION_H_ +#define CPOBG_SRC_CPOSQL_SQLTRANSACTION_H_ + +class QSqlDatabase; + +/** Define a context for DB transactions. + * Upon construction of this class a transaction is started, and unless the + * transaction is committed, the destructor will roll-back the state. + */ +class SqlTransaction +{ + public: + /** Start the transaction. + * + * @param db The database to transact within. + * @throw SqlError if the transaction fails. + * @post The transaction is entered. + */ + SqlTransaction(QSqlDatabase &db); + + /** Rollback the transaction unless already committed. + * @post The transaction has been rolled-back. + */ + ~SqlTransaction(); + + /** Commit the transaction to avoid rollback. + * @throw SqlError if the transaction fails. + * @post The transaction has been committed. + */ + void commit(); + + private: + /// The database, which is non-null during the transaction. + QSqlDatabase *_db; +}; + +#endif /* CPOBG_SRC_CPOSQL_SQLTRANSACTION_H_ */ diff --git a/src/afcsql/SqlUpdate.cpp b/src/afcsql/SqlUpdate.cpp new file mode 100644 index 0000000..4b6e916 --- /dev/null +++ b/src/afcsql/SqlUpdate.cpp @@ -0,0 +1,65 @@ +// + +#include "SqlUpdate.h" +#include "SqlHelpers.h" +#include "SqlTable.h" +#include + +SqlUpdate::SqlUpdate(const QSqlDatabase &db, const QString &tableExpr) : + _db(db), _tableExpr(tableExpr) +{ +} + +SqlUpdate::SqlUpdate(const QSqlDatabase &db, const SqlTable &table) : _db(db) +{ + _tableExpr = table.expression(); +} + +SqlUpdate &SqlUpdate::setPlaceholder(const QString &col) +{ + return set(QString("%1=?").arg(col)); +} + +SqlUpdate &SqlUpdate::setValue(const QString &col, const QVariant &value) +{ + return set(QString("%1=%2").arg(col).arg(SqlHelpers::quoted(_db.driver(), value))); +} + +SqlUpdate &SqlUpdate::whereEqual(const QString &col, const QVariant &value) +{ + const QString valEnc = SqlHelpers::quoted(_db.driver(), value); + const QString op = (value.isNull() ? "IS" : "="); + return where(QString("(%1 %2 %3)").arg(col, op, valEnc)); +} + +SqlUpdate &SqlUpdate::whereNull(const QString &col) +{ + return where(QString("(%1 IS NULL)").arg(col)); +} + +SqlUpdate &SqlUpdate::whereEqualPlaceholder(const QString &col) +{ + return where(QString("(%1 = ?)").arg(col)); +} + +QString SqlUpdate::query() const +{ + QString queryStr = "UPDATE "; + queryStr += _tableExpr; + + // Add list of SET parts + queryStr += " SET " + _setExprs.join(", "); + + // Add list of WHERE clauses + if (!_whereExprs.isEmpty()) { + queryStr += " WHERE " + _whereExprs.join(" AND "); + } + + return queryStr; +} + +QSqlQuery SqlUpdate::run() const +{ + const QString queryStr = query(); + return SqlHelpers::exec(_db, queryStr); +} diff --git a/src/afcsql/SqlUpdate.h b/src/afcsql/SqlUpdate.h new file mode 100644 index 0000000..8004c72 --- /dev/null +++ b/src/afcsql/SqlUpdate.h @@ -0,0 +1,142 @@ +// + +#ifndef SQL_UPDATE_H +#define SQL_UPDATE_H + +#include +#include + +class SqlTable; +class QVariant; + +/** An interface specifically for the particular needs of the UPDATE query. + */ +class SqlUpdate +{ + public: + /** Create an invalid object which can be assigned-to later. + */ + SqlUpdate() + { + } + + /** Create a new UPDATE query on a given database and table. + * @param db The database to query. + * @param tableExpr The definition of the table(s) to update. + */ + SqlUpdate(const QSqlDatabase &db, const QString &tableExpr); + + /** Create a new UPDATE query on a given database and table. + * @param db The database to query. + * @param table The definition of the table(s) to update. + */ + SqlUpdate(const QSqlDatabase &db, const SqlTable &table); + + /** Get the underlying database object. + * @param The database to query. + */ + const QSqlDatabase &database() const + { + return _db; + } + + /** Add a new column and value to set. + * + * @param expr The full SET clause to add. + * @return The updated query object. + */ + SqlUpdate &set(const QString &expr) + { + _setExprs.append(expr); + return *this; + } + + /** Add a new column and value to set. + * + * @param col The column name to set. + * @param expr The SQL expression to set to. + * @return The updated query object. + */ + SqlUpdate &setExpr(const QString &col, const QString &expr) + { + return set(QString("%1=(%2)").arg(col, expr)); + } + + /** Add a new column to set with a prepared query. + * All SET placeholders occur before any WHERE placeholders. + * + * @param col The column name to set. + * @return The updated query object. + * @sa PreparedQuery + */ + SqlUpdate &setPlaceholder(const QString &col); + + /** Add a new column and value to set. + * + * @param col The column name to set. + * @param expr The value to set to. This value will be properly quoted. + * @return The updated query object. + */ + SqlUpdate &setValue(const QString &col, const QVariant &value); + + /** Add an arbitrary WHERE clause to the UPDATE. + * Each call to this function adds a new clause which will be joined with + * the "AND" operator. + * + * @param expr The WHERE expression. + * @return The updated query object. + */ + SqlUpdate &where(const QString &expr) + { + _whereExprs << expr; + return *this; + } + + /** Add a WHERE clause to the UPDATE for a single column. + * @param col The name of the column to filter. + * @param value The value which must be in the column. + * @return The updated query object. + */ + SqlUpdate &whereEqual(const QString &col, const QVariant &value); + + /** Add a WHERE clause to the UPDATE for a single column. + * @param col The name of the column to filter to include @c null values. + * @return The updated query object. + */ + SqlUpdate &whereNull(const QString &col); + + /** Add a WHERE clause to the UPDATE for a single column. + * This is intended to be used in prepared queries, where particular + * values are bound later. + * @param col The name of the column to filter. + * @return The updated query object. + * @sa PreparedQuery + */ + SqlUpdate &whereEqualPlaceholder(const QString &col); + + /** Get the SQL query string which would be executed by run(). + * @return The query string to be executed. + * The order of the query is defined by @xref{SQL-92} with DB-specific + * clauses at end. + */ + QString query() const; + + /** Build and execute the query and return the QSqlQuery result. + * @return The successful result of a query execution. + * @throw SqlError if the query fails to execute. + */ + QSqlQuery run() const; + + private: + /// Underlying database + QSqlDatabase _db; + /// Fully formed table expression + QString _tableExpr; + /// List of quoted column names and values to set + QStringList _setExprs; + /// List of WHERE clauses to be ANDed together, with unbound values + /// The clauses should be parenthesized to avoid error + QStringList _whereExprs; +}; + +#endif /* SQL_UPDATE_H */ diff --git a/src/afcsql/SqlValueMap.cpp b/src/afcsql/SqlValueMap.cpp new file mode 100644 index 0000000..cc707c3 --- /dev/null +++ b/src/afcsql/SqlValueMap.cpp @@ -0,0 +1,126 @@ +// + +#include +#include +#include +#include +#include "SqlValueMap.h" +#include "afclogging/ErrStream.h" + +namespace +{ +/** Determine if a value is floating point. + * + * @param var The variant value. + * @return True if the value is floating point. + */ +bool isFloat(const QVariant &var) +{ + // userType() will either be a QVariant ID or a QMetaType ID + return ((var.userType() == QVariant::Double) || (var.userType() == QMetaType::Float)); +} + +/** Compare two floating point values. + * + * @param valA The first value. + * @param valB The second value. + * @return True if the values are within a fraction of their magnitudes. + */ +bool compareFloat(double valA, double valB) +{ + const double diff = std::abs(valB - valA); + const double mag = std::abs(valA) + std::abs(valB); + if (mag == 0) { + // both are zero + return true; + } + return (diff / mag) < 1e-5; +} +} + +SqlValueMap::SqlValueMap(const QSqlRecord &rec) +{ + const int valCount = rec.count(); + + for (int colI = 0; colI < valCount; ++colI) { + const QSqlField field = rec.field(colI); + _vals.insert(field.name(), field.value()); + } +} + +QVariant SqlValueMap::value(const QString &name) const +{ + const QVariantMap::const_iterator it = _vals.find(name); + if (it == _vals.end()) { + throw std::runtime_error(ErrStream() << "Bad value map name \"" << name << "\""); + } + return it.value(); +} + +QVariantList SqlValueMap::values(const QStringList &names) const +{ + QVariantList vals; + + foreach(const QString &name, names) + { + vals.append(value(name)); + } + + return vals; +} + +SqlValueMap::MismatchMap SqlValueMap::mismatch(const SqlValueMap &other) const +{ + MismatchMap result; + + // QMap iterates in key-order so keys will have same order + QVariantMap::const_iterator itA = _vals.begin(); + QVariantMap::const_iterator itB = other._vals.begin(); + + while ((itA != _vals.end()) && (itB != other._vals.end())) { + if (itA.key() != itB.key()) { + // itB is ahead, other missing at key itA + if (itA.key() < itB.key()) { + result.insert(itA.key(), MismatchPair(itA->toString(), QString())); + ++itA; + } else { + result.insert(itB.key(), MismatchPair(QString(), itB->toString())); + ++itB; + } + } + + bool same; + if (isFloat(itA.value()) || isFloat(itB.value())) { + // compare as floats with tolerance + same = compareFloat(itA->toDouble(), itB->toDouble()); + } else if ((itA->type() == QVariant::String) || (itB->type() == QVariant::String)) { + // compare as strings ignoring case + same = (itA->toString().compare(itB->toString(), Qt::CaseInsensitive) == 0); + } else { + same = (itA.value() == itB.value()); + } + if (!same) { + result.insert(itA.key(), MismatchPair(itA->toString(), itB->toString())); + } + + ++itA; + ++itB; + } + + // Keys only in this map + for (; itA != _vals.end(); ++itA) { + result.insert(itA.key(), MismatchPair(itA->toString(), QString())); + } + // Keys only in other map + for (; itB != other._vals.end(); ++itB) { + result.insert(itB.key(), MismatchPair(QString(), itB->toString())); + } + + return result; +} + +QDebug operator<<(QDebug stream, const SqlValueMap &obj) +{ + stream << obj._vals; + return stream; +} diff --git a/src/afcsql/SqlValueMap.h b/src/afcsql/SqlValueMap.h new file mode 100644 index 0000000..288ecbd --- /dev/null +++ b/src/afcsql/SqlValueMap.h @@ -0,0 +1,78 @@ +// + +#ifndef SQL_VALUE_MAP_H +#define SQL_VALUE_MAP_H + +#include +#include + +/** A class to represent a single row of an SQL query result by a name--value + * map. + */ +class SqlValueMap +{ + public: + /** Pair of mismatched values. + * The @c first value is from this object, the @c second value + * is from the other object. + */ + typedef QPair MismatchPair; + /** Type for result of mismatch() function. + * Map from key string to pair of (this, other) values. + */ + typedef QMap MismatchMap; + + /// Create an empty value map + SqlValueMap() + { + } + + /** Extract a value map from an SQL record. + * + * @param rec The record to extract from. + */ + explicit SqlValueMap(const QSqlRecord &rec); + + /** Get a single value from the set. + * + * @param name The unique name for the value. + * @return The value in the map by the name. + * @throw RuntimeError If the name does not exist. + */ + QVariant value(const QString &name) const; + + /** Get multiple values from the set. + * + * @param names The ordered list of unique names to get. + * If the names are not unique within the list it is an error. + * @return The ordered list of values associated with each name. + * @throw RuntimeError If any name does not exist, or any name is + * duplicated. + */ + QVariantList values(const QStringList &names) const; + + /** Determine if two value sets are identical. + * + * @param other The values to compare against. + * @return An empty map if both sets have identical names and each name has + * identical values. A non-empty map indicates which names (and values) + * are mismatched. + */ + MismatchMap mismatch(const SqlValueMap &other) const; + + friend QDebug operator<<(QDebug stream, const SqlValueMap &obj); + + private: + /// Value storage + QVariantMap _vals; +}; + +/** Debug printing. + * + * @param stream The stream to write. + * @param obj The values to write. + * @return The updated stream. + */ +QDebug operator<<(QDebug stream, const SqlValueMap &obj); + +#endif /* SQL_VALUE_MAP_H */ diff --git a/src/afcsql/test/CMakeLists.txt b/src/afcsql/test/CMakeLists.txt new file mode 100644 index 0000000..a5bb8e6 --- /dev/null +++ b/src/afcsql/test/CMakeLists.txt @@ -0,0 +1,5 @@ +# All source files to same target +file(GLOB ALL_CPP "*.cpp") +add_gtest_executable(${TGT_NAME}-test ${ALL_CPP}) +target_link_libraries(${TGT_NAME}-test PUBLIC ${TGT_NAME}) +target_link_libraries(${TGT_NAME}-test PUBLIC gtest_main) diff --git a/src/afcsql/test/TestSqlHelpers.cpp b/src/afcsql/test/TestSqlHelpers.cpp new file mode 100644 index 0000000..31aa1cd --- /dev/null +++ b/src/afcsql/test/TestSqlHelpers.cpp @@ -0,0 +1,60 @@ +// + +#include "../SqlHelpers.h" +#include +#include + +class TestSqlHelpers : public testing::Test +{ + public: + void SetUp() override + { + QSqlDatabase::addDatabase("QMYSQL", "mysql"); + _conns.push_back("mysql"); + } + + void TearDown() override + { + for (const QString &name : _conns) { + QSqlDatabase::removeDatabase(name); + } + _conns.clear(); + } + + std::vector _conns; +}; + +TEST_F(TestSqlHelpers, testWrite) +{ +} + +#if 0 +void TestSqlHelpers::testQuotedVariant_data(){ + QTest::addColumn("var"); + QTest::addColumn("encoded"); + + QTest::newRow("null") << QVariant() << "NULL"; + QTest::newRow("text") << QVariant("test") << "'test'"; + QTest::newRow("empty") << QVariant(QString()) << "NULL"; + QTest::newRow("bytes") << QVariant(QByteArray("hello")) << "'68656c6c6f'"; + QTest::newRow("number") << QVariant(2) << "2"; + QTest::newRow("utctime") << QVariant(QDateTime(QDate(2013, 1, 2), QTime(3, 4), Qt::UTC)) << "'2013-01-02 03:04:00'"; + //QTest::newRow("localtime") << QVariant(QDateTime(QDate(2013, 1, 2), QTime(3, 4), Qt::LocalTime)) << "'2013-01-02 03:04:00'"; +} + + #include +void TestSqlHelpers::testQuotedVariant(){ + const QFETCH(QVariant, var); + const QFETCH(QString, encoded); + + foreach(const QString &name, _conns){ + QSqlDriver *drv = QSqlDatabase::database(name, false).driver(); + #if 0 + QSqlField field; + field.setValue(var); + qDebug() << drv << var << drv->formatValue(field) << SqlHelpers::quoted(drv, var); + #endif + QCOMPARE(SqlHelpers::quoted(drv, var), encoded); + } +} +#endif diff --git a/src/afcsql/test/TestSqlTimeout.cpp b/src/afcsql/test/TestSqlTimeout.cpp new file mode 100644 index 0000000..546a41e --- /dev/null +++ b/src/afcsql/test/TestSqlTimeout.cpp @@ -0,0 +1,60 @@ +// + +#include "../SqlConnectionDefinition.h" +#include "../SqlScopedConnection.h" +#include "../SqlHelpers.h" +#include "../SqlError.h" + +#if 0 +AUTOTEST_REGISTER(TestSqlTimeout); + +void TestSqlTimeout::initTestCase(){ + _mysqlPort = UnitTestHelpers::randomTcpPort(); + _mysql.reset(new MysqlTestServer(_mysqlPort, QStringList() + << "wait_timeout=1" + )); +} + +void TestSqlTimeout::cleanupTestCase(){ + _mysql.reset(); +} + +void TestSqlTimeout::testAutoReconnect(){ + SqlConnectionDefinition defn; + defn.driverName = "QMYSQL"; + defn.hostName = "127.0.0.1"; + defn.port = _mysqlPort; + + { + SqlScopedConnection conn; + *conn = defn.newConnection(); + QVERIFY(!conn->isOpen()); + conn->tryOpen(); + QVERIFY(conn->isOpen()); + QVERIFY_CATCH("query", SqlHelpers::exec(*conn, "SELECT 1");); + + // wait past timeout + QTest::qWait(1300); + + QVERIFY(conn->isOpen()); + QVERIFY_THROW(SqlError, SqlHelpers::exec(*conn, "SELECT 1");); + } + + // now with reconnect + defn.options = "MYSQL_OPT_RECONNECT=1"; + { + SqlScopedConnection conn; + *conn = defn.newConnection(); + QVERIFY(!conn->isOpen()); + conn->tryOpen(); + QVERIFY(conn->isOpen()); + QVERIFY_CATCH("query", SqlHelpers::exec(*conn, "SELECT 1");); + + // wait past timeout + QTest::qWait(1300); + + QVERIFY(conn->isOpen()); + QVERIFY_CATCH("query", SqlHelpers::exec(*conn, "SELECT 1");); + } +} +#endif diff --git a/src/coalition_ulsprocessor/.vscode/settings.json b/src/coalition_ulsprocessor/.vscode/settings.json new file mode 100644 index 0000000..f1706df --- /dev/null +++ b/src/coalition_ulsprocessor/.vscode/settings.json @@ -0,0 +1,57 @@ +{ + "files.associations": { + "array": "cpp", + "atomic": "cpp", + "*.tcc": "cpp", + "cctype": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "condition_variable": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "list": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "map": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "string": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "fstream": "cpp", + "future": "cpp", + "initializer_list": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "mutex": "cpp", + "new": "cpp", + "ostream": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "thread": "cpp", + "typeinfo": "cpp", + "variant": "cpp" + } +} \ No newline at end of file diff --git a/src/coalition_ulsprocessor/.vscode/tasks.json b/src/coalition_ulsprocessor/.vscode/tasks.json new file mode 100644 index 0000000..4c031a8 --- /dev/null +++ b/src/coalition_ulsprocessor/.vscode/tasks.json @@ -0,0 +1,46 @@ +{ + "tasks": [ + { + "type": "cppbuild", + "label": "C/C++: gcc build active file", + "command": "/usr/bin/gcc", + "args": [ + "-g", + "${file}", + "-o", + "${fileDirname}/${fileBasenameNoExtension}" + ], + "options": { + "cwd": "${fileDirname}" + }, + "problemMatcher": [ + "$gcc" + ], + "group": "build", + "detail": "Task generated by Debugger." + }, + { + "type": "cppbuild", + "label": "C/C++: cpp build active file", + "command": "/usr/bin/cpp", + "args": [ + "-g", + "${file}", + "-o", + "${fileDirname}/${fileBasenameNoExtension}" + ], + "options": { + "cwd": "${fileDirname}" + }, + "problemMatcher": [ + "$gcc" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "detail": "Task generated by Debugger." + } + ], + "version": "2.0.0" +} \ No newline at end of file diff --git a/src/coalition_ulsprocessor/CMakeLists.txt b/src/coalition_ulsprocessor/CMakeLists.txt new file mode 100644 index 0000000..ab64ba3 --- /dev/null +++ b/src/coalition_ulsprocessor/CMakeLists.txt @@ -0,0 +1,10 @@ +# External version naming +file(READ "${CMAKE_SOURCE_DIR}/version.txt" VERSIONFILE) +string(STRIP ${VERSIONFILE} VERSIONFILE) +project(uls-script VERSION ${VERSIONFILE}) + +set(SOVERSION "${PROJECT_VERSION}") + +# Build/install in source path +add_subdirectory(src) + diff --git a/src/coalition_ulsprocessor/build.bat b/src/coalition_ulsprocessor/build.bat new file mode 100644 index 0000000..ae1d8b9 --- /dev/null +++ b/src/coalition_ulsprocessor/build.bat @@ -0,0 +1,52 @@ +@rem Script to build each project and install into ./testroot +call vcvarsall.bat x86_amd64 +if errorlevel 1 ( pause ) + +set ROOTDIR=%~dp0 +set TESTROOT=%ROOTDIR%\testroot + +@rem Use utilities and runtime dependencies +set PATH=%TESTROOT%\bin;%PATH% +set BUILDTYPE=RelWithDebInfo +set CMAKE=cmake ^ + -DCMAKE_BUILD_TYPE=%BUILDTYPE% ^ + -DCMAKE_INSTALL_PREFIX="%TESTROOT%" ^ + -DCMAKE_PREFIX_PATH="%TESTROOT%" ^ + -DCPO_TEST_MODE=BOOL:TRUE ^ + -G "Ninja" +set NINJA=ninja -k10 + +@rem Set these to "1" to enable build parts +set DO_CONANENV=1 +set DO_BUILD=1 + +if %DO_CONANENV%==1 ( + pushd conanenv + + conan install conanfile.py -s build_type=%BUILDTYPE% + if errorlevel 1 ( pause ) + rmdir /s /q importroot + + @rem unpack components to testroot + 7z x -y -o"%TESTROOT%" uls-deps-runtime.zip + if errorlevel 1 ( pause ) + + popd +) + +if %DO_BUILD%==1 ( + @rem CPOBG sub-project + mkdir build + cd build + + %CMAKE% .. + if errorlevel 1 ( pause ) + %NINJA% + if errorlevel 1 ( pause ) + %NINJA% install + if errorlevel 1 ( pause ) + + popd +) + +pause diff --git a/src/coalition_ulsprocessor/build.sh b/src/coalition_ulsprocessor/build.sh new file mode 100644 index 0000000..5106e05 --- /dev/null +++ b/src/coalition_ulsprocessor/build.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Script to build each project and install into ./testroot +set -e + +ROOTDIR=$(readlink -f $(dirname "${BASH_SOURCE[0]}")) +BUILDTYPE="RelWithDebInfo" +CMAKE="cmake3 \ + -DCMAKE_BUILD_TYPE=${BUILDTYPE} \ + -DCMAKE_INSTALL_PREFIX=${ROOTDIR}/testroot \ + -DCMAKE_PREFIX_PATH=${ROOTDIR}/testroot \ + -G Ninja" +NINJA="ninja-build" + +# CPOBG libraries +BUILDDIR=${ROOTDIR}/build +mkdir -p $BUILDDIR +pushd $BUILDDIR + +${CMAKE} .. +${NINJA} +${NINJA} install + +popd \ No newline at end of file diff --git a/src/coalition_ulsprocessor/cmake/CheckCoverage.cmake b/src/coalition_ulsprocessor/cmake/CheckCoverage.cmake new file mode 100644 index 0000000..f0656be --- /dev/null +++ b/src/coalition_ulsprocessor/cmake/CheckCoverage.cmake @@ -0,0 +1,40 @@ +# Add a target to allow test coverage analysis (all tests together). +# This include fails on non-unix systems + +if(NOT UNIX) + message(FATAL_ERROR "Unable to coverage-check non-unix") +endif(NOT UNIX) + + +FIND_PROGRAM(LCOV_PATH lcov) +FIND_PROGRAM(GENHTML_PATH genhtml) + +IF(NOT LCOV_PATH) + MESSAGE(FATAL_ERROR "lcov not found") +ENDIF(NOT LCOV_PATH) + +IF(NOT GENHTML_PATH) + MESSAGE(FATAL_ERROR "genhtml not found!") +ENDIF(NOT GENHTML_PATH) + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --coverage") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage") + +# Target to check with pre- and post-steps to capture coverage results +SET(OUT_RAW "${CMAKE_BINARY_DIR}/coverage-raw.lcov") +SET(OUT_CLEAN "${CMAKE_BINARY_DIR}/coverage-clean.lcov") +add_custom_target(check-coverage + # Before any tests + COMMAND ${LCOV_PATH} --directory . --zerocounters + + # Actually run the tests, ignoring exit code + COMMAND ${CMAKE_CTEST_COMMAND} --verbose || : + + # Pull together results, ignoring system files and auto-built files + COMMAND ${LCOV_PATH} --directory . --capture --output-file ${OUT_RAW} + COMMAND ${LCOV_PATH} --remove ${OUT_RAW} '*/test/*' '${CMAKE_BINARY_DIR}/*' '/usr/*' --output-file ${OUT_CLEAN} + COMMAND ${GENHTML_PATH} -o coverage ${OUT_CLEAN} + COMMAND ${CMAKE_COMMAND} -E remove ${OUT_RAW} ${OUT_CLEAN} + + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} +) diff --git a/src/coalition_ulsprocessor/cmake/FindGoogleBreakpad.cmake b/src/coalition_ulsprocessor/cmake/FindGoogleBreakpad.cmake new file mode 100644 index 0000000..e8f1805 --- /dev/null +++ b/src/coalition_ulsprocessor/cmake/FindGoogleBreakpad.cmake @@ -0,0 +1,37 @@ + +IF(UNIX) + find_package(PkgConfig) + pkg_search_module(BREAKPAD REQUIRED google-breakpad) +ENDIF(UNIX) +IF(WIN32) + find_path(BREAKPAD_HEADER_DIR common/basictypes.h ${BREAKPAD_INCLUDE_DIRS} ${CONAN_INCLUDE_DIRS}) + + add_library(breakpad_common STATIC IMPORTED) + find_library(BREAKPAD_COMMON_LIB common) + set_target_properties(breakpad_common PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${BREAKPAD_HEADER_DIR} + IMPORTED_LOCATION "${BREAKPAD_COMMON_LIB}" + ) + add_library(breakpad_handler STATIC IMPORTED) + find_library(BREAKPAD_HANDLER_LIB exception_handler) + set_target_properties(breakpad_handler PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${BREAKPAD_HEADER_DIR} + IMPORTED_LOCATION "${BREAKPAD_HANDLER_LIB}" + ) + add_library(breakpad_client STATIC IMPORTED) + find_library(BREAKPAD_CLIENT_LIB crash_generation_client) + set_target_properties(breakpad_client PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${BREAKPAD_HEADER_DIR} + IMPORTED_LOCATION "${BREAKPAD_CLIENT_LIB}" + ) + + # handle the QUIETLY and REQUIRED arguments and set JPEG_FOUND to TRUE if + # all listed variables are TRUE + include(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(BREAKPAD DEFAULT_MSG BREAKPAD_HEADER_DIR BREAKPAD_COMMON_LIB BREAKPAD_HANDLER_LIB BREAKPAD_CLIENT_LIB) + + if(BREAKPAD_FOUND) + set(BREAKPAD_INCLUDE_DIRS ${BREAKPAD_HEADER_DIR}) + set(BREAKPAD_LIBRARIES breakpad_common breakpad_handler breakpad_client) + endif(BREAKPAD_FOUND) +ENDIF(WIN32) diff --git a/src/coalition_ulsprocessor/cmake/FindQwt.cmake b/src/coalition_ulsprocessor/cmake/FindQwt.cmake new file mode 100644 index 0000000..b4eea69 --- /dev/null +++ b/src/coalition_ulsprocessor/cmake/FindQwt.cmake @@ -0,0 +1,40 @@ + +IF(UNIX) + find_package(PkgConfig) + pkg_search_module(QWT REQUIRED Qt5Qwt6) + if(NOT ${QWT_FOUND} EQUAL 1) + message(FATAL_ERROR "Qwt is missing") + endif() + set(QWT_LIBNAME "qwt-qt5") +ENDIF(UNIX) +IF(WIN32) + # Always using shared library + add_definitions(-DQWT_DLL) + + # Verify headers present + find_path(QWT_MAIN_INCLUDE qwt.h PATHS ${QWT_INCLUDE_DIRS} ${CONAN_INCLUDE_DIRS_QWT}) + + # Verify dynamic library present + find_library(QWT_MAIN_LIB NAMES qwt qwtd PATHS ${QWT_LIBDIR} ${CONAN_LIB_DIRS_QWT}) + find_file(QWT_MAIN_DLL NAMES qwt.dll qwtd.dll PATHS ${QWT_BINDIR} ${CONAN_BIN_DIRS_QWT}) + message("-- Found Qwt at ${QWT_MAIN_LIB} ${QWT_MAIN_DLL}") + + add_library(qwt-qt5 SHARED IMPORTED) + set_target_properties(qwt-qt5 PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${QWT_MAIN_INCLUDE} + ) + set_target_properties(qwt-qt5 PROPERTIES + IMPORTED_LOCATION "${QWT_MAIN_DLL}" + IMPORTED_IMPLIB "${QWT_MAIN_LIB}" + ) + + # handle the QUIETLY and REQUIRED arguments and set JPEG_FOUND to TRUE if + # all listed variables are TRUE + include(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(QWT DEFAULT_MSG QWT_MAIN_LIB QWT_MAIN_INCLUDE) + + if(QWT_FOUND) + set(QWT_INCLUDE_DIRS ${QWT_MAIN_INCLUDE}) + set(QWT_LIBRARIES qwt-qt5) + endif(QWT_FOUND) +ENDIF(WIN32) diff --git a/src/coalition_ulsprocessor/cmake/FindV8.cmake b/src/coalition_ulsprocessor/cmake/FindV8.cmake new file mode 100644 index 0000000..81625d4 --- /dev/null +++ b/src/coalition_ulsprocessor/cmake/FindV8.cmake @@ -0,0 +1,83 @@ + +IF(UNIX) + # Verify headers present + find_path(V8_MAIN_INCLUDE v8.h ${V8_INCLUDE_DIR}) + + # Verify dynamic library present + find_library(V8_MAIN_LIB v8 ${V8_LIBDIR}) + message("-- Found V8 at ${V8_MAIN_LIB}") + add_library(v8 SHARED IMPORTED) + set_target_properties(v8 PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${V8_MAIN_INCLUDE} + ) + set_target_properties(v8 PROPERTIES + IMPORTED_LOCATION "${V8_MAIN_LIB}" + ) + + find_library(V8_LIBBASE_LIB v8_libbase ${V8_LIBDIR}) + message("-- Found v8_libbase at ${V8_LIBBASE_LIB}") + add_library(v8_libbase SHARED IMPORTED) + set_target_properties(v8_libbase PROPERTIES + IMPORTED_LOCATION "${V8_LIBBASE_LIB}" + ) + + find_library(V8_LIBPLATFORM_LIB v8_libplatform ${V8_LIBDIR}) + message("-- Found v8_libplatform at ${V8_LIBPLATFORM_LIB}") + add_library(v8_libplatform SHARED IMPORTED) + set_target_properties(v8_libplatform PROPERTIES + IMPORTED_LOCATION "${V8_LIBPLATFORM_LIB}" + ) + + # handle the QUIETLY and REQUIRED arguments and set V8_FOUND to TRUE if + # all listed variables are TRUE + include(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(V8 DEFAULT_MSG V8_MAIN_INCLUDE V8_MAIN_LIB V8_LIBBASE_LIB V8_LIBPLATFORM_LIB) + + if(V8_FOUND) + set(V8_INCLUDE_DIRS ${V8_MAIN_INCLUDE}) + set(V8_LIBRARIES v8 v8_libplatform v8_libbase) + endif(V8_FOUND) +ENDIF(UNIX) +IF(WIN32) + # Verify headers present + find_path(V8_MAIN_INCLUDE v8.h ${V8_INCLUDE_DIR}) + + # Verify dynamic library present + find_library(V8_MAIN_LIB v8.dll.lib ${V8_LIBDIR}) + find_file(V8_MAIN_DLL v8.dll ${V8_BINDIR}) + message("-- Found V8 at ${V8_MAIN_LIB}") + add_library(v8 SHARED IMPORTED) + set_target_properties(v8 PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${V8_MAIN_INCLUDE} + ) + set_target_properties(v8 PROPERTIES + IMPORTED_LOCATION "${V8_MAIN_DLL}" + IMPORTED_IMPLIB "${V8_MAIN_LIB}" + ) + + find_library(V8_LIBBASE_LIB v8_libbase.dll.lib ${V8_LIBDIR}) + find_library(V8_LIBBASE_DLL v8_libbase.dll ${V8_BINDIR}) + add_library(v8_libbase SHARED IMPORTED) + set_target_properties(v8_libbase PROPERTIES + IMPORTED_LOCATION "${V8_LIBBASE_DLL}" + IMPORTED_IMPLIB "${V8_LIBBASE_LIB}" + ) + + find_library(V8_LIBPLATFORM_LIB v8_libplatform.dll.lib ${V8_LIBDIR}) + find_library(V8_LIBPLATFORM_DLL v8_libplatform.dll ${V8_BINDIR}) + add_library(v8_libplatform SHARED IMPORTED) + set_target_properties(v8_libplatform PROPERTIES + IMPORTED_LOCATION "${V8_LIBPLATFORM_DLL}" + IMPORTED_IMPLIB "${V8_LIBPLATFORM_LIB}" + ) + + # handle the QUIETLY and REQUIRED arguments and set V8_FOUND to TRUE if + # all listed variables are TRUE + include(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(V8 DEFAULT_MSG V8_MAIN_INCLUDE V8_MAIN_LIB V8_LIBBASE_LIB V8_LIBPLATFORM_LIB) + + if(V8_FOUND) + set(V8_INCLUDE_DIRS ${V8_MAIN_INCLUDE}) + set(V8_LIBRARIES v8 v8_libplatform v8_libbase winmm) + endif(V8_FOUND) +ENDIF(WIN32) diff --git a/src/coalition_ulsprocessor/cmake/Findlibdaemon.cmake b/src/coalition_ulsprocessor/cmake/Findlibdaemon.cmake new file mode 100644 index 0000000..470d269 --- /dev/null +++ b/src/coalition_ulsprocessor/cmake/Findlibdaemon.cmake @@ -0,0 +1,11 @@ + +IF(UNIX) + find_package(PkgConfig) + pkg_search_module(LIBDAEMON REQUIRED libdaemon) + if(NOT ${LIBDAEMON_FOUND} EQUAL 1) + message(FATAL_ERROR "libdaemon is missing") + endif() +ENDIF(UNIX) +IF(WIN32) + message(FATAL_ERROR "libdaemon not on Windows") +ENDIF(WIN32) diff --git a/src/coalition_ulsprocessor/cmake/Findminizip.cmake b/src/coalition_ulsprocessor/cmake/Findminizip.cmake new file mode 100644 index 0000000..b00b376 --- /dev/null +++ b/src/coalition_ulsprocessor/cmake/Findminizip.cmake @@ -0,0 +1,36 @@ + +IF(UNIX) + find_package(PkgConfig) + pkg_search_module(MINIZIP REQUIRED minizip) + if(NOT ${MINIZIP_FOUND} EQUAL 1) + message(FATAL_ERROR "minizip is missing") + endif() +ENDIF(UNIX) +IF(WIN32) + # Verify headers present + find_path(MINIZIP_MAIN_INCLUDE minizip/zip.h PATHS ${MINIZIP_INCLUDE_DIRS} ${CONAN_INCLUDE_DIRS}) + + # Verify link and dynamic library present + find_library(MINIZIP_MAIN_LIB NAMES minizip minizipd PATHS ${MINIZIP_LIBDIR} ${CONAN_LIB_DIRS}) + find_file(MINIZIP_MAIN_DLL NAMES minizip.dll minizipd.dll PATHS ${MINIZIP_BINDIR} ${CONAN_BIN_DIRS}) + message("-- Found minizip at ${MINIZIP_MAIN_LIB} ${MINIZIP_MAIN_DLL}") + + add_library(minizip SHARED IMPORTED) + set_target_properties(minizip PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${MINIZIP_MAIN_INCLUDE} + ) + set_target_properties(minizip PROPERTIES + IMPORTED_LOCATION "${MINIZIP_MAIN_DLL}" + IMPORTED_IMPLIB "${MINIZIP_MAIN_LIB}" + ) + + # handle the QUIETLY and REQUIRED arguments and set JPEG_FOUND to TRUE if + # all listed variables are TRUE + include(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(MINIZIP DEFAULT_MSG MINIZIP_MAIN_LIB MINIZIP_MAIN_INCLUDE) + + if(MINIZIP_FOUND) + set(MINIZIP_INCLUDE_DIRS ${MINIZIP_MAIN_INCLUDE}) + set(MINIZIP_LIBRARIES minizip) + endif(MINIZIP_FOUND) +ENDIF(WIN32) diff --git a/src/coalition_ulsprocessor/cmake/FindosgQt.cmake b/src/coalition_ulsprocessor/cmake/FindosgQt.cmake new file mode 100644 index 0000000..6704a4f --- /dev/null +++ b/src/coalition_ulsprocessor/cmake/FindosgQt.cmake @@ -0,0 +1,36 @@ + +IF(UNIX) + find_package(PkgConfig) + pkg_search_module(OSGQT REQUIRED openscenegraph-osgQt5 openscenegraph-osgQt) + if(NOT ${OSGQT_FOUND} EQUAL 1) + message(FATAL_ERROR "osgQt is missing") + endif() +ENDIF(UNIX) +IF(WIN32) + # Verify headers present + find_path(OSGQT_MAIN_INCLUDE osgQt/GraphicsWindowQt PATHS ${OSGQT_INCLUDE_DIRS} ${CONAN_INCLUDE_DIRS_OSGQT}) + + # Verify dynamic library present + find_library(OSGQT_MAIN_LIB NAMES osgQt5 osgQt osgQt5d osgQtd PATHS ${OSGQT_LIBDIR} ${CONAN_LIB_DIRS_OSGQT}) + find_file(OSGQT_MAIN_DLL NAMES osgQt5.dll osgQt.dll osgQt5d.dll osgQtd.dll PATHS ${OSGQT_BINDIR} ${CONAN_BIN_DIRS_OSGQT}) + message("-- Found osgQt at ${OSGQT_MAIN_LIB} ${OSGQT_MAIN_DLL}") + + add_library(osgQt SHARED IMPORTED) + set_target_properties(osgQt PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${OSGQT_MAIN_INCLUDE} + ) + set_target_properties(osgQt PROPERTIES + IMPORTED_LOCATION "${OSGQT_MAIN_DLL}" + IMPORTED_IMPLIB "${OSGQT_MAIN_LIB}" + ) + + # handle the QUIETLY and REQUIRED arguments and set JPEG_FOUND to TRUE if + # all listed variables are TRUE + include(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(OSGQT DEFAULT_MSG OSGQT_MAIN_LIB OSGQT_MAIN_INCLUDE) + + if(OSGQT_FOUND) + set(OSGQT_INCLUDE_DIRS ${OSGQT_MAIN_INCLUDE}) + set(OSGQT_LIBRARIES osgQt) + endif(OSGQT_FOUND) +ENDIF(WIN32) diff --git a/src/coalition_ulsprocessor/cmake/Findosgearth.cmake b/src/coalition_ulsprocessor/cmake/Findosgearth.cmake new file mode 100644 index 0000000..0852734 --- /dev/null +++ b/src/coalition_ulsprocessor/cmake/Findosgearth.cmake @@ -0,0 +1,128 @@ +# This module defines + +# OSGEARTH_LIBRARY +# OSGEARTH_FOUND, if false, do not try to link to osg +# OSGEARTH_INCLUDE_DIRS, where to find the headers +# OSGEARTH_INCLUDE_DIR, where to find the source headers +# OSGEARTH_GEN_INCLUDE_DIR, where to find the generated headers + +# to use this module, set variables to point to the osg build +# directory, and source directory, respectively +# OSGEARTHDIR or OSGEARTH_SOURCE_DIR: osg source directory, typically OpenSceneGraph +# OSGEARTH_DIR or OSGEARTH_BUILD_DIR: osg build directory, place in which you've +# built osg via cmake + +# Header files are presumed to be included like +# #include +# #include + +###### headers ###### + +MACRO( FIND_OSGEARTH_INCLUDE THIS_OSGEARTH_INCLUDE_DIR THIS_OSGEARTH_INCLUDE_FILE ) + +FIND_PATH( ${THIS_OSGEARTH_INCLUDE_DIR} ${THIS_OSGEARTH_INCLUDE_FILE} + PATHS + ${OSGEARTH_DIR} + $ENV{OSGEARTH_SOURCE_DIR} + $ENV{OSGEARTHDIR} + $ENV{OSGEARTH_DIR} + $ENV{OSGEO4W_ROOT} + /usr/local/ + /usr/ + /sw/ # Fink + /opt/local/ # DarwinPorts + /opt/csw/ # Blastwave + /opt/ + [HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session\ Manager\\Environment;OSGEARTH_ROOT]/ + ~/Library/Frameworks + /Library/Frameworks + PATH_SUFFIXES + /include/ +) + +ENDMACRO( FIND_OSGEARTH_INCLUDE THIS_OSGEARTH_INCLUDE_DIR THIS_OSGEARTH_INCLUDE_FILE ) + +FIND_OSGEARTH_INCLUDE( OSGEARTH_GEN_INCLUDE_DIR osgEarth/Common ) +FIND_OSGEARTH_INCLUDE( OSGEARTH_INCLUDE_DIR osgEarth/TileSource ) +FIND_OSGEARTH_INCLUDE( OSGEARTH_ELEVATION_QUERY osgEarth/ElevationQuery ) + +###### libraries ###### + +MACRO( FIND_OSGEARTH_LIBRARY MYLIBRARY ) + +FIND_LIBRARY(${MYLIBRARY} + NAMES + ${ARGN} + PATHS + ${OSGEARTH_DIR} + $ENV{OSGEARTH_BUILD_DIR} + $ENV{OSGEARTH_DIR} + $ENV{OSGEARTHDIR} + $ENV{OSGEARTH_ROOT} + $ENV{OSGEO4W_ROOT} + ~/Library/Frameworks + /Library/Frameworks + /usr/local + /usr + /sw + /opt/local + /opt/csw + /opt + [HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session\ Manager\\Environment;OSGEARTH_ROOT]/lib + /usr/freeware + PATH_SUFFIXES + /lib/ + /lib64/ + /build/lib/ + /build/lib64/ + /Build/lib/ + /Build/lib64/ + ) + +ENDMACRO(FIND_OSGEARTH_LIBRARY LIBRARY LIBRARYNAME) + +FIND_OSGEARTH_LIBRARY( OSGEARTH_LIBRARY osgEarth ) +FIND_OSGEARTH_LIBRARY( OSGEARTH_LIBRARY_DEBUG osgEarthd ) + +FIND_OSGEARTH_LIBRARY( OSGEARTHUTIL_LIBRARY osgEarthUtil ) +FIND_OSGEARTH_LIBRARY( OSGEARTHUTIL_LIBRARY_DEBUG osgEarthUtild ) + +FIND_OSGEARTH_LIBRARY( OSGEARTHFEATURES_LIBRARY osgEarthFeatures ) +FIND_OSGEARTH_LIBRARY( OSGEARTHFEATURES_LIBRARY_DEBUG osgEarthFeaturesd ) + +FIND_OSGEARTH_LIBRARY( OSGEARTHSYMBOLOGY_LIBRARY osgEarthSymbology ) +FIND_OSGEARTH_LIBRARY( OSGEARTHSYMBOLOGY_LIBRARY_DEBUG osgEarthSymbologyd ) + +FIND_OSGEARTH_LIBRARY( OSGEARTHQT_LIBRARY osgEarthQt5 osgEarthQt) +FIND_OSGEARTH_LIBRARY( OSGEARTHQT_LIBRARY_DEBUG osgEarthQtd osgEarthQt5d) + +FIND_OSGEARTH_LIBRARY( OSGEARTHANNOTATION_LIBRARY osgEarthAnnotation ) +FIND_OSGEARTH_LIBRARY( OSGEARTHANNOTATION_LIBRARY_DEBUG osgEarthAnnotationd ) + + +SET( OSGEARTH_FOUND "NO" ) +IF( OSGEARTH_LIBRARY AND OSGEARTH_INCLUDE_DIR ) + SET( OSGEARTH_FOUND "YES" ) + SET( OSGEARTH_INCLUDE_DIRS ${OSGEARTH_INCLUDE_DIR} ${OSGEARTH_GEN_INCLUDE_DIR} ) + INCLUDE(CheckCXXSourceCompiles) + SET(SAFE_CMAKE_REQUIRED_INCLUDES ${CMAKE_REQUIRED_INCLUDES}) + SET(SAFE_CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES}) + SET(CMAKE_REQUIRED_INCLUDES ${CMAKE_REQUIRED_INCLUDES} ${OSGEARTH_INCLUDE_DIR}) + SET(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} ${OSGEARTHUTIL_LIBRARY}) + IF(APPLE) + # no extra LDFLAGS used in link test, may fail in OS X SDK + SET(CMAKE_REQUIRED_LIBRARIES "-F/Library/Frameworks" ${CMAKE_REQUIRED_LIBRARIES}) + ENDIF(APPLE) + CHECK_CXX_SOURCE_COMPILES(" +#include +using namespace osgEarth::Util::Controls; +int main(int argc, char **argv) +{ + Container *c; + c->setChildSpacing(0.0); +} +" HAVE_OSGEARTH_CHILD_SPACING) + SET(CMAKE_REQUIRED_INCLUDES ${SAFE_CMAKE_REQUIRED_INCLUDES}) + SET(CMAKE_REQUIRED_LIBRARIES ${SAFE_CMAKE_REQUIRED_LIBRARIES}) + GET_FILENAME_COMPONENT( OSGEARTH_LIBRARIES_DIR ${OSGEARTH_LIBRARY} PATH ) +ENDIF( OSGEARTH_LIBRARY AND OSGEARTH_INCLUDE_DIR ) diff --git a/src/coalition_ulsprocessor/cmake/SubdirUtil.cmake b/src/coalition_ulsprocessor/cmake/SubdirUtil.cmake new file mode 100644 index 0000000..63a2683 --- /dev/null +++ b/src/coalition_ulsprocessor/cmake/SubdirUtil.cmake @@ -0,0 +1,13 @@ + +# Utility for exposing immediate subdirectories +MACRO(SUBDIRLIST result curdir) + FILE(GLOB children RELATIVE ${curdir} ${curdir}/*) + SET(dirlist "") + FOREACH(child ${children}) + IF(IS_DIRECTORY ${curdir}/${child}) + LIST(APPEND dirlist ${child}) + ENDIF() + ENDFOREACH() + SET(${result} ${dirlist}) +ENDMACRO() + diff --git a/src/coalition_ulsprocessor/cmake/srcfunctions.cmake b/src/coalition_ulsprocessor/cmake/srcfunctions.cmake new file mode 100644 index 0000000..bdf4ebe --- /dev/null +++ b/src/coalition_ulsprocessor/cmake/srcfunctions.cmake @@ -0,0 +1,422 @@ +# Redirect add_... functions to accumulate target names + +# +# Define a library from sources with its headers. +# This relies on the pre-existing values: +# - "VERSION" to set the library PROJECT_VERSION property +# - "SOVERSION" to set the library SOVERSION property +# +function(add_dist_library) + set(PARSE_OPTS ) + set(PARSE_ARGS_SINGLE TARGET EXPORTNAME) + set(PARSE_ARGS_MULTI SOURCES HEADERS) + cmake_parse_arguments(ADD_DIST_LIB "${PARSE_OPTS}" "${PARSE_ARGS_SINGLE}" "${PARSE_ARGS_MULTI}" ${ARGN}) + if("${ADD_DIST_LIB_TARGET}" STREQUAL "") + message(FATAL_ERROR "add_dist_library missing TARGET parameter") + endif() + if("${ADD_DIST_LIB_EXPORTNAME}" STREQUAL "") + message(FATAL_ERROR "add_dist_library missing EXPORTNAME parameter") + endif() + if("${ADD_DIST_LIB_SOURCES}" STREQUAL "") + message(FATAL_ERROR "add_dist_library missing SOURCES parameter") + endif() + + if(WIN32 AND BUILD_SHARED_LIBS) + # Give the DLL version markings + set(WINRES_COMPANY_NAME_STR "OpenAFC") + set(WINRES_PRODUCT_NAME_STR ${PROJECT_NAME}) + set(WINRES_PRODUCT_VERSION_RES "${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0") + set(WINRES_PRODUCT_VERSION_STR "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}-${SVN_LAST_REVISION}") + set(WINRES_INTERNAL_NAME_STR ${ADD_DIST_LIB_TARGET}) + set(WINRES_ORIG_FILENAME "${CMAKE_SHARED_LIBRARY_PREFIX}${ADD_DIST_LIB_TARGET}${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(WINRES_FILE_DESCRIPTION_STR "Runtime for ${ADD_DIST_LIB_TARGET}") + set(WINRES_FILE_VERSION_RES ${WINRES_PRODUCT_VERSION_RES}) + set(WINRES_FILE_VERSION_STR ${WINRES_PRODUCT_VERSION_STR}) + set(WINRES_COMMENTS_STR "") + configure_file("${CMAKE_SOURCE_DIR}/src/libinfo.rc.in" "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_LIB_TARGET}-libinfo.rc" @ONLY) + list(APPEND ADD_DIST_LIB_SOURCES "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_LIB_TARGET}-libinfo.rc") + endif(WIN32 AND BUILD_SHARED_LIBS) + + add_library(${ADD_DIST_LIB_TARGET} ${ADD_DIST_LIB_SOURCES}) + set_target_properties( + ${ADD_DIST_LIB_TARGET} PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${SOVERSION} + ) + + include(GenerateExportHeader) + generate_export_header(${ADD_DIST_LIB_TARGET}) + target_include_directories(${ADD_DIST_LIB_TARGET} PUBLIC + $ + $/${PKG_INSTALL_INCLUDEDIR}> + ) + list(APPEND ADD_DIST_LIB_HEADERS "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_LIB_TARGET}_export.h") + + # Source-directory relative path + get_filename_component(SOURCE_DIRNAME ${CMAKE_CURRENT_SOURCE_DIR} NAME) + + # Include headers, with original directory name + install( + FILES ${ADD_DIST_LIB_HEADERS} + DESTINATION ${PKG_INSTALL_INCLUDEDIR}/${SOURCE_DIRNAME} + COMPONENT development + ) + + if(WIN32) + # PDB for symbol mapping + install( + FILES "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${ADD_DIST_LIB_TARGET}.pdb" + DESTINATION ${PKG_INSTALL_DEBUGDIR} + COMPONENT debuginfo + ) + # Sources for debugger (directory name is target name) + install( + FILES ${ADD_DIST_LIB_HEADERS} ${ADD_DIST_LIB_SOURCES} + DESTINATION ${PKG_INSTALL_DEBUGDIR}/${SOURCE_DIRNAME} + COMPONENT debuginfo + ) + endif(WIN32) + + install( + TARGETS ${ADD_DIST_LIB_TARGET} + EXPORT ${ADD_DIST_LIB_EXPORTNAME} + # For Win32 + RUNTIME + DESTINATION ${PKG_INSTALL_BINDIR} + COMPONENT runtime + ARCHIVE + DESTINATION ${PKG_INSTALL_LIBDIR} + COMPONENT development + # For unix + LIBRARY + DESTINATION ${PKG_INSTALL_LIBDIR} + COMPONENT runtime + ) +endfunction(add_dist_library) + +function(add_dist_module) + set(PARSE_OPTS ) + set(PARSE_ARGS_SINGLE TARGET EXPORTNAME COMPONENT) + set(PARSE_ARGS_MULTI SOURCES HEADERS) + cmake_parse_arguments(ADD_DIST_LIB "${PARSE_OPTS}" "${PARSE_ARGS_SINGLE}" "${PARSE_ARGS_MULTI}" ${ARGN}) + if("${ADD_DIST_LIB_TARGET}" STREQUAL "") + message(FATAL_ERROR "add_dist_library missing TARGET parameter") + endif() + if("${ADD_DIST_LIB_COMPONENT}" STREQUAL "") + set(ADD_DIST_LIB_COMPONENT runtime) + endif() + if("${ADD_DIST_LIB_SOURCES}" STREQUAL "") + message(FATAL_ERROR "add_dist_library missing SOURCES parameter") + endif() + if(NOT PKG_MODULE_LIBDIR) + message(FATAL_ERROR "Must define PKG_MODULE_LIBDIR for installation") + endif() + + if(WIN32 AND BUILD_SHARED_LIBS) + # Give the DLL version markings + set(WINRES_COMPANY_NAME_STR "OpenAFC") + set(WINRES_PRODUCT_NAME_STR ${PROJECT_NAME}) + set(WINRES_PRODUCT_VERSION_RES "${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0") + set(WINRES_PRODUCT_VERSION_STR "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}-${SVN_LAST_REVISION}") + set(WINRES_INTERNAL_NAME_STR ${ADD_DIST_LIB_TARGET}) + set(WINRES_ORIG_FILENAME "${CMAKE_SHARED_LIBRARY_PREFIX}${ADD_DIST_LIB_TARGET}${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(WINRES_FILE_DESCRIPTION_STR "Runtime for ${ADD_DIST_LIB_TARGET}") + set(WINRES_FILE_VERSION_RES ${WINRES_PRODUCT_VERSION_RES}) + set(WINRES_FILE_VERSION_STR ${WINRES_PRODUCT_VERSION_STR}) + set(WINRES_COMMENTS_STR "") + configure_file("${CMAKE_SOURCE_DIR}/src/libinfo.rc.in" "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_LIB_TARGET}-libinfo.rc" @ONLY) + list(APPEND ADD_DIST_LIB_SOURCES "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_LIB_TARGET}-libinfo.rc") + endif(WIN32 AND BUILD_SHARED_LIBS) + + add_library(${ADD_DIST_LIB_TARGET} MODULE ${ADD_DIST_LIB_SOURCES}) + set_target_properties( + ${ADD_DIST_LIB_TARGET} PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${SOVERSION} + # no "lib" prefix on unix + PREFIX "" + ) + + include(GenerateExportHeader) + generate_export_header(${ADD_DIST_LIB_TARGET}) + list(APPEND ADD_DIST_LIB_HEADERS "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_LIB_TARGET}_export.h") + + # Source-directory relative path + get_filename_component(SOURCE_DIRNAME ${CMAKE_CURRENT_SOURCE_DIR} NAME) + + if(WIN32) + # PDB for symbol mapping + install( + FILES "${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}/${ADD_DIST_LIB_TARGET}.pdb" + DESTINATION ${PKG_INSTALL_DEBUGDIR} + COMPONENT debuginfo + ) + # Sources for debugger (directory name is target name) + install( + FILES ${ADD_DIST_LIB_HEADERS} ${ADD_DIST_LIB_SOURCES} + DESTINATION ${PKG_INSTALL_DEBUGDIR}/${SOURCE_DIRNAME} + COMPONENT debuginfo + ) + endif(WIN32) + + install( + TARGETS ${ADD_DIST_LIB_TARGET} + EXPORT ${ADD_DIST_LIB_EXPORTNAME} + # For Win32 + RUNTIME + DESTINATION ${PKG_MODULE_LIBDIR} + COMPONENT ${ADD_DIST_LIB_COMPONENT} + ARCHIVE + DESTINATION ${PKG_INSTALL_LIBDIR} + COMPONENT development + # For unix + LIBRARY + DESTINATION ${PKG_MODULE_LIBDIR} + COMPONENT ${ADD_DIST_LIB_COMPONENT} + ) +endfunction(add_dist_module) + +function(add_dist_executable) + set(PARSE_OPTS SYSTEMEXEC) + set(PARSE_ARGS_SINGLE TARGET EXPORTNAME) + set(PARSE_ARGS_MULTI SOURCES HEADERS) + cmake_parse_arguments(ADD_DIST_BIN "${PARSE_OPTS}" "${PARSE_ARGS_SINGLE}" "${PARSE_ARGS_MULTI}" ${ARGN}) + if("${ADD_DIST_BIN_TARGET}" STREQUAL "") + message(FATAL_ERROR "add_dist_executable missing TARGET parameter") + endif() + if("${ADD_DIST_BIN_SOURCES}" STREQUAL "") + message(FATAL_ERROR "add_dist_executable missing SOURCES parameter") + endif() + + if(WIN32) + # Give the DLL version markings + set(WINRES_COMPANY_NAME_STR "OpenAFC") + set(WINRES_PRODUCT_NAME_STR ${PROJECT_NAME}) + set(WINRES_PRODUCT_VERSION_RES "${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0") + set(WINRES_PRODUCT_VERSION_STR "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}-${SVN_LAST_REVISION}") + set(WINRES_INTERNAL_NAME_STR ${ADD_DIST_BIN_TARGET}) + set(WINRES_ORIG_FILENAME "${ADD_DIST_BIN_TARGET}${CMAKE_EXECUTABLE_SUFFIX}") + set(WINRES_FILE_DESCRIPTION_STR "Runtime for ${ADD_DIST_BIN_TARGET}") + set(WINRES_FILE_VERSION_RES ${WINRES_PRODUCT_VERSION_RES}) + set(WINRES_FILE_VERSION_STR ${WINRES_PRODUCT_VERSION_STR}) + set(WINRES_COMMENTS_STR "") + configure_file("${CMAKE_SOURCE_DIR}/src/libinfo.rc.in" "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_BIN_TARGET}-libinfo.rc" @ONLY) + list(APPEND ADD_DIST_BIN_SOURCES "${CMAKE_CURRENT_BINARY_DIR}/${ADD_DIST_BIN_TARGET}-libinfo.rc") + endif(WIN32) + + add_executable(${ADD_DIST_BIN_TARGET} ${ADD_DIST_BIN_SOURCES}) + + if(TARGET Threads::Threads) + target_link_libraries(${ADD_DIST_BIN_TARGET} Threads::Threads) + endif() + + if(${ADD_DIST_BIN_SYSTEMEXEC}) + set(ADD_DIST_BIN_DEST ${PKG_INSTALL_SBINDIR}) + else() + set(ADD_DIST_BIN_DEST ${PKG_INSTALL_BINDIR}) + endif() + install( + TARGETS ${ADD_DIST_BIN_TARGET} + EXPORT ${ADD_DIST_BIN_EXPORTNAME} + DESTINATION ${ADD_DIST_BIN_DEST} + COMPONENT runtime + ) + if(WIN32) + get_filename_component(SOURCE_DIRNAME ${CMAKE_CURRENT_SOURCE_DIR} NAME) + # PDB for symbol mapping + install( + FILES "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${ADD_DIST_BIN_TARGET}.pdb" + DESTINATION ${PKG_INSTALL_DEBUGDIR} + COMPONENT debuginfo + ) + # Sources for debugger (directory name is target name) + install( + FILES ${ADD_DIST_BIN_HEADERS} ${ADD_DIST_BIN_SOURCES} + DESTINATION ${PKG_INSTALL_DEBUGDIR}/${SOURCE_DIRNAME} + COMPONENT debuginfo + ) + endif(WIN32) +endfunction(add_dist_executable) + +# +# Define a python library from sources. +# The named function arguments are: +# TARGET: The cmake target name to create. +# SETUP_TEMPLATE: A file to be used as template for setup.py. +# COMPONENT: The cmake "install" component to install the library as. +# SOURCES: All of the dependencies of the target, python files or otherwise. +# +# When processing the setup template, a variable is created for a windows-safe +# escaped file path to the source directory named +# CMAKE_CURRENT_SOURCE_DIR_ESCAPED. +# +function(add_dist_pythonlibrary) + set(PARSE_OPTS ) + set(PARSE_ARGS_SINGLE TARGET SETUP_TEMPLATE COMPONENT) + set(PARSE_ARGS_MULTI SOURCES) + cmake_parse_arguments(ADD_DIST_LIB "${PARSE_OPTS}" "${PARSE_ARGS_SINGLE}" "${PARSE_ARGS_MULTI}" ${ARGN}) + if("${ADD_DIST_LIB_TARGET}" STREQUAL "") + message(FATAL_ERROR "add_dist_pythonlibrary missing TARGET parameter") + endif() + if("${ADD_DIST_LIB_SETUP_TEMPLATE}" STREQUAL "") + message(FATAL_ERROR "add_dist_pythonlibrary missing SETUP_TEMPLATE parameter") + endif() + if("${ADD_DIST_LIB_SOURCES}" STREQUAL "") + message(FATAL_ERROR "add_dist_pythonlibrary missing SOURCES parameter") + endif() + + + find_program(PYTHON "python") + + # Need to escape the path for windows + if(WIN32) + string(REPLACE "/" "\\\\" CMAKE_CURRENT_SOURCE_DIR_ESCAPED ${CMAKE_CURRENT_SOURCE_DIR}) + else(WIN32) + set(CMAKE_CURRENT_SOURCE_DIR_ESCAPED ${CMAKE_CURRENT_SOURCE_DIR}) + endif(WIN32) + + # Assemble the actual setup.py input + configure_file(${ADD_DIST_LIB_SETUP_TEMPLATE} setup.py @ONLY) + + # Record an explicit sentinel file for the build + add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/timestamp" + COMMAND ${PYTHON} "${CMAKE_CURRENT_BINARY_DIR}/setup.py" build --quiet + COMMAND ${CMAKE_COMMAND} -E touch "${CMAKE_CURRENT_BINARY_DIR}/timestamp" + DEPENDS ${SOURCES} + ) + add_custom_target(${ADD_DIST_LIB_TARGET} ALL + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/timestamp" + ) + + # Use DESTDIR from actual install environment + set(ADD_DIST_LIB_INSTALL_CMD "${PYTHON} \"${CMAKE_CURRENT_BINARY_DIR}/setup.py\" install --root=\$DESTDIR/${CMAKE_INSTALL_PREFIX} --prefix=") + if(PKG_INSTALL_PYTHONSITEDIR) + set(ADD_DIST_LIB_INSTALL_CMD "${ADD_DIST_LIB_INSTALL_CMD} --install-lib=${PKG_INSTALL_PYTHONSITEDIR}") + endif() + install( + CODE "execute_process(COMMAND ${ADD_DIST_LIB_INSTALL_CMD})" + COMPONENT ${ADD_DIST_LIB_COMPONENT} + ) + +endfunction(add_dist_pythonlibrary) + +# Use qt "lrelease" to generate a translation binary from a source file. +# The named function arguments are: +# TARGET: The output QM file to create. +# SOURCE: The input TS file to read. +function(add_qt_translation) + set(PARSE_OPTS ) + set(PARSE_ARGS_SINGLE TARGET SOURCE) + set(PARSE_ARGS_MULTI ) + cmake_parse_arguments(ADD_TRANSLATION "${PARSE_OPTS}" "${PARSE_ARGS_SINGLE}" "${PARSE_ARGS_MULTI}" ${ARGN}) + if(NOT ADD_TRANSLATION_TARGET) + message(FATAL_ERROR "add_qt_translation missing TARGET parameter") + endif() + if(NOT ADD_TRANSLATION_SOURCE) + message(FATAL_ERROR "add_qt_translation missing SOURCE parameter") + endif() + + find_package(Qt5LinguistTools) + add_custom_command( + OUTPUT ${ADD_TRANSLATION_TARGET} + DEPENDS ${ADD_TRANSLATION_SOURCE} + COMMAND Qt5::lrelease -qm "${ADD_TRANSLATION_TARGET}" "${ADD_TRANSLATION_SOURCE}" + ) +endfunction(add_qt_translation) + +# Common run-time test behavior +set(GTEST_RUN_ARGS "--gtest_output=xml:test-detail.junit.xml") +function(add_gtest_executable TARGET_NAME ...) + add_executable(${ARGV}) + set_target_properties(${TARGET_NAME} PROPERTIES + COMPILE_FLAGS "-DGTEST_LINKED_AS_SHARED_LIBRARY=1" + ) + target_include_directories(${TARGET_NAME} PRIVATE ${GTEST_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${GTEST_BOTH_LIBRARIES}) + + find_package(Threads QUIET) + if(TARGET Threads::Threads) + target_link_libraries(${TARGET_NAME} Threads::Threads) + endif() + + add_test( + NAME ${TARGET_NAME} + COMMAND ${TARGET_NAME} ${GTEST_RUN_ARGS} + ) + set_property( + TEST ${TARGET_NAME} + APPEND PROPERTY + ENVIRONMENT + "TEST_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}" + "TEST_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}" + ) + if(UNIX) + set_property( + TEST ${TARGET_NAME} + APPEND PROPERTY + ENVIRONMENT + "XDG_DATA_DIRS=${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}:/usr/share" + ) + elseif(WIN32) + set_property( + TEST ${TARGET_NAME} + APPEND PROPERTY + ENVIRONMENT + "LOCALAPPDATA=${CMAKE_INSTALL_PREFIX}\\${CMAKE_INSTALL_DATADIR}" + ) + + set(PATH_WIN "${CMAKE_INSTALL_PREFIX}\\bin\;${GTEST_PATH}\;$ENV{PATH}") + # escape for ctest string processing + string(REPLACE ";" "\\;" PATH_WIN "${PATH_WIN}") + string(REPLACE "/" "\\" PATH_WIN "${PATH_WIN}") + set_property( + TEST ${TARGET_NAME} + APPEND PROPERTY + ENVIRONMENT + "PATH=${PATH_WIN}" + "QT_PLUGIN_PATH=${CMAKE_INSTALL_PREFIX}\\bin" + ) + endif() +endfunction(add_gtest_executable) + +function(add_nosetest_run TEST_NAME) + set(PARSE_OPTS ) + set(PARSE_ARGS_SINGLE PYTHONPATH) + set(PARSE_ARGS_MULTI ) + cmake_parse_arguments(ADD_NOSETEST "${PARSE_OPTS}" "${PARSE_ARGS_SINGLE}" "${PARSE_ARGS_MULTI}" ${ARGN}) + + find_program(NOSETEST_BIN "nosetests") + if(NOT NOSETEST_BIN) + message(FATAL_ERROR "Missing executable for 'nosetests'") + endif() + set(NOSETEST_RUN_ARGS "-v" "--with-xunit" "--xunit-file=test-detail.xunit.xml") + add_test( + NAME ${TEST_NAME} + COMMAND ${NOSETEST_BIN} ${CMAKE_CURRENT_SOURCE_DIR} ${NOSETEST_RUN_ARGS} + ) + + set_property( + TEST ${TEST_NAME} + APPEND PROPERTY + ENVIRONMENT + "PYTHONPATH=${ADD_NOSETEST_PYTHONPATH}:${CMAKE_INSTALL_PREFIX}/${PKG_INSTALL_PYTHONSITEDIR}:$ENV{PYTHONPATH}" + "TEST_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}" + "TEST_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}" + ) + if(UNIX) + set_property( + TEST ${TEST_NAME} + APPEND PROPERTY + ENVIRONMENT + "XDG_DATA_DIRS=${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}:/usr/share" + ) + elseif(WIN32) + set_property( + TEST ${TEST_NAME} + APPEND PROPERTY + ENVIRONMENT + "LOCALAPPDATA=${CMAKE_INSTALL_PREFIX}\\${CMAKE_INSTALL_DATADIR}" + ) + endif() +endfunction(add_nosetest_run) diff --git a/src/coalition_ulsprocessor/conanenv/conanfile.py b/src/coalition_ulsprocessor/conanenv/conanfile.py new file mode 100644 index 0000000..d45083c --- /dev/null +++ b/src/coalition_ulsprocessor/conanenv/conanfile.py @@ -0,0 +1,125 @@ +# File based on boilerplate at https://github.com/bincrafters/conan-templates/blob/master/conanfile.py +# When 'install'ed this conanfile produces a 'cpodeps-runtime.zip' file in +# the current directory + +import contextlib +import os.path +import shutil +import subprocess +import tempfile +import zipfile +from conans import ConanFile, CMake +from conans.errors import ConanException +from conans.client import tools + + +def path_append(paths, suffix): + return tuple( + os.path.join(path, suffix) + for path in paths + ) + + +class UlsDepsConan(ConanFile): + settings = "os", "compiler", "build_type", "arch" + options = { + 'import_runtime': [True, False], + 'import_symstore': [True, False], + } + default_options = { + 'import_runtime': True, + 'import_symstore': False, + } + generators = "cmake" + + requires = ( + 'qt/5.9.0@cpo/stable', + ) + + def _copy_to_zipfile(self, relroot, outzip_name): + ''' Recursively copy a file tree into a zip file with relative paths. + ''' + with zipfile.ZipFile(outzip_name, 'w') as zip: + for (rootpath, dirs, files) in os.walk(relroot): + for filename in files: + filepath = os.path.join(rootpath, filename) + relpath = os.path.relpath(filepath, relroot) + zip.write(filepath, relpath) + + def _import_runtime(self, tmpdir): + ''' Copy runtime dependencies. ''' + outzip_name = 'uls-deps-runtime.zip' + + # Do nothing if output zip exists and is newer than this spec + if os.path.exists(outzip_name): + own_mtime = os.path.getmtime(os.path.realpath(__file__)) + outzip_mtime = os.path.getmtime(outzip_name) + if own_mtime < outzip_mtime: + self.output.info( + 'Skipping imports because file newer file exists: {0}'.format(outzip_name)) + return + + bindir = os.path.join(tmpdir, 'bin') + if not os.path.exists(bindir): + os.makedirs(bindir) + # MSVC runtime redistributable DLLs + msvcrt_libs = ('msvcrt', 'vcruntime140', + 'vcruntime140d', 'msvcp140', 'msvcp140d') + for libname in msvcrt_libs: + filename = '{0}.dll'.format(libname) + shutil.copyfile( + os.path.join(os.environ['SYSTEMROOT'], 'system32', filename), + os.path.join(bindir, filename) + ) + # All-deps DLLs + self.copy("Qt5Core.dll", dst=bindir, src="bin") + self.copy("zlib*.dll", dst=bindir, src="bin") + self.copy("icu*.dll", dst=bindir, src="bin") + + # Write this zip file last + self._copy_to_zipfile(tmpdir, outzip_name) + + def _import_symstore(self, tmpdir): + ''' Extract runtime debugging info into a symbol store. ''' + outzip_name = 'uls-script-symstore.zip' + + # Do nothing if output zip exists and is newer than this spec + if os.path.exists(outzip_name): + own_mtime = os.path.getmtime(os.path.realpath(__file__)) + outzip_mtime = os.path.getmtime(outzip_name) + if own_mtime < outzip_mtime: + self.output.info( + 'Skipping imports because file newer file exists: {0}'.format(outzip_name)) + return + + # Normalize all files into a single symbol store + for (name, cppinfo) in self.deps_cpp_info.dependencies: + self.output.info('Building symbol store for "{0}"'.format(name)) + subprocess.check_call([ + 'symstore', 'add', '/r', + '/compress', + '/f', cppinfo.rootpath, + '/s', tmpdir, + '/t', name, + '/v', cppinfo.version, + ]) + + # Write this zip file last + self._copy_to_zipfile(tmpdir, outzip_name) + + def imports(self): + ''' Copy only runtime-required DLLs and resources into local zip file. + A bit of an abuse of the conan imports system but ensures isolation from testroot. + + Conan uses imported files to build a manifest (with checksum) after + this function exits, so the "importroot" cannot disappear after zipping. + ''' + importdir = os.path.abspath('importroot') + if os.path.exists(importdir): + shutil.rmtree(importdir) + os.makedirs(importdir) + + if self.options.import_runtime: + self._import_runtime(os.path.join(importdir, 'runtime')) + if self.options.import_symstore: + self._import_symstore(os.path.join(importdir, 'symstore')) diff --git a/src/coalition_ulsprocessor/flowchart.txt b/src/coalition_ulsprocessor/flowchart.txt new file mode 100644 index 0000000..075ef5e --- /dev/null +++ b/src/coalition_ulsprocessor/flowchart.txt @@ -0,0 +1,33 @@ +graph Parsing + A[Parsing] -->|Follows https://www.fcc.gov/sites/default/files/public_access_database_definitions_v4.pdf| B[/Frequency/] + B --> C([Call Sign, Transmit Location Number, Transmit Antenna Number]) + C -->|All Found| D[Microwave Path - PA] + C --> E[No Match] + D --> F([Call Sign, Location Number]) + F -->|Matches| G[Transmitter Location - LO] + F --> H[No Match] + G --> I([Call Sign, Location Number, Antenna Number]) + I -->|Matches| J[Transmitter Antenna - AN] + I --> K[No Match] + J --> L([Call Sign, Location Number]) + L -->|Matches| M[Receiver Location - LO] + L --> N[No Match] + M --> O([Call Sign, Path Number, Segment Number == 1]) + O -->|Matches|P[Receiver Antenna - AN ] + O --> Q[No Match] + P --> R([Call Sign, Path Number, Segment Number == 1]) + R --> S[Microwave Segment - SG ] + S --> T([Location Number, Antenna Number, Frequency Number]) + T -->|Matches| U[Emissions - EM] + T --> V[No Match] + U --> W([Call Sign]) + W -->|Matches| Y[Application/License Header - HD] + W --> Z[No Match] + Y --> AA([Call Sign, Status == A]) + AA -->|Matches| AB[Entity - EN] + AA --> AC[No Match] + AB --> AD([Call Sign]) + AD --> AE[Control Point - CP] + AE --> AF[/Emission - EM/] + AF --> AG([low frequency < 7125, high frequency > 5925]) + AG --> AH((Write to CSV)) diff --git a/src/coalition_ulsprocessor/src/CMakeLists.txt b/src/coalition_ulsprocessor/src/CMakeLists.txt new file mode 100644 index 0000000..ead4238 --- /dev/null +++ b/src/coalition_ulsprocessor/src/CMakeLists.txt @@ -0,0 +1,19 @@ +# Allow includes relative to this "src" directory +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) +# Allow includes for generated headers +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(BUILD_SHARED_LIBS ON) +set(CMAKE_AUTOMOC ON) + +# Helper functions for all target types +include(srcfunctions) +set(TARGET_LIBS "") +set(TARGET_BINS "") +set(TARGET_SBINS "") + +# Make visible to project scope +set(TARGET_LIBS "${TARGET_LIBS}" PARENT_SCOPE) +add_subdirectory(uls-script) \ No newline at end of file diff --git a/src/coalition_ulsprocessor/src/libinfo.rc.in b/src/coalition_ulsprocessor/src/libinfo.rc.in new file mode 100644 index 0000000..b59fe12 --- /dev/null +++ b/src/coalition_ulsprocessor/src/libinfo.rc.in @@ -0,0 +1,46 @@ +#include "winres.h" + +#if !defined(VS_VERSION_INFO) +#define VS_VERSION_INFO 1 +#endif + +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +VS_VERSION_INFO VERSIONINFO + PRODUCTVERSION @WINRES_PRODUCT_VERSION_RES@ + FILEVERSION @WINRES_FILE_VERSION_RES@ + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#if !defined(NDEBUG) + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0 +#endif + FILEOS VOS_NT_WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" /* LANG_ENGLISH/SUBLANG_ENGLISH_US */ + BEGIN + VALUE "CompanyName", "@WINRES_COMPANY_NAME_STR@\0" + VALUE "ProductName", "@WINRES_PRODUCT_NAME_STR@\0" + VALUE "ProductVersion", "@WINRES_PRODUCT_VERSION_STR@\0" + VALUE "InternalName", "@WINRES_INTERNAL_NAME_STR@\0" + VALUE "OriginalFilename", "@WINRES_ORIG_FILENAME@\0" + VALUE "FileDescription", "@WINRES_FILE_DESCRIPTION_STR@\0" + VALUE "FileVersion", "@WINRES_FILE_VERSION_STR@\0" + VALUE "Comments", "@WINRES_COMMENTS_STR@\0" + END + END + + BLOCK "VarFileInfo" + BEGIN + /* The following line should only be modified for localized versions. */ + /* It consists of any number of WORD,WORD pairs, with each pair */ + /* describing a language,codepage combination supported by the file. */ + /* For example, a file might have values "0x409,1252" indicating that it */ + /* supports English language (0x409) in the Windows ANSI codepage (1252). */ + VALUE "Translation", 0x409, 1252 + END +END diff --git a/src/coalition_ulsprocessor/src/uls-script/.gitignore b/src/coalition_ulsprocessor/src/uls-script/.gitignore new file mode 100644 index 0000000..a4eb05f --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/.gitignore @@ -0,0 +1,9 @@ +*.o +*.pro +.qmake.stash +uls-script +warning_uls.txt +Makefile +anomalous_uls.csv +cmd.txt +mfk_* diff --git a/src/coalition_ulsprocessor/src/uls-script/AntennaModelMap.cpp b/src/coalition_ulsprocessor/src/uls-script/AntennaModelMap.cpp new file mode 100644 index 0000000..2f0668b --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/AntennaModelMap.cpp @@ -0,0 +1,886 @@ +/******************************************************************************************/ +/**** FILE: AntennaModelMap.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include "AntennaModelMap.h" + +/******************************************************************************************/ +/**** CONSTRUCTOR: AntennaModelClass::AntennaModelClass ****/ +/******************************************************************************************/ +AntennaModelClass::AntennaModelClass(std::string nameVal) : name(nameVal) +{ + type = AntennaModel::UnknownType; + category = AntennaModel::UnknownCategory; + diameterM = -1.0; + midbandGain = std::numeric_limits::quiet_NaN(); + reflectorWidthM = -1.0; + reflectorHeightM = -1.0; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: AntennaPrefixClass::AntennaPrefixClass ****/ +/******************************************************************************************/ +AntennaPrefixClass::AntennaPrefixClass(std::string prefixVal) : prefix(prefixVal) +{ + type = AntennaModel::UnknownType; + category = AntennaModel::UnknownCategory; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** STATIC FUNCTION: AntennaModel::categoryStr() ****/ +/******************************************************************************************/ +std::string AntennaModel::categoryStr(AntennaModel::CategoryEnum categoryVal) +{ + std::string str; + + switch (categoryVal) { + case B1Category: + str = "B1"; + break; + case HPCategory: + str = "HP"; + break; + case OtherCategory: + str = "OTHER"; + break; + case UnknownCategory: + str = "UNKNOWN"; + break; + default: + CORE_DUMP; + break; + } + + return (str); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** STATIC FUNCTION: AntennaModel::typeStr() ****/ +/******************************************************************************************/ +std::string AntennaModel::typeStr(AntennaModel::TypeEnum typeVal) +{ + std::string str; + + switch (typeVal) { + case AntennaType: + str = "Ant"; + break; + case ReflectorType: + str = "Ref"; + break; + case UnknownType: + str = "UNKNOWN"; + break; + default: + CORE_DUMP; + break; + } + + return (str); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: AntennaModelMapClass::AntennaModelMapClass ****/ +/******************************************************************************************/ +AntennaModelMapClass::AntennaModelMapClass(std::string antModelListFile, + std::string antPrefixListFile, + std::string antModelMapFile) +{ + readModelList(antModelListFile); + readPrefixList(antPrefixListFile); + readModelMap(antModelMapFile); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AntennaModelMapClass::readModelList() ****/ +/******************************************************************************************/ +void AntennaModelMapClass::readModelList(const std::string filename) +{ + int linenum, fIdx; + std::string line, strval; + char *chptr; + FILE *fp = (FILE *)NULL; + std::string str; + std::string reasonIgnored; + std::ostringstream errStr; + + int modelNameFieldIdx = -1; + int typeFieldIdx = -1; + int categoryFieldIdx = -1; + int diameterMFieldIdx = -1; + int midbandGainFieldIdx = -1; + int reflectorWidthMFieldIdx = -1; + int reflectorHeightMFieldIdx = -1; + + std::vector fieldIdxList; + std::vector fieldLabelList; + fieldIdxList.push_back(&modelNameFieldIdx); + fieldLabelList.push_back("Ant Model"); + fieldIdxList.push_back(&typeFieldIdx); + fieldLabelList.push_back("Type"); + fieldIdxList.push_back(&categoryFieldIdx); + fieldLabelList.push_back("Category"); + fieldIdxList.push_back(&diameterMFieldIdx); + fieldLabelList.push_back("Diameter (m)"); + fieldIdxList.push_back(&midbandGainFieldIdx); + fieldLabelList.push_back("Midband Gain (dBi)"); + fieldIdxList.push_back(&reflectorWidthMFieldIdx); + fieldLabelList.push_back("Reflector Width (m)"); + fieldIdxList.push_back(&reflectorHeightMFieldIdx); + fieldLabelList.push_back("Reflector Height (m)"); + + std::string name; + AntennaModel::CategoryEnum category; + AntennaModel::TypeEnum type; + double diameterM; + double midbandGain; + double reflectorWidthM = -1; + double reflectorHeightM = -1; + + int fieldIdx; + + if (filename.empty()) { + throw std::runtime_error("ERROR: No Antenna Model List File specified"); + } + + if (!(fp = fopen(filename.c_str(), "rb"))) { + str = std::string("ERROR: Unable to open Antenna Model List File \"") + filename + + std::string("\"\n"); + throw std::runtime_error(str); + } + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + AntennaModelClass *antennaModel; + + linenum = 0; + bool foundLabelLine = false; + while (fgetline(fp, line, false)) { + linenum++; + std::vector fieldList = splitCSV(line); + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if ((lineType == unknownLineType) && (!foundLabelLine)) { + lineType = labelLineType; + foundLabelLine = 1; + } + if ((lineType == unknownLineType) && (foundLabelLine)) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + bool found; + std::string field; + switch (lineType) { + case labelLineType: + for (fieldIdx = 0; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + + // std::cout << "FIELD: \"" << field << "\"" << std::endl; + + found = false; + for (fIdx = 0; + (fIdx < (int)fieldLabelList.size()) && (!found); + fIdx++) { + if (field == fieldLabelList.at(fIdx)) { + *fieldIdxList.at(fIdx) = fieldIdx; + found = true; + } + } + } + + for (fIdx = 0; fIdx < (int)fieldIdxList.size(); fIdx++) { + if (*fieldIdxList.at(fIdx) == -1) { + errStr << "ERROR: Invalid Antenna Model List file " + "\"" + << filename << "\" label line missing \"" + << fieldLabelList.at(fIdx) << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + } + + break; + case dataLineType: + /**************************************************************************/ + /* modelName */ + /**************************************************************************/ + strval = fieldList.at(modelNameFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Antenna Model List file \"" << filename + << "\" line " << linenum << " missing model name" + << std::endl; + throw std::runtime_error(errStr.str()); + } + + name = strval; + /**************************************************************************/ + + /**************************************************************************/ + /* type */ + /**************************************************************************/ + strval = fieldList.at(typeFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Antenna Model List file \"" << filename + << "\" line " << linenum << " missing type" + << std::endl; + throw std::runtime_error(errStr.str()); + } + + if ((strval == "Ant") || (strval == "Antenna")) { + type = AntennaModel::AntennaType; + } else if ((strval == "Ref") || (strval == "Reflector")) { + type = AntennaModel::ReflectorType; + } else { + errStr << "ERROR: Antenna Model List file \"" << filename + << "\" line " << linenum + << " invalid type: " << strval << std::endl; + throw std::runtime_error(errStr.str()); + } + /**************************************************************************/ + + /**************************************************************************/ + /* category */ + /**************************************************************************/ + strval = fieldList.at(categoryFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Antenna Model List file \"" << filename + << "\" line " << linenum << " missing category" + << std::endl; + throw std::runtime_error(errStr.str()); + } + + if (strval == "HP") { + category = AntennaModel::HPCategory; + } else if (strval == "B1") { + category = AntennaModel::B1Category; + } else if ((strval == "OTHER") || (strval == "Other")) { + category = AntennaModel::OtherCategory; + } else { + errStr << "ERROR: Antenna Model List file \"" << filename + << "\" line " << linenum + << " invalid category: " << strval << std::endl; + throw std::runtime_error(errStr.str()); + } + /**************************************************************************/ + + /**************************************************************************/ + /* diameter */ + /**************************************************************************/ + strval = fieldList.at(diameterMFieldIdx); + if (strval.empty()) { + diameterM = -1.0; // Use -1 for unknown + } else { + diameterM = std::strtod(strval.c_str(), &chptr); + if (diameterM <= 0.0) { + errStr << "ERROR: Antenna Model List file \"" + << filename << "\" line " << linenum + << " invalid diameter: \"" << strval << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + // Use meters in input file, no conversion here + // diameter *= 12*2.54*0.01; // convert ft to meters + } + /**************************************************************************/ + + /**************************************************************************/ + /* midband gain */ + /**************************************************************************/ + strval = fieldList.at(midbandGainFieldIdx); + if (strval.empty()) { + midbandGain = std::numeric_limits::quiet_NaN(); + } else { + midbandGain = std::strtod(strval.c_str(), &chptr); + } + /**************************************************************************/ + + /**************************************************************************/ + /* reflectorWidth */ + /**************************************************************************/ + strval = fieldList.at(reflectorWidthMFieldIdx); + if (strval.empty()) { + reflectorWidthM = -1.0; // Use -1 for unknown + } else { + reflectorWidthM = std::strtod(strval.c_str(), &chptr); + if (reflectorWidthM <= 0.0) { + errStr << "ERROR: Antenna Model List file \"" + << filename << "\" line " << linenum + << " invalid reflector width: \"" << strval + << "\"" << std::endl; + throw std::runtime_error(errStr.str()); + } + } + /**************************************************************************/ + + /**************************************************************************/ + /* reflectorHeight */ + /**************************************************************************/ + strval = fieldList.at(reflectorHeightMFieldIdx); + if (strval.empty()) { + reflectorHeightM = -1.0; // Use -1 for unknown + } else { + reflectorHeightM = std::strtod(strval.c_str(), &chptr); + if (reflectorHeightM <= 0.0) { + errStr << "ERROR: Antenna Model List file \"" + << filename << "\" line " << linenum + << " invalid reflector height: \"" << strval + << "\"" << std::endl; + throw std::runtime_error(errStr.str()); + } + } + /**************************************************************************/ + + antennaModel = new AntennaModelClass(name); + antennaModel->setCategory(category); + antennaModel->setType(type); + antennaModel->setDiameterM(diameterM); + antennaModel->setMidbandGain(midbandGain); + antennaModel->setReflectorWidthM(reflectorWidthM); + antennaModel->setReflectorHeightM(reflectorHeightM); + + antennaModelList.push_back(antennaModel); + + break; + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + CORE_DUMP; + break; + } + } + + if (fp) { + fclose(fp); + } + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AntennaModelMapClass::readPrefixList() ****/ +/******************************************************************************************/ +void AntennaModelMapClass::readPrefixList(const std::string filename) +{ + int linenum, fIdx; + std::string line, strval; + FILE *fp = (FILE *)NULL; + std::string str; + std::string reasonIgnored; + std::ostringstream errStr; + + int prefixFieldIdx = -1; + int typeFieldIdx = -1; + int categoryFieldIdx = -1; + + std::vector fieldIdxList; + std::vector fieldLabelList; + fieldIdxList.push_back(&prefixFieldIdx); + fieldLabelList.push_back("Prefix"); + fieldIdxList.push_back(&typeFieldIdx); + fieldLabelList.push_back("Type"); + fieldIdxList.push_back(&categoryFieldIdx); + fieldLabelList.push_back("Category"); + + std::string prefix; + AntennaModel::CategoryEnum category; + AntennaModel::TypeEnum type; + + int fieldIdx; + + if (filename.empty()) { + throw std::runtime_error("ERROR: No Antenna Prefix File specified"); + } + + if (!(fp = fopen(filename.c_str(), "rb"))) { + str = std::string("ERROR: Unable to open Antenna Prefix File \"") + filename + + std::string("\"\n"); + throw std::runtime_error(str); + } + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + AntennaPrefixClass *antennaPrefix; + + linenum = 0; + bool foundLabelLine = false; + while (fgetline(fp, line, false)) { + linenum++; + std::vector fieldList = splitCSV(line); + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if ((lineType == unknownLineType) && (!foundLabelLine)) { + lineType = labelLineType; + foundLabelLine = 1; + } + if ((lineType == unknownLineType) && (foundLabelLine)) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + bool found; + std::string field; + switch (lineType) { + case labelLineType: + for (fieldIdx = 0; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + + // std::cout << "FIELD: \"" << field << "\"" << std::endl; + + found = false; + for (fIdx = 0; + (fIdx < (int)fieldLabelList.size()) && (!found); + fIdx++) { + if (field == fieldLabelList.at(fIdx)) { + *fieldIdxList.at(fIdx) = fieldIdx; + found = true; + } + } + } + + for (fIdx = 0; fIdx < (int)fieldIdxList.size(); fIdx++) { + if (*fieldIdxList.at(fIdx) == -1) { + errStr << "ERROR: Invalid Antenna Model List file " + "\"" + << filename << "\" label line missing \"" + << fieldLabelList.at(fIdx) << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + } + + break; + case dataLineType: + /**************************************************************************/ + /* prefix */ + /**************************************************************************/ + strval = fieldList.at(prefixFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Antenna Prefix file \"" << filename + << "\" line " << linenum << " missing prefix" + << std::endl; + throw std::runtime_error(errStr.str()); + } + + prefix = strval; + /**************************************************************************/ + + /**************************************************************************/ + /* type */ + /**************************************************************************/ + strval = fieldList.at(typeFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Antenna Prefix file \"" << filename + << "\" line " << linenum << " missing type" + << std::endl; + throw std::runtime_error(errStr.str()); + } + + if ((strval == "Ant") || (strval == "Antenna")) { + type = AntennaModel::AntennaType; + } else if ((strval == "Ref") || (strval == "Reflector")) { + type = AntennaModel::ReflectorType; + } else { + errStr << "ERROR: Antenna Prefix file \"" << filename + << "\" line " << linenum + << " invalid type: " << strval << std::endl; + throw std::runtime_error(errStr.str()); + } + /**************************************************************************/ + + /**************************************************************************/ + /* category */ + /**************************************************************************/ + strval = fieldList.at(categoryFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Antenna Prefix file \"" << filename + << "\" line " << linenum << " missing category" + << std::endl; + throw std::runtime_error(errStr.str()); + } + + if (strval == "HP") { + category = AntennaModel::HPCategory; + } else if (strval == "B1") { + category = AntennaModel::B1Category; + } else if ((strval == "OTHER") || (strval == "Other")) { + category = AntennaModel::OtherCategory; + } else { + errStr << "ERROR: Antenna Prefix file \"" << filename + << "\" line " << linenum + << " invalid category: " << strval << std::endl; + throw std::runtime_error(errStr.str()); + } + /**************************************************************************/ + + antennaPrefix = new AntennaPrefixClass(prefix); + antennaPrefix->setCategory(category); + antennaPrefix->setType(type); + + antennaPrefixList.push_back(antennaPrefix); + + break; + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + CORE_DUMP; + break; + } + } + + if (fp) { + fclose(fp); + } + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: AntennaModelMapClass::readModelMap() ****/ +/******************************************************************************************/ +void AntennaModelMapClass::readModelMap(const std::string filename) +{ + int linenum, fIdx; + std::string line, strval; + FILE *fp = (FILE *)NULL; + std::string str; + std::string reasonIgnored; + std::ostringstream errStr; + + int regexFieldIdx = -1; + int modelNameFieldIdx = -1; + + std::vector fieldIdxList; + std::vector fieldLabelList; + fieldIdxList.push_back(®exFieldIdx); + fieldLabelList.push_back("regex"); + fieldIdxList.push_back(&modelNameFieldIdx); + fieldLabelList.push_back("Ant Model"); + + int i; + int antIdx; + std::string name; + std::string regexStr; + std::regex *regExpr; + + int fieldIdx; + + if (filename.empty()) { + throw std::runtime_error("ERROR: No Antenna Model List File specified"); + } + + if (!(fp = fopen(filename.c_str(), "rb"))) { + str = std::string("ERROR: Unable to open Antenna Model List File \"") + filename + + std::string("\"\n"); + throw std::runtime_error(str); + } + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + linenum = 0; + bool foundLabelLine = false; + while (fgetline(fp, line, false)) { + linenum++; + std::vector fieldList = splitCSV(line); + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if ((lineType == unknownLineType) && (!foundLabelLine)) { + lineType = labelLineType; + foundLabelLine = 1; + } + if ((lineType == unknownLineType) && (foundLabelLine)) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + bool found; + std::string field; + switch (lineType) { + case labelLineType: + for (fieldIdx = 0; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + + // std::cout << "FIELD: \"" << field << "\"" << std::endl; + + found = false; + for (fIdx = 0; + (fIdx < (int)fieldLabelList.size()) && (!found); + fIdx++) { + if (field == fieldLabelList.at(fIdx)) { + *fieldIdxList.at(fIdx) = fieldIdx; + found = true; + } + } + } + + for (fIdx = 0; fIdx < (int)fieldIdxList.size(); fIdx++) { + if (*fieldIdxList.at(fIdx) == -1) { + errStr << "ERROR: Invalid Antenna Model Map file \"" + << filename << "\" label line missing \"" + << fieldLabelList.at(fIdx) << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + } + + break; + case dataLineType: + /**************************************************************************/ + /* regex */ + /**************************************************************************/ + strval = fieldList.at(regexFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Antenna Model Map file \"" << filename + << "\" line " << linenum << " missing regex" + << std::endl; + throw std::runtime_error(errStr.str()); + } + + regexStr = strval; + /**************************************************************************/ + + /**************************************************************************/ + /* modelName */ + /**************************************************************************/ + strval = fieldList.at(modelNameFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Antenna Model Map file \"" << filename + << "\" line " << linenum << " missing model name" + << std::endl; + throw std::runtime_error(errStr.str()); + } + + name = strval; + /**************************************************************************/ + + regExpr = new std::regex(regexStr, std::regex_constants::icase); + + antIdx = -1; + found = false; + for (i = 0; (i < (int)antennaModelList.size()) && (!found); ++i) { + if (antennaModelList[i]->name == name) { + found = true; + antIdx = i; + } + } + if (!found) { + errStr << "ERROR: Antenna Model Map file \"" << filename + << "\" line " << linenum + << " invalid model name: " << name << std::endl; + throw std::runtime_error(errStr.str()); + } + + regexList.push_back(regExpr); + antIdxList.push_back(antIdx); + + break; + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + CORE_DUMP; + break; + } + } + + if (fp) { + fclose(fp); + } + + return; +} +/******************************************************************************************/ + +inline bool isInvalidModelNameChar(char c) +{ + // Valid characters are 'A' - 'Z' and '0' - '9' + bool isLetter = (c >= 'A') && (c <= 'Z'); + bool isNum = (c >= '0') && (c <= '9'); + bool valid = isLetter || isNum; + return (!valid); +} + +/******************************************************************************************/ +/**** FUNCTION: AntennaModelMapClass::find() ****/ +/******************************************************************************************/ +AntennaModelClass *AntennaModelMapClass::find(std::string antPfx, + std::string modelName, + AntennaModel::CategoryEnum &category, + AntennaModel::CategoryEnum modelNameBlankCategory) +{ + bool found = false; + int antIdx; + int i; + + category = AntennaModel::UnknownCategory; + + for (i = 0; (i < (int)regexList.size()) && (!found); ++i) { + if (regex_match(modelName, *regexList[i])) { + found = true; + antIdx = antIdxList[i]; + } + } + + AntennaModelClass *antennaModel = (AntennaModelClass *)NULL; + if (found) { + antennaModel = antennaModelList[antIdx]; + } else { + /**********************************************************************************/ + /* Convert ModelName to uppercase */ + /**********************************************************************************/ + std::transform(modelName.begin(), modelName.end(), modelName.begin(), ::toupper); + /**********************************************************************************/ + + /**********************************************************************************/ + /* Remove non-alhpanumeric characters */ + /**********************************************************************************/ + modelName.erase(std::remove_if(modelName.begin(), + modelName.end(), + isInvalidModelNameChar), + modelName.end()); + /**********************************************************************************/ + + /**********************************************************************************/ + /* Prepend model name prefix */ + /**********************************************************************************/ + modelName = antPfx + modelName; + /**********************************************************************************/ + + /**********************************************************************************/ + /* Match if antennaModelList contains a model that is: */ + /* If forceModelNameEqualFlag is false */ + /* * B1 or HP model and is a prefix of modelName */ + /* * not B1 or HP and model equals modelName */ + /* If forceModelNameEqualFlag is true */ + /* * model equals modelName */ + /**********************************************************************************/ + bool forceModelNameEqualFlag = true; + for (i = 0; (i < (int)antennaModelList.size()) && (!found); ++i) { + AntennaModelClass *m = antennaModelList[i]; + bool prefixFlag; + if ((!forceModelNameEqualFlag) && (m->type == AntennaModel::AntennaType) && + ((m->category == AntennaModel::HPCategory) || + (m->category == AntennaModel::B1Category))) { + prefixFlag = true; + } else { + prefixFlag = false; + } + + if (((prefixFlag) && + (modelName.compare(0, m->name.size(), m->name) == 0)) || + (m->name == modelName)) { + found = true; + antennaModel = m; + } + } + /**********************************************************************************/ + + /**********************************************************************************/ + /* If still not found, determine category by checking antennaPrefixList */ + /**********************************************************************************/ + for (i = 0; (i < (int)antennaPrefixList.size()) && (!found); ++i) { + AntennaPrefixClass *pfx = antennaPrefixList[i]; + if ((modelName.compare(0, pfx->prefix.size(), pfx->prefix) == 0)) { + category = pfx->category; + found = true; + } + } + /**********************************************************************************/ + + /**********************************************************************************/ + /* If still not found, set category to B1 if modelName is blank */ + /**********************************************************************************/ + if (!found) { + if (modelName == antPfx) { + category = modelNameBlankCategory; + } + } + /**********************************************************************************/ + } + + return (antennaModel); +} +/******************************************************************************************/ diff --git a/src/coalition_ulsprocessor/src/uls-script/AntennaModelMap.h b/src/coalition_ulsprocessor/src/uls-script/AntennaModelMap.h new file mode 100644 index 0000000..dc46046 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/AntennaModelMap.h @@ -0,0 +1,99 @@ +#ifndef ANTENNA_MODEL_MAP_H +#define ANTENNA_MODEL_MAP_H + +#include +#include +#include +#include "global_fn.h" + +namespace AntennaModel +{ +enum CategoryEnum { HPCategory, B1Category, OtherCategory, UnknownCategory }; + +enum TypeEnum { AntennaType, ReflectorType, UnknownType }; + +std::string categoryStr(AntennaModel::CategoryEnum categoryVal); +std::string typeStr(AntennaModel::TypeEnum typeVal); +} + +class AntennaModelClass +{ + public: + AntennaModelClass(std::string nameVal); + + void setType(AntennaModel::TypeEnum typeVal) + { + type = typeVal; + } + void setCategory(AntennaModel::CategoryEnum categoryVal) + { + category = categoryVal; + } + void setDiameterM(double diameterMVal) + { + diameterM = diameterMVal; + } + void setMidbandGain(double midbandGainVal) + { + midbandGain = midbandGainVal; + } + void setReflectorWidthM(double reflectorWidthMVal) + { + reflectorWidthM = reflectorWidthMVal; + } + void setReflectorHeightM(double reflectorHeightMVal) + { + reflectorHeightM = reflectorHeightMVal; + } + + std::string name; + AntennaModel::TypeEnum type; + AntennaModel::CategoryEnum category; + double diameterM; // Antenna diameter in meters + double midbandGain; // Antenna midband gain (dB) + double reflectorWidthM; // Reflector Width (m) + double reflectorHeightM; // AReflector Height (m) +}; + +class AntennaPrefixClass +{ + public: + AntennaPrefixClass(std::string prefixVal); + + void setType(AntennaModel::TypeEnum typeVal) + { + type = typeVal; + } + void setCategory(AntennaModel::CategoryEnum categoryVal) + { + category = categoryVal; + } + + std::string prefix; + AntennaModel::TypeEnum type; + AntennaModel::CategoryEnum category; +}; + +class AntennaModelMapClass +{ + public: + AntennaModelMapClass(const std::string antListFile, + const std::string antPrefixFile, + const std::string antMapFile); + AntennaModelClass *find(std::string antPfx, + std::string modelName, + AntennaModel::CategoryEnum &category, + AntennaModel::CategoryEnum modelNameBlankCategory); + + private: + void readModelList(const std::string filename); + void readPrefixList(const std::string filename); + void readModelMap(const std::string filename); + + std::vector antennaModelList; + std::vector antennaPrefixList; + std::vector regexList; + std::vector antIdxList; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/CMakeLists.txt b/src/coalition_ulsprocessor/src/uls-script/CMakeLists.txt new file mode 100644 index 0000000..25d9984 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/CMakeLists.txt @@ -0,0 +1,8 @@ +# All source files to same target +set(TGT_NAME "uls-script") + +file(GLOB ALL_CPP "*.[ch]pp") +file(GLOB ALL_HEADER "*.h") +add_dist_executable(TARGET ${TGT_NAME} SOURCES ${ALL_CPP} ${ALL_HEADER}) + +target_link_libraries(${TGT_NAME} PUBLIC Qt5::Core) \ No newline at end of file diff --git a/src/coalition_ulsprocessor/src/uls-script/CsvWriter.cpp b/src/coalition_ulsprocessor/src/uls-script/CsvWriter.cpp new file mode 100644 index 0000000..f15bbcb --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/CsvWriter.cpp @@ -0,0 +1,123 @@ + +#include +#include +#include "CsvWriter.h" + +const CsvWriter::EndRow CsvWriter::endr = CsvWriter::EndRow(); + +CsvWriter::CsvWriter(const QString &fileName) : _ownFile(true), _colI(0) +{ + _defaultOpts(); + + // Destructor is not called if constructor throws + QScopedPointer file(new QFile(fileName)); + if (!file->open(QIODevice::WriteOnly)) { + throw FileError(QString("Failed to open \"%1\" for writing: %2") + .arg(fileName, file->errorString())); + } + _str.setDevice(file.take()); +} + +CsvWriter::CsvWriter(QIODevice &device) : _ownFile(false), _colI(0) +{ + _defaultOpts(); + + if (!device.isOpen()) { + if (!device.open(QIODevice::WriteOnly)) { + throw FileError(QString("Failed to open for writing: %1") + .arg(device.errorString())); + } + } + _str.setDevice(&device); +} + +CsvWriter::~CsvWriter() +{ + if (_colI > 0) { + writeEndRow(); + } + if (_ownFile) { + delete _str.device(); + } +} + +void CsvWriter::setCharacters(const QChar &separator, const QChar "e) +{ + if (separator == quote) { + throw std::logic_error("Cannot use same character for quote and separator"); + } + _quotedChars.remove(_sep); + _quotedChars.remove(_quote); + _sep = separator; + _quote = quote; + _quotedChars.insert(_sep); + _quotedChars.insert(_quote); +} + +void CsvWriter::writeRow(const QStringList &elements) +{ + foreach(const QString &elem, elements) + { + writeRecord(elem); + } + writeEndRow(); +} + +void CsvWriter::writeRecord(const QString &rec) +{ + // Escape the text if necessary + QString text = rec; + + bool doQuote = false; + if (_quotedCols.contains(_colI)) { + doQuote = true; + } else if (!_quotedExpr.isEmpty() && (_quotedExpr.indexIn(text) >= 0)) { + doQuote = true; + } else { + foreach(const QChar &val, _quotedChars) + { + if (text.contains(val)) { + doQuote = true; + break; + } + } + } + + if (doQuote) { + // Escape quote with an extra quote + text.replace(_quote, QString("%1%1").arg(_quote)); + // Wrap with quote characters + text = QString("%1%2%1").arg(_quote).arg(text); + } + + // Insert separator character if necessary + if (_colI > 0) { + text.push_front(_sep); + } + + _write(text); + ++_colI; +} + +void CsvWriter::writeEndRow() +{ + _write(_eol); + _colI = 0; +} + +void CsvWriter::_defaultOpts() +{ + _sep = ','; + _quote = '"'; + _eol = "\r\n"; + _quotedChars << _sep << _quote << '\r' << '\n'; +} + +void CsvWriter::_write(const QString &text) +{ + _str << text; + if (_str.status() != QTextStream::Ok) { + throw FileError( + QString("Failed to write output: %1").arg(_str.device()->errorString())); + } +} diff --git a/src/coalition_ulsprocessor/src/uls-script/CsvWriter.h b/src/coalition_ulsprocessor/src/uls-script/CsvWriter.h new file mode 100644 index 0000000..042df55 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/CsvWriter.h @@ -0,0 +1,150 @@ + +#ifndef CSV_WRITER_H +#define CSV_WRITER_H + +#include +#include +#include +#include +#include + +/** Write files per comma separated value format of RFC-4180. + * Optional file properties are non-standard separator, quotation characters, + * and end-of-line string. + */ +class CsvWriter +{ + /// Empty type for EOL placeholder + struct EndRow {}; + + public: + /** Any error associated with writing a CSV file. + */ + class FileError : public std::runtime_error + { + public: + FileError(const QString &msg) : runtime_error(msg.toStdString()) + { + } + }; + + /** Open the file for reading upon construction. + * @param fileName The name of the file to open for the lifetime of the + * CsvWriter object. + * @throw FileError if file cannot be opened. + */ + CsvWriter(const QString &fileName); + + /** Bind the writer to a given output device, opening it if necessary. + * @param device The device to write to. + * The lifetime of the device must be longer than the CsvWriter to + * avoid a dangling pointer. + * @throw FileError if the device is not writable. + */ + CsvWriter(QIODevice &device); + + /** Close the file if constructed with the fileName argument. + * If necessary, an end-of-row marker is written. + */ + ~CsvWriter(); + + /** Use a non-standard separator or quotation character. + * @param separator The field separator character. + * @param quote The field quotation character. + * @throw std::logic_error If the characters are the same. + */ + void setCharacters(const QChar &separator, const QChar "e); + + /** Set a static list of which columns should be unconditionally quoted. + * @param cols Each value is a column index to be quoted. + */ + void setQuotedColumns(const QSet &cols) + { + _quotedCols = cols; + } + + /** Define a non-standard definition of when to quote a CSV field. + * The standard is to quote if a quote, separator, or EOL is encountered. + */ + void setQuotedMatch(const QRegExp ®ex) + { + _quotedExpr = regex; + } + + /** Read a list of elements from a row in the file. + * @param records The list of records to write. + * @throw FileError if file write fails. + * @post All of the elements and an end-of-row marker are written to the stream. + */ + void writeRow(const QStringList &records); + + /** Write a single record to the CSV stream. + * When all records in a row are written, writeEndRow() should be called. + * @param record The element to write + * @throw FileError if file write fails. + */ + void writeRecord(const QString &record); + + /** Write the end-of-row indicator and start a new row. + * @pre Some number of records should be written with writeRecord(). + */ + void writeEndRow(); + + /// Placeholder for finishing row writes + static const EndRow endr; + + /** Write a single element to the CSV stream. + * @param record The element to write + * @return The modified CSV stream. + */ + CsvWriter &operator<<(const QString &record) + { + writeRecord(record); + return *this; + } + + /** Write and end-of-row indicator and start a new row. + * @return The modified CSV stream. + */ + CsvWriter &operator<<(const EndRow &) + { + writeEndRow(); + return *this; + } + + private: + /** Set control options to defaults. + * @post The #_sep, #_quote, and #_eol characters are set to RFC defaults. + */ + void _defaultOpts(); + + /** Write data to the device and verify status. + * @param text The text to write. + * @throw FileError if a problem occurs. + */ + void _write(const QString &text); + + /// Inserted between values + QChar _sep; + /// Surround values to be quoted + QChar _quote; + /// Appended to end row + QByteArray _eol; + + /// List of strings which, if contained, will cause the value to be quoted + QSet _quotedChars; + /// List of column indices (zero-indexed) to be quoted unconditionally + QSet _quotedCols; + /// Expression used to determine which values to quote + QRegExp _quotedExpr; + + /// Set to true if this object owns the IO device + bool _ownFile; + /// Underlying output stream + QTextStream _str; + + /// The current column index (starting at zero) + int _colI; +}; + +#endif // CSV_WRITER_H diff --git a/src/coalition_ulsprocessor/src/uls-script/EcefModel.cpp b/src/coalition_ulsprocessor/src/uls-script/EcefModel.cpp new file mode 100644 index 0000000..a367d96 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/EcefModel.cpp @@ -0,0 +1,88 @@ +#include +#include +#include "MathConstants.h" +#include "EcefModel.h" + +// Note: Altitude here is a true altitude, i.e. a height. Given an altitude (in km), this returns a +// value in an ECEF coordinate +// frame in km. +Vector3 EcefModel::geodeticToEcef(double lat, double lon, double alt) +{ + const double a = + MathConstants::WGS84EarthSemiMajorAxis; // 6378.137; // Radius of the earth in km. + const double esq = + MathConstants::WGS84EarthFirstEccentricitySquared; // 6.694379901e-3; // First + // eccentricity squared. + + // Convert lat/lon to radians. + const double latr = lat * M_PI / 180.0; + const double lonr = lon * M_PI / 180.0; + + double cosLon, sinLon; + ::sincos(lonr, &sinLon, &cosLon); + double cosLat, sinLat; + ::sincos(latr, &sinLat, &cosLat); + + // Compute 'chi', which adjusts for vertical eccentricity. + const double chi = sqrt(1.0 - esq * sinLat * sinLat); + + return Vector3((a / chi + alt) * cosLat * cosLon, + (a / chi + alt) * cosLat * sinLon, + (a * (1 - esq) / chi + alt) * sinLat); +} + +// Converts from ecef to geodetic coordinates. This algorithm is from Wikipedia, and +// all constants are from WGS '84. +GeodeticCoord EcefModel::ecefToGeodetic(const Vector3 &ecef) +{ + const double a = MathConstants::WGS84EarthSemiMajorAxis; // 6378.137; + const double b = MathConstants::WGS84EarthSemiMinorAxis; // 6356.7523142; + // double e = sqrt(MathConstants::WGS84EarthFirstEccentricitySquared); + const double eprime = sqrt(MathConstants::WGS84EarthSecondEccentricitySquared); + const double esq = MathConstants::WGS84EarthFirstEccentricitySquared; + // double eprimesq = MathConstants::WGS84EarthSecondEccentricitySquared; + + const double X = ecef.x(); + const double Y = ecef.y(); + const double Z = ecef.z(); + + double r = sqrt(X * X + Y * Y); + double Esq = a * a - b * b; + double F = 54 * b * b * Z * Z; + double G = r * r + (1 - esq) * Z * Z - esq * Esq; + double C = esq * esq * F * r * r / (G * G * G); + double S = pow(1 + C + sqrt(C * C + 2 * C), 1.0 / 3.0); + double P = F / (3 * (S + 1 / S + 1) * (S + 1 / S + 1) * G * G); + double Q = sqrt(1 + 2 * esq * esq * P); + double r0 = -(P * esq * r) / (1 + Q) + + sqrt(a * a / 2 * (1 + 1 / Q) - (P * (1 - esq) * Z * Z) / (Q * (1 + Q)) - + P * r * r / 2.0); + double U = sqrt((r - esq * r0) * (r - esq * r0) + Z * Z); + double V = sqrt((r - esq * r0) * (r - esq * r0) + (1 - esq) * Z * Z); + double Z0 = (b * b * Z) / (a * V); + + double h = U * (1 - (b * b) / (a * V)); + double lat = atan((Z + eprime * eprime * Z0) / r) * 180 / M_PI; + double lon = atan2(Y, X) * 180 / M_PI; + return GeodeticCoord(lon, lat, h); +} + +Vector3 EcefModel::fromGeodetic(const GeodeticCoord &in) +{ + return geodeticToEcef(in.latitudeDeg, in.longitudeDeg, in.heightKm); +} + +GeodeticCoord EcefModel::toGeodetic(const Vector3 &in) +{ + return ecefToGeodetic(in); +} + +Vector3 EcefModel::localVertical(const GeodeticCoord &in) +{ + double cosLon, sinLon; + ::sincos(M_PI / 180.0 * in.longitudeDeg, &sinLon, &cosLon); + double cosLat, sinLat; + ::sincos(M_PI / 180.0 * in.latitudeDeg, &sinLat, &cosLat); + + return Vector3(cosLat * cosLon, cosLat * sinLon, sinLat); +} diff --git a/src/coalition_ulsprocessor/src/uls-script/EcefModel.h b/src/coalition_ulsprocessor/src/uls-script/EcefModel.h new file mode 100644 index 0000000..556b1ab --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/EcefModel.h @@ -0,0 +1,36 @@ +#ifndef ECEF_MODEL_H +#define ECEF_MODEL_H + +#include +#include "GeodeticCoord.h" +#include "Vector3.h" + +/** Convert between geodetic coordinates and WGS84 Earth-centered Earth-fixed + * (ECEF) coordinates. + */ +class EcefModel +{ + public: + static Vector3 geodeticToEcef(double lat, double lon, double alt); + static GeodeticCoord ecefToGeodetic(const Vector3 &ecef); + + /** Convert from geodetic coordinates to ECEF point. + * @param in The geodetic coordinates to convert. + * @return The ECEF coordinates for the same location (in units kilometers). + */ + static Vector3 fromGeodetic(const GeodeticCoord &in); + + /** Convert from ECEF point to geodetic coordinates. + * @param in The ECEF coordiantes to convert (in units kilometers). + * @return The geodetic coordinates for the same location. + */ + static GeodeticCoord toGeodetic(const Vector3 &in); + + /** Determine the local ellipsoid normal "up" direction at a given location. + * @param in The geodetic coordinates of the location. + * @return A unit vector in ECEF coordinates in the direction "up". + */ + static Vector3 localVertical(const GeodeticCoord &in); +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/FreqAssignment.cpp b/src/coalition_ulsprocessor/src/uls-script/FreqAssignment.cpp new file mode 100644 index 0000000..485efd1 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/FreqAssignment.cpp @@ -0,0 +1,237 @@ +/******************************************************************************************/ +/**** FILE: FreqAssignment.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include "FreqAssignment.h" + +/******************************************************************************************/ +/**** CONSTRUCTOR: FreqAssignmentClass::FreqAssignmentClass ****/ +/******************************************************************************************/ +FreqAssignmentClass::FreqAssignmentClass(std::string freqAssignmentFile) +{ + readFreqAssignment(freqAssignmentFile); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: FreqAssignmentClass::readFreqAssignment() ****/ +/******************************************************************************************/ +void FreqAssignmentClass::readFreqAssignment(const std::string filename) +{ + int linenum, fIdx; + std::string line, strval; + char *chptr; + FILE *fp = (FILE *)NULL; + std::string str; + std::string reasonIgnored; + std::ostringstream errStr; + + int frequencyFieldIdx = -1; + int bandwidthFieldIdx = -1; + + std::vector fieldIdxList; + std::vector fieldLabelList; + fieldIdxList.push_back(&frequencyFieldIdx); + fieldLabelList.push_back("channelFrequency"); + fieldIdxList.push_back(&bandwidthFieldIdx); + fieldLabelList.push_back("channelBandwidth"); + + double frequency; + double bandwidth; + + int fieldIdx; + + if (filename.empty()) { + throw std::runtime_error("ERROR: No Frequency Assignment File specified"); + } + + if (!(fp = fopen(filename.c_str(), "rb"))) { + str = std::string("ERROR: Unable to open Frequency Assignment File \"") + filename + + std::string("\"\n"); + throw std::runtime_error(str); + } + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + linenum = 0; + bool foundLabelLine = false; + while (fgetline(fp, line, false)) { + linenum++; + std::vector fieldList = splitCSV(line); + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if ((lineType == unknownLineType) && (!foundLabelLine)) { + lineType = labelLineType; + foundLabelLine = 1; + } + if ((lineType == unknownLineType) && (foundLabelLine)) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + bool found; + std::string field; + switch (lineType) { + case labelLineType: + for (fieldIdx = 0; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + + // std::cout << "FIELD: \"" << field << "\"" << std::endl; + + found = false; + for (fIdx = 0; + (fIdx < (int)fieldLabelList.size()) && (!found); + fIdx++) { + if (field == fieldLabelList.at(fIdx)) { + *fieldIdxList.at(fIdx) = fieldIdx; + found = true; + } + } + } + + for (fIdx = 0; fIdx < (int)fieldIdxList.size(); fIdx++) { + if (*fieldIdxList.at(fIdx) == -1) { + errStr << "ERROR: Invalid Frequency Assignment " + "file \"" + << filename << "\" label line missing \"" + << fieldLabelList.at(fIdx) << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + } + + break; + case dataLineType: + /**************************************************************************/ + /* frequency */ + /**************************************************************************/ + strval = fieldList.at(frequencyFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Frequency Assignment file \"" << filename + << "\" line " << linenum << " missing frequency" + << std::endl; + throw std::runtime_error(errStr.str()); + } else { + frequency = std::strtod(strval.c_str(), &chptr); + if (frequency <= 0.0) { + errStr << "ERROR: Frequency Assignment file \"" + << filename << "\" line " << linenum + << " invalid frequency: \"" << strval << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + } + /**************************************************************************/ + + /**************************************************************************/ + /* bandwidth */ + /**************************************************************************/ + strval = fieldList.at(bandwidthFieldIdx); + if (strval.empty()) { + errStr << "ERROR: Frequency Assignment file \"" << filename + << "\" line " << linenum << " missing bandwidth" + << std::endl; + throw std::runtime_error(errStr.str()); + } else { + bandwidth = std::strtod(strval.c_str(), &chptr); + if (bandwidth <= 0.0) { + errStr << "ERROR: Frequency Assignment file \"" + << filename << "\" line " << linenum + << " invalid bandwidth: \"" << strval << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + } + /**************************************************************************/ + + freqBWList.push_back(std::make_tuple(frequency, bandwidth)); + + break; + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + CORE_DUMP; + break; + } + } + + if (fp) { + fclose(fp); + } + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: FreqAssignmentClass::getBandwidthUS ****/ +/******************************************************************************************/ +double FreqAssignmentClass::getBandwidthUS(double freqMHz) +{ + bool found = false; + double bandwidth; + int i; + + // R2-AIP-19 (b), (c) + for (i = 0; (i < freqBWList.size()) && (!found); ++i) { + double freq = std::get<0>(freqBWList[i]); + double bw = std::get<1>(freqBWList[i]); + if (fabs(freqMHz - freq) <= 0.5) { + found = true; + bandwidth = bw; + } + } + + if (!found) { + // R2-AIP-19 (d) + if (freqMHz < 5925.0) { + bandwidth = -1.0; + } else if (freqMHz < 5955.0) { + bandwidth = 2 * (freqMHz - 5925.0); + } else if (freqMHz < 6395.0) { + bandwidth = 60.0; + } else if (freqMHz < 6425.0) { + bandwidth = 2 * (6425.0 - freqMHz); + } else if (freqMHz < 6525.0) { + bandwidth = -1.0; // UNII-6 not allowed for US + } else if (freqMHz < 6540.0) { + bandwidth = 2 * (freqMHz - 6525.0); + } else if (freqMHz < 6860.0) { + bandwidth = 30.0; + } else if (freqMHz < 6875.0) { + bandwidth = 2 * (6875.0 - freqMHz); + } else { + bandwidth = -1.0; + } + } + + return (bandwidth); +} +/******************************************************************************************/ diff --git a/src/coalition_ulsprocessor/src/uls-script/FreqAssignment.h b/src/coalition_ulsprocessor/src/uls-script/FreqAssignment.h new file mode 100644 index 0000000..c058be0 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/FreqAssignment.h @@ -0,0 +1,22 @@ +#ifndef FREQ_ASSIGNMENT_H +#define FREQ_ASSIGNMENT_H + +#include +#include +#include +#include +#include "global_fn.h" + +class FreqAssignmentClass +{ + public: + FreqAssignmentClass(const std::string freqAssignmentFile); + double getBandwidthUS(double freqMHz); + + private: + void readFreqAssignment(const std::string filename); + + std::vector> freqBWList; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/GeodeticCoord.cpp b/src/coalition_ulsprocessor/src/uls-script/GeodeticCoord.cpp new file mode 100644 index 0000000..90782ae --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/GeodeticCoord.cpp @@ -0,0 +1,41 @@ + +#include +#include +#include +#include "GeodeticCoord.h" + +namespace +{ +const int metaTypeId = qRegisterMetaType("GeodeticCoord"); +} + +const qreal GeodeticCoord::nan = std::numeric_limits::quiet_NaN(); + +bool GeodeticCoord::isNull() const +{ + return (std::isnan(longitudeDeg) || std::isnan(latitudeDeg) || std::isnan(heightKm)); +} + +void GeodeticCoord::normalize() +{ + // This is the number of (positive or negative) wraps occurring + const int over = std::floor((longitudeDeg + 180.0) / 360.0); + // Remove the number of wraps from longitude + longitudeDeg -= 360.0 * over; + // Clamp latitude + latitudeDeg = std::max(-90.0, std::min(+90.0, latitudeDeg)); +} + +bool GeodeticCoord::isIdenticalTo(const GeodeticCoord &other, qreal accuracy) const +{ + const qreal diffLon = longitudeDeg - other.longitudeDeg; + const qreal diffLat = latitudeDeg - other.latitudeDeg; + return ((std::abs(diffLon) <= accuracy) && (std::abs(diffLat) <= accuracy)); +} + +QDebug operator<<(QDebug stream, const GeodeticCoord &pt) +{ + stream.nospace() << "(lon: " << pt.longitudeDeg << ", lat: " << pt.latitudeDeg + << ", height: " << pt.heightKm << ")"; + return stream.space(); +} diff --git a/src/coalition_ulsprocessor/src/uls-script/GeodeticCoord.h b/src/coalition_ulsprocessor/src/uls-script/GeodeticCoord.h new file mode 100644 index 0000000..9d74a78 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/GeodeticCoord.h @@ -0,0 +1,97 @@ + + +#ifndef GEODETIC_COORD_H +#define GEODETIC_COORD_H + +#include +#include +//#include + +/** The base structure contains a 3D Earth-fixed geodetic coordinate. + * This is in the WGS84 ellipsoid, so any conversion functions must follow + * the WGS84 conventions. The height is an optional constructor parameter + * because it is unused in many cases, but it is still more consistent to + * have a single geodetic coordinate type than to have a 2D type and a 3D type + * separate from each other. + */ +struct GeodeticCoord { + /// Convenience definition for NaN value + static const qreal nan; + + /// Static helper function for lat/lon coordinate order. + static inline GeodeticCoord fromLatLon(qreal latDeg, qreal lonDeg, qreal htKm = 0) + { + return GeodeticCoord(lonDeg, latDeg, htKm); + } + + /// Static helper function for lon/lat coordinate order. + static inline GeodeticCoord fromLonLat(qreal lonDeg, qreal latDeg, qreal htKm = 0) + { + return GeodeticCoord(lonDeg, latDeg, htKm); + } + + /** Default constructor has NaN horizontal values to distinguish an + * invalid coordinate, but zero height value. + */ + GeodeticCoord() : longitudeDeg(nan), latitudeDeg(nan), heightKm(0) + { + } + + /** Construct a new geodetic coordinate, the height is optional. + */ + GeodeticCoord(qreal inLongitudeDeg, qreal inLatitudeDeg, qreal inHeightKm = 0) : + longitudeDeg(inLongitudeDeg), + latitudeDeg(inLatitudeDeg), + heightKm(inHeightKm) + { + } + + /** Implicit conversion to QVariant. + * @return The variant containing this GeodeticCoord value. + */ + operator QVariant() const + { + return QVariant::fromValue(*this); + } + + /** Determine if this location is the default NaN-valued. + * @return True if any coordinate is NaN. + */ + bool isNull() const; + + /** Normalize the coordinates in-place. + * Longitude is limited to the range [-180, +180) by wrapping. + * Latitude is limited to the range [-90, +90] by clamping. + */ + void normalize(); + + /** Get a normalized copy of the coordinates. + * @return A copy of these coordinates after normalize() is called on it. + */ + GeodeticCoord normalized() const + { + GeodeticCoord oth(*this); + oth.normalize(); + return oth; + } + + /** Compare two points to some required accuracy of sameness. + * @param other The point to compare against. + * @param accuracy This applies to difference between each of the + * longitudes and latitude independently. + */ + bool isIdenticalTo(const GeodeticCoord &other, qreal accuracy) const; + + /// Longitude referenced to WGS84 zero meridian; units of degrees. + qreal longitudeDeg; + /// Latitude referenced to WGS84 equator; units of degrees. + qreal latitudeDeg; + /// Height referenced to WGS84 ellipsoid; units of kilometers. + qreal heightKm; +}; +Q_DECLARE_METATYPE(GeodeticCoord); + +/// Allow debugging prints +QDebug operator<<(QDebug stream, const GeodeticCoord &pt); + +#endif /* GEODETIC_COORD_H */ diff --git a/src/coalition_ulsprocessor/src/uls-script/MathConstants.cpp b/src/coalition_ulsprocessor/src/uls-script/MathConstants.cpp new file mode 100644 index 0000000..2b283a8 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/MathConstants.cpp @@ -0,0 +1,21 @@ +/* + * Useful constants for various purposes. All static and public so they can be seen from + * everywhere. + */ + +#include "MathConstants.h" + +const double MathConstants::GeostationaryOrbitHeight = 35786.094; // altitude above sea-level (km) +const double MathConstants::EarthMaxRadius = 6378.137; // km, from WGS '84 +const double MathConstants::EarthMinRadius = 6356.760; // km +const double MathConstants::GeostationaryOrbitRadius = + MathConstants::EarthMaxRadius + + MathConstants::GeostationaryOrbitHeight; // from earth center (km) + +const double MathConstants::WGS84EarthSemiMajorAxis = 6378.137; // km +const double MathConstants::WGS84EarthSemiMinorAxis = 6356.7523142; // km +const double MathConstants::WGS84EarthFirstEccentricitySquared = 6.69437999014e-3; // unitless +const double MathConstants::WGS84EarthSecondEccentricitySquared = 6.73949674228e-3; // unitless + +const double MathConstants::speedOfLight = 2997924580.0; +const double MathConstants::boltzmannConstant = 1.3806488e-23; diff --git a/src/coalition_ulsprocessor/src/uls-script/MathConstants.h b/src/coalition_ulsprocessor/src/uls-script/MathConstants.h new file mode 100644 index 0000000..ab2e1c9 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/MathConstants.h @@ -0,0 +1,25 @@ +#ifndef MATH_CONSTANTS_H +#define MATH_CONSTANTS_H + +// See MathConstants.cpp for implementation details, values and units. + +class MathConstants +{ + public: + static const double GeostationaryOrbitHeight; + static const double GeostationaryOrbitRadius; + static const double EarthMaxRadius, EarthMinRadius; + static const double EarthEccentricitySquared; + + // WGS'84 constants. + static const double WGS84EarthSemiMajorAxis; + static const double WGS84EarthSemiMinorAxis; + static const double WGS84EarthFirstEccentricitySquared; + static const double WGS84EarthSecondEccentricitySquared; + + // Physics constants + static const double speedOfLight; + static const double boltzmannConstant; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/MathHelpers.cpp b/src/coalition_ulsprocessor/src/uls-script/MathHelpers.cpp new file mode 100644 index 0000000..bfcd7e9 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/MathHelpers.cpp @@ -0,0 +1,31 @@ + +#include +#include "MathHelpers.h" + +double MathHelpers::tile(double size, double value) +{ + // This is the number of (positive or negative) wraps occurring + const int over = std::floor(value / size); + // Remove the number of wraps + return value - size * over; +} + +double MathHelpers::clamp(double size, double value) +{ + return std::max(0.0, std::min(size, value)); +} + +double MathHelpers::mirror(double size, double value) +{ + // This is the number of (positive or negative) wraps occurring + const int over = std::floor(value / size); + // Even wraps simply tile + if (over % 2 == 0) { + // Remove the number of wraps + return value - size * over; + } + // Odd wraps tile with inversion + else { + return size * over - value; + } +} diff --git a/src/coalition_ulsprocessor/src/uls-script/MathHelpers.h b/src/coalition_ulsprocessor/src/uls-script/MathHelpers.h new file mode 100644 index 0000000..46fb204 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/MathHelpers.h @@ -0,0 +1,213 @@ + +#ifndef MATH_HELPERS_H +#define MATH_HELPERS_H + +#include +#include +#include + +namespace MathHelpers +{ + +/** Convert an angle from degrees to radians. + * @param deg The angle in degrees + * @return The angle in radians. + */ +template +inline T deg2rad(T deg) +{ + return M_PI / 180.0 * deg; +} +/** Convert an angle from radians to degrees. + * @param rad The angle in radians + * @return The angle in degrees. + */ +template +inline T rad2deg(T rad) +{ + return 180.0 / M_PI * rad; +} + +/** Shortcut for computing squares. + * @param val The value to square. + * @return The square of @c val. + */ +template +inline T sqr(T val) +{ + return val * val; +} + +/** Shortcut for computing cubes. + * @param val The value to cube. + * @return The cube of @c val. + */ +template +inline T cube(T val) +{ + return val * val * val; +} + +/** Shortcut for computing sinc. + * @param x The value to compute sinc(x) = sin(pi * x) / (pi * x). + * @return The sinc of @c x. + */ +template +inline T sinc(T x) +{ + static const double eps = 1e-6; + if (x < eps && x > -eps) + return 1.0 - sqr(M_PI * x) / 6.0; + else + return sin(M_PI * x) / (M_PI * x); +} + +/** One-dimensional linear interpolation. + * @param val1 The zero-scale value. + * @param val2 The unit-scale value. + * @param t The scale factor. + * @return The effective value (t * val1) + ((1-t) * val2) + */ +inline double interp1d(double val1, double val2, double t) +{ + return val1 + t * (val2 - val1); +} + +/** Wrap a value to a particular size by tiling the object space onto + * the image space. + * @param size The exclusive maximum limit. + * @param value The value to be limited. + * @return The value limited to the range [0, @a size) by tiling. + */ +double tile(double size, double value); + +/** Wrap a value to a particular size by clamping the object space to the + * edge of the image space. + * @param size The exclusive maximum limit. + * @param value The value to be limited. + * @return The value limited to the range [0, @a size] by clamping. + */ +double clamp(double size, double value); + +/** Wrap a value to a particular size by mirroring the object space onto + * the image space. + * @param size The exclusive maximum limit. + * @param value The value to be limited. + * @return The value limited to the range [0, @a size) by mirroring. + */ +double mirror(double size, double value); + +/** Prepare a sample point for interpolating. + * This stores a set of two (low/high) integer-points corresponding to + * a single sample point, and a scale factor between them. + */ +struct Align { + /** Compute the grid-aligned points and scale. + * + * @param value The value to compute for. + */ + Align(const double value) + { + p1 = std::floor(value); + p2 = std::ceil(value); + factor = value - p1; + } + + /// First integer-point lower than the value + double p1; + /// First integer-point higher than the value + double p2; + /** Inverse weight factor to use for #p1 point. + * Value has range [0, 1] where 0 means p1 == value, 1 means p2 == value. + */ + double factor; +}; + +// helper class to calculate statistics of a continuously sampled one dimensional process +// without storage of the invidual samples +template +class RunningStatistic +{ + public: + RunningStatistic() + { + _count = 0; + _sum = 0.0; + _sumOfSquares = 0.0; + _max = 0.0; + _min = std::numeric_limits::max(); + } + + inline RunningStatistic &operator<<(T sample) + { + if (_count == 0) + _max = _min = sample; + + _count++; + _sum += sample; + _sumOfSquares += sqr(sample); + _max = std::max(_max, sample); + _min = std::min(_min, sample); + return *this; + } + + inline RunningStatistic &operator<<(const RunningStatistic &statistic) + { + _count += statistic._count; + _sum += statistic._sum; + _sumOfSquares += statistic._sumOfSquares; + _max = std::max(_max, statistic._max); + _min = std::min(_min, statistic._min); + return *this; + } + + inline int count() const + { + return _count; + } + + inline T mean() const + { + if (_count == 0) + return 0.0; + + return _sum / T(_count); + } + + inline T min() const + { + if (_count == 0) + return 0.0; + + return _min; + } + + inline T max() const + { + return _max; + } + + inline T variance(bool unbiased = true) const + { + if (_count < 1) + return 0.0; + + T u = mean(); + if (unbiased) + return _sumOfSquares / T(_count - 1) - + T(_count) / T(_count - 1) * sqr(u); + else + return _sumOfSquares / T(_count) - sqr(u); + } + + private: + int _count; + T _sum; + T _sumOfSquares; + T _min; + T _max; +}; + +} // end namespace MathHelpers + +#endif /* MATH_HELPERS_H */ diff --git a/src/coalition_ulsprocessor/src/uls-script/PassiveRepeaterCA.h b/src/coalition_ulsprocessor/src/uls-script/PassiveRepeaterCA.h new file mode 100644 index 0000000..cedc831 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/PassiveRepeaterCA.h @@ -0,0 +1,82 @@ +/* + * This data structure stores the data related to a Back To Back Passive Repeater Data and + * Billboard Passive Repeader Data for Canada (CA). There is a field in this for each relevent + * column in the ISED database, regardless of whether or not it is populated. + * + */ + +#ifndef PASSIVE_REPEATER_CA_H +#define PASSIVE_REPEATER_CA_H + +class PassiveRepeaterCAClass +{ + public: + /**************************************************************************************/ + /**** PRType ****/ + /**************************************************************************************/ + enum PRTypeEnum { + backToBackAntennaPRType, + billboardReflectorPRType, + unknownPRType + }; + /**************************************************************************************/ + + std::string authorizationNumber; + double latitudeDeg; + double longitudeDeg; + double groundElevation; + double heightAGLA; + double heightAGLB; + + PRTypeEnum type; + + // Back to Back Antenna Parameters + double antennaGainA; + double antennaGainB; + std::string antennaModelA; + std::string antennaModelB; + double azimuthPtgA; + double azimuthPtgB; + double elevationPtgA; + double elevationPtgB; + Vector3 positionA; + Vector3 pointingVecA; + Vector3 positionB; + Vector3 pointingVecB; + + // Billboard Reflector Parameters + double reflectorHeight; + double reflectorWidth; + Vector3 reflectorPosition; + Vector3 reflectorPointingVec; +}; + +class BackToBackPassiveRepeaterCAClass +{ + public: + std::string authorizationNumber; + double latitudeDeg; + double longitudeDeg; + double groundElevation; + double heightAGL; + double antennaGain; + std::string antennaModel; + double azimuthPtg; + double elevationPtg; +}; + +class ReflectorPassiveRepeaterCAClass +{ + public: + std::string authorizationNumber; + double latitudeDeg; + double longitudeDeg; + double groundElevation; + double heightAGL; + double reflectorHeight; + double reflectorWidth; + double azimuthPtg; + double elevationPtg; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/RAS.h b/src/coalition_ulsprocessor/src/uls-script/RAS.h new file mode 100644 index 0000000..ae26d1e --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/RAS.h @@ -0,0 +1,34 @@ +/* + * This data structure stores the data related to a Radio Astronomy Station + * + */ + +#ifndef RAS_H +#define RAS_H + +#include "Vector3.h" + +class RASClass +{ + public: + std::string region; + std::string name; + std::string location; + double startFreqMHz; + double stopFreqMHz; + std::string exclusionZone; + double rect1lat1; + double rect1lat2; + double rect1lon1; + double rect1lon2; + double rect2lat1; + double rect2lat2; + double rect2lon1; + double rect2lon2; + double radiusKm; + double centerLat; + double centerLon; + double heightAGL; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/StationDataCA.h b/src/coalition_ulsprocessor/src/uls-script/StationDataCA.h new file mode 100644 index 0000000..94c73ab --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/StationDataCA.h @@ -0,0 +1,41 @@ +/* + * This data structure stores the data related to a Station Data for Canada (CA). + * There is a field in this for each relevent column in the ISED database, regardless of + * whether or not it is populated. + * + */ + +#ifndef STATION_DATA_CA_H +#define STATION_DATA_CA_H + +#include "Vector3.h" + +class StationDataCAClass +{ + public: + int service; + int subService; + std::string authorizationNumber; + std::string callsign; + double latitudeDeg; + double longitudeDeg; + double groundElevation; + double antennaHeightAGL; + std::string emissionsDesignator; + double bandwidthMHz; + double centerFreqMHz; + double antennaGain; + std::string antennaModel; + std::string modulation; + double azimuthPtg; + double elevationPtg; + double lineLoss; + std::string inServiceDate; + std::string stationLocation; + std::string antennaManufacturer; + + Vector3 position; + Vector3 pointingVec; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/TransmitterCA.h b/src/coalition_ulsprocessor/src/uls-script/TransmitterCA.h new file mode 100644 index 0000000..b23bed4 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/TransmitterCA.h @@ -0,0 +1,31 @@ +/* + * This data structure stores the data related to Transmitters for Canada (CA). + * There is a field in this for each relevent column in the ISED database, regardless of + * whether or not it is populated. + * + */ + +#ifndef TRANSMITTER_CA_H +#define TRANSMITTER_CA_H + +class TransmitterCAClass +{ + public: + int service; + int subService; + std::string authorizationNumber; + std::string callsign; + double latitudeDeg; + double longitudeDeg; + double groundElevation; + double antennaHeightAGL; + std::string emissionsDesignator; + double bandwidthMHz; + double centerFreqMHz; + double antennaGain; + std::string antennaModel; + std::string modulation; + double modRate; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/TransmitterModelMap.cpp b/src/coalition_ulsprocessor/src/uls-script/TransmitterModelMap.cpp new file mode 100644 index 0000000..d90b91a --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/TransmitterModelMap.cpp @@ -0,0 +1,355 @@ +/******************************************************************************************/ +/**** FILE: TransmitterModelMap.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include "TransmitterModelMap.h" + +/******************************************************************************************/ +/**** CONSTRUCTOR: TransmitterModelClass::TransmitterModelClass ****/ +/******************************************************************************************/ +TransmitterModelClass::TransmitterModelClass(std::string nameVal) : name(nameVal) +{ + architecture = UnknownArchitecture; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** STATIC FUNCTION: TransmitterModelClass::architectureStr() ****/ +/******************************************************************************************/ +std::string TransmitterModelClass::architectureStr(ArchitectureEnum architectureVal) +{ + std::string str; + + switch (architectureVal) { + case IDUArchitecture: + str = "IDU"; + break; + case ODUArchitecture: + str = "ODU"; + break; + case UnknownArchitecture: + str = "UNKNOWN"; + break; + default: + CORE_DUMP; + break; + } + + return (str); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** CONSTRUCTOR: TransmitterModelMapClass::TransmitterModelMapClass ****/ +/******************************************************************************************/ +TransmitterModelMapClass::TransmitterModelMapClass(std::string transmitterModelListFile) +{ + readModelList(transmitterModelListFile); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: TransmitterModelMapClass::readModelList() ****/ +/******************************************************************************************/ +void TransmitterModelMapClass::readModelList(const std::string filename) +{ + int linenum, fIdx; + std::string line, strval; + FILE *fp = (FILE *)NULL; + std::string str; + std::string reasonIgnored; + std::ostringstream errStr; + int numError = 0; + int numIgnore = 0; + + int modelNameFieldIdx = -1; + int architectureFieldIdx = -1; + + std::vector fieldIdxList; + std::vector fieldLabelList; + fieldIdxList.push_back(&modelNameFieldIdx); + fieldLabelList.push_back("radioModelPrefix"); + fieldIdxList.push_back(&architectureFieldIdx); + fieldLabelList.push_back("architecture"); + + std::string name; + TransmitterModelClass::ArchitectureEnum architecture; + + int fieldIdx; + + if (filename.empty()) { + throw std::runtime_error("ERROR: No Transmitter Model List File specified"); + } + + if (!(fp = fopen(filename.c_str(), "rb"))) { + str = std::string("ERROR: Unable to open Transmitter Model List File \"") + + filename + std::string("\"\n"); + throw std::runtime_error(str); + } + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + TransmitterModelClass *transmitterModel; + + linenum = 0; + bool foundLabelLine = false; + while (fgetline(fp, line, false)) { + linenum++; + std::vector fieldList = splitCSV(line); + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if ((lineType == unknownLineType) && (!foundLabelLine)) { + lineType = labelLineType; + foundLabelLine = 1; + } + if ((lineType == unknownLineType) && (foundLabelLine)) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + bool found; + bool errorFlag; + bool ignoreFlag; + std::string field; + switch (lineType) { + case labelLineType: + for (fieldIdx = 0; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + + // std::cout << "FIELD: \"" << field << "\"" << std::endl; + + found = false; + for (fIdx = 0; + (fIdx < (int)fieldLabelList.size()) && (!found); + fIdx++) { + if (field == fieldLabelList.at(fIdx)) { + *fieldIdxList.at(fIdx) = fieldIdx; + found = true; + } + } + } + + for (fIdx = 0; fIdx < (int)fieldIdxList.size(); fIdx++) { + if (*fieldIdxList.at(fIdx) == -1) { + errStr << "ERROR: Invalid Transmitter Model List " + "file \"" + << filename << "\" label line missing \"" + << fieldLabelList.at(fIdx) << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + } + + break; + case dataLineType: + errorFlag = false; + ignoreFlag = false; + + /**************************************************************************/ + /* modelName */ + /**************************************************************************/ + strval = fieldList.at(modelNameFieldIdx); + if (strval.empty()) { + errStr << "WARNING: Transmitter Model List file \"" + << filename << "\" line " << linenum + << " missing model name" << std::endl; + + // throw std::runtime_error(errStr.str()); + + std::cout << errStr.str(); + errStr.str(std::string()); + errorFlag = true; + } + + name = strval; + /**************************************************************************/ + + /**************************************************************************/ + /* architecture */ + /**************************************************************************/ + if (!errorFlag) { + strval = fieldList.at(architectureFieldIdx); + + if (strval.empty()) { + errStr << "ERROR: Transmitter Model List file \"" + << filename << "\" line " << linenum + << " missing architecture" << std::endl; + + // throw std::runtime_error(errStr.str()); + + std::cout << errStr.str(); + errStr.str(std::string()); + errorFlag = true; + } else if (strval == "IDU") { + architecture = + TransmitterModelClass::IDUArchitecture; + } else if (strval == "ODU") { + architecture = + TransmitterModelClass::ODUArchitecture; + } else if ((strval == "Unknown") || (strval == "UNKNOWN")) { + architecture = + TransmitterModelClass::UnknownArchitecture; + } else { + errStr << "ERROR: Transmitter Model List file \"" + << filename << "\" line " << linenum + << " invalid architecture: " << strval + << std::endl; + + // throw std::runtime_error(errStr.str()); + + std::cout << errStr.str(); + errorFlag = true; + } + } + /**************************************************************************/ + + if (errorFlag) { + numError++; + } else if (ignoreFlag) { + numIgnore++; + } else { + transmitterModel = new TransmitterModelClass(name); + transmitterModel->setArchitecture(architecture); + + transmitterModelList.push_back(transmitterModel); + } + + break; + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + CORE_DUMP; + break; + } + } + + if (fp) { + fclose(fp); + } + + std::cout << "NUM LINES IGNORED ERROR in " << filename << ": " << numError << std::endl; + std::cout << "NUM LINES IGNORED ARCHITECTURE UNKNOWN in " << filename << ": " << numIgnore + << std::endl; + + return; +} +/******************************************************************************************/ + +inline bool isInvalidModelNameChar(char c) +{ + // Valid characters are 'A' - 'Z' and '0' - '9' + bool isLetter = (c >= 'A') && (c <= 'Z'); + bool isNum = (c >= '0') && (c <= '9'); + bool valid = isLetter || isNum; + return (!valid); +} + +/******************************************************************************************/ +/**** FUNCTION: TransmitterModelMapClass::find() ****/ +/******************************************************************************************/ +TransmitterModelClass *TransmitterModelMapClass::find(std::string modelName) +{ + bool found = false; + int i; + + TransmitterModelClass *transmitterModel = (TransmitterModelClass *)NULL; + + /**********************************************************************************/ + /* Convert ModelName to uppercase */ + /**********************************************************************************/ + std::transform(modelName.begin(), modelName.end(), modelName.begin(), ::toupper); + /**********************************************************************************/ + + /**********************************************************************************/ + /* Remove non-alhpanumeric characters */ + /**********************************************************************************/ + modelName.erase(std::remove_if(modelName.begin(), modelName.end(), isInvalidModelNameChar), + modelName.end()); + /**********************************************************************************/ + + /**********************************************************************************/ + /* Match if an transmitterModelList contains a model that is: */ + /* * prefix of modelName */ + /* If multiple prefices are found, select the longest one */ + /**********************************************************************************/ + for (i = 0; i < (int)transmitterModelList.size(); ++i) { + TransmitterModelClass *m = transmitterModelList[i]; + + if (modelName.compare(0, m->name.size(), m->name) == 0) { + if (!found) { + found = true; + transmitterModel = m; + } else if (m->name.size() > transmitterModel->name.size()) { + transmitterModel = m; + } + } + } + /**********************************************************************************/ + + return (transmitterModel); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: TransmitterModelMapClass::find() ****/ +/******************************************************************************************/ +int TransmitterModelMapClass::checkPrefixValues() +{ + int ia, ib; + int numError = 0; + + for (ia = 0; ia < (int)transmitterModelList.size(); ++ia) { + TransmitterModelClass *ma = transmitterModelList[ia]; + for (ib = 0; ib < (int)transmitterModelList.size(); ++ib) { + if (ib != ia) { + TransmitterModelClass *mb = transmitterModelList[ib]; + if ((ma->architecture != + TransmitterModelClass::UnknownArchitecture) && + (mb->name.compare(0, ma->name.size(), ma->name) == 0)) { + numError++; + std::cout << "(" << numError << ") " << ma->name << "[" + << TransmitterModelClass::architectureStr( + ma->architecture) + << "] is a prefix of " << mb->name << "[" + << TransmitterModelClass::architectureStr( + mb->architecture) + << "]" + << (ma->architecture != mb->architecture ? + " DIFFERENT ARCHITECTURE" : + "") + << std::endl; + } + } + } + } + + return (numError); +} +/******************************************************************************************/ diff --git a/src/coalition_ulsprocessor/src/uls-script/TransmitterModelMap.h b/src/coalition_ulsprocessor/src/uls-script/TransmitterModelMap.h new file mode 100644 index 0000000..85f1077 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/TransmitterModelMap.h @@ -0,0 +1,40 @@ +#ifndef TRANSMITTER_MODEL_MAP_H +#define TRANSMITTER_MODEL_MAP_H + +#include +#include +#include +#include "global_fn.h" + +class TransmitterModelClass +{ + public: + enum ArchitectureEnum { IDUArchitecture, ODUArchitecture, UnknownArchitecture }; + + TransmitterModelClass(std::string nameVal); + + void setArchitecture(ArchitectureEnum architectureVal) + { + architecture = architectureVal; + } + + static std::string architectureStr(ArchitectureEnum architectureVal); + + std::string name; + ArchitectureEnum architecture; +}; + +class TransmitterModelMapClass +{ + public: + TransmitterModelMapClass(const std::string transmitterModelListFile); + TransmitterModelClass *find(std::string modelName); + int checkPrefixValues(); + + private: + void readModelList(const std::string filename); + + std::vector transmitterModelList; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsAntenna.h b/src/coalition_ulsprocessor/src/uls-script/UlsAntenna.h new file mode 100644 index 0000000..5efe799 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsAntenna.h @@ -0,0 +1,119 @@ +/* + * This data structure stores the data related to an antenna. + * There is a field in this for each relevent column in the ULS database, regardless of whether + * or not it is populated. + * + */ + +#ifndef ULS_ANTENNA_H +#define ULS_ANTENNA_H + +class UlsAntenna +{ + public: + long long systemId; // unique identifier for this record; may not be necessary + char callsign[11]; // this is the 'key' for the owner of this antenna. + int antennaNumber; // identifier for this antenna + int locationNumber; // location of the antenna. This matches up with a UlsLocation + // of the same callsign. + char recvZoneCode[6]; // marker for the received zone? + char antennaType; // R or T or P + double heightToTip; // Presumably in feet. + double heightToCenterRAAT; // Likewise. + std::string antennaMake; // Make/model of antenna. + std::string antennaModel; + double tilt; + char polarizationCode[5]; // Many of these properties are unpopulated. + double beamwidth; + double gain; + double azimuth; + double heightAboveAverageTerrain; + double diversityHeight; + double diversityGain; + double diversityBeam; + double reflectorHeight; + double reflectorWidth; + double reflectorSeparation; + int passiveRepeaterNumber; + double backtobackTxGain; + double backtobackRxGain; + char locationName[20]; // Used to cross-check against locations. + int passiveRepeaterSequenceId; + char alternativeCGSA; + int pathNumber; // Which microwave path this record is used in. This maps to a + // UlsPath of the same callsign. + double lineLoss; + char statusCode; + char statusDate[11]; + + enum AntennaParameters { + MinAntennaParameter = 0x01000000, + AntennaSystemId = 0x01000001, + AntennaCallsign = 0x01000002, + AntennaNumber = 0x01000003, + AntennaLocationNumber = 0x01000004, + AntennaReceiveZoneCode = 0x01000005, + AntennaType = 0x01000006, + AntennaHeightToTip = 0x01000007, + AntennaHeightToCenterRAAT = 0x01000008, + AntennaMake = 0x01000009, + AntennaModel = 0x0100000A, + AntennaTilt = 0x0100000B, + AntennaPolarizationCode = 0x0100000C, + AntennaBeamWidth = 0x0100000D, + AntennaGain = 0x0100000F, + AntennaAzimuth = 0x01000010, + AntennaHeightAboveAverageTerrain = 0x01000011, + AntennaDiversityHeight = 0x01000012, + AntennaDiversityGain = 0x01000013, + AntennaDiversityBeam = 0x01000014, + AntennaReflectorHeight = 0x01000015, + AntennaReflectorWidth = 0x01000016, + AntennaReflectorSeparation = 0x01000017, + AntennaPassiveRepeaterNumber = 0x01000018, + AntennaBackToBackTxGain = 0x01000019, + AntennaBackToBackRxGain = 0x0100001a, + AntennaLocationName = 0x0100001b, + AntennaPassiveRepeaterSequenceId = 0x0100001c, + AntennaAlternativeCGSA = 0x0100001d, + AntennaPathNumber = 0x0100001e, + AntennaLineLoss = 0x0100001f, + AntennaStatusCode = 0x01000020, + AntennaStatusDate = 0x01000021, + MaxAntennaParameter = 0x01000022, + ReceiveAntennaSystemId = 0x11000001, + ReceiveAntennaCallsign = 0x11000002, + ReceiveAntennaNumber = 0x11000003, + ReceiveAntennaLocationNumber = 0x11000004, + ReceiveAntennaReceiveZoneCode = 0x11000005, + ReceiveAntennaType = 0x11000006, + ReceiveAntennaHeightToTip = 0x11000007, + ReceiveAntennaHeightToCenterRAAT = 0x11000008, + ReceiveAntennaMake = 0x11000009, + ReceiveAntennaModel = 0x1100000A, + ReceiveAntennaTilt = 0x1100000B, + ReceiveAntennaPolarizationCode = 0x1100000C, + ReceiveAntennaBeamWidth = 0x1100000D, + ReceiveAntennaGain = 0x1100000F, + ReceiveAntennaAzimuth = 0x11000010, + ReceiveAntennaHeightAboveAverageTerrain = 0x11000011, + ReceiveAntennaDiversityHeight = 0x11000012, + ReceiveAntennaDiversityGain = 0x11000013, + ReceiveAntennaDiversityBeam = 0x11000014, + ReceiveAntennaReflectorHeight = 0x11000015, + ReceiveAntennaReflectorWidth = 0x11000016, + ReceiveAntennaReflectorSeparation = 0x11000017, + ReceiveAntennaPassiveRepeaterNumber = 0x11000018, + ReceiveAntennaBackToBackTxGain = 0x11000019, + ReceiveAntennaBackToBackRxGain = 0x1100001a, + ReceiveAntennaLocationName = 0x1100001b, + ReceiveAntennaPassiveRepeaterSequenceId = 0x1100001c, + ReceiveAntennaAlternativeCGSA = 0x1100001d, + ReceiveAntennaPathNumber = 0x1100001e, + ReceiveAntennaLineLoss = 0x1100001f, + ReceiveAntennaStatusCode = 0x11000020, + ReceiveAntennaStatusDate = 0x11000021, + }; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsCallsign.h b/src/coalition_ulsprocessor/src/uls-script/UlsCallsign.h new file mode 100644 index 0000000..f0cbe83 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsCallsign.h @@ -0,0 +1,44 @@ +/* + * This database permits fast lookup of links, locations, etc via callsign. Each callsign has + * its own table of locations, antennas, etc. + * + */ + +#ifndef ULS_CALLSIGN_H +#define ULS_CALLSIGN_H + +#include "UlsLocation.h" +#include "UlsAntenna.h" +#include "UlsFrequency.h" +#include "UlsPath.h" +#include "UlsEmission.h" +#include "UlsEntity.h" +#include "UlsMarketFrequency.h" +#include +#include +#include + +class UlsCallsign +{ + public: + QString callsign; + char callsignascii[11]; + QList *headers; + QList *antennas; + QList *locations; + QList *frequencies; + QList *emissions; + QList *entities; + QList *marketFreqs; + + UlsCallsign() + { + antennas = NULL; + locations = NULL; + frequencies = NULL; + headers = NULL; + strlcpy(callsignascii, "", sizeof(callsignascii)); + } +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsControlPoint.h b/src/coalition_ulsprocessor/src/uls-script/UlsControlPoint.h new file mode 100644 index 0000000..c13ad9f --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsControlPoint.h @@ -0,0 +1,22 @@ +#ifndef ULS_CONTROL_POINT_H +#define ULS_CONTROL_POINT_H + +class UlsControlPoint +{ + public: + long long systemId; + char ulsFilenumber[15]; + char ebfNumber[31]; + char callsign[11]; + char controlPointActionPerformed; + int controlPointNumber; + char controlPointAddress[81]; + char controlPointCity[21]; + char controlPointState[3]; + char controlPointPhone[11]; + char controlPointCounty[61]; + char controlPointStatus[1]; + char controlPointStatusDate[14]; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsEmission.h b/src/coalition_ulsprocessor/src/uls-script/UlsEmission.h new file mode 100644 index 0000000..edc17af --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsEmission.h @@ -0,0 +1,18 @@ +#ifndef ULS_EMISSION_H +#define ULS_EMISSION_H + +class UlsEmission +{ + public: + long long systemId; + char callsign[11]; + int locationId; + int antennaId; + double frequency; + char desig[11]; + double modRate; + std::string modCode; + int frequencyId; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsEntity.h b/src/coalition_ulsprocessor/src/uls-script/UlsEntity.h new file mode 100644 index 0000000..6acab0d --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsEntity.h @@ -0,0 +1,64 @@ +#ifndef ULS_ENTITY_H +#define ULS_ENTITY_H + +class UlsEntity +{ + public: + long long systemId; + char ulsFilenumber[15]; + char ebfNumber[31]; + char callsign[11]; + char entityType[3]; + char licenseeId[10]; + char entityName[201]; + char firstName[21]; + char mi[2]; + char lastName[21]; + char suffix[4]; + char phone[11]; + char fax[11]; + char email[51]; + char street[61]; + char city[21]; + char state[3]; + char zip[10]; + char pobox[21]; + char attnLine[36]; + char sgin[4]; + char frn[11]; + char applicantTypeCode[2]; + char applicantTypeCodeOther[41]; + char statusCode[1]; + char statusDate[14]; + + enum Parameter { + MinEntityParameter = 0x05000000, + EntityUlsFilenumber = 0x05000001, + EntityEbfNumber = 0x05000002, + EntityCallSign = 0x05000003, + EntityEntityType = 0x05000004, + EntityLicenseeId = 0x05000005, + EntityEntityName = 0x05000006, + EntityFirstName = 0x05000007, + EntityMiddleInit = 0x05000008, + EntityLastName = 0x05000009, + EntitySuffix = 0x0500000a, + EntityPhoneNumber = 0x0500000b, + EntityFaxNumber = 0x0500000c, + EntityEmailAddress = 0x0500000d, + EntityStreetAddress = 0x0500000e, + EntityState = 0x0500000f, + EntityZipCode = 0x05000010, + EntityPoBox = 0x05000011, + EntityAttnLine = 0x05000012, + EntitySGIN = 0x05000013, + EntityFRN = 0x05000014, + EntityApplicantTypeCode = 0x05000015, + EntityApplicantTypeCodeOther = 0x05000016, + EntityStatusCode = 0x05000017, + EntityStatusDate = 0x05000018, + MaxEntityParameter = 0x05000019, + }; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsFileReader.cpp b/src/coalition_ulsprocessor/src/uls-script/UlsFileReader.cpp new file mode 100644 index 0000000..0dcbe33 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsFileReader.cpp @@ -0,0 +1,1850 @@ +#include +#include +#include +#include +#include +#include + +#include "global_fn.h" +#include "UlsFileReader.h" +#include "UlsFunctions.h" +#include "EcefModel.h" + +bool fixMissingRxCallsign = false; + +namespace +{ +double emptyAtof(const char *str) +{ + char *endptr; + double z = strtod(str, &endptr); + + if (endptr == str) { + // No conversion performed, str empty or not numeric + z = std::numeric_limits::quiet_NaN(); + } + + return z; +} + +// On AUG 18, 2022 the FCC modified the format of PA records increasing the number of columns from +// 22 to 24. The variable maxcol is set to 22, and this function ignores any additional columns +// after maxcol. void SetToNextLine(FILE *fi, char c) { +// while(c != '\n' && c != EOF) { +// c = fgetc(fi); +// } +// +// } +}; // namespace + +/**************************************************************************/ +/* UlsFileReader::UlsFileReader() */ +/**************************************************************************/ +UlsFileReader::UlsFileReader(const char *fpath, + FILE *fwarn, + bool alignFederatedFlag, + double alignFederatedScale) +{ + FILE *fi = fopen(fpath, "r"); + std::ostringstream errStr; + std::string line; + + std::string front; + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + int linenum = 0; + QMap autoCellCounts; + + while (fgetline(fi, line, false)) { + linenum++; + std::vector fieldList = split(line, '|'); + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + int fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if (lineType == unknownLineType) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + switch (lineType) { + case labelLineType: + CORE_DUMP; + break; + case dataLineType: + front = fieldList[0]; + + /******************************************************************/ + /* United States Data (US) */ + /******************************************************************/ + if (front == "US:HD") { + readIndividualHeaderUS(fieldList); + } else if (front == "US:PA") { + readIndividualPathUS(fieldList); + } else if (front == "US:AN") { + readIndividualAntennaUS(fieldList, fwarn); + } else if (front == "US:FR") { + readIndividualFrequencyUS(fieldList, fwarn); + } else if (front == "US:LO") { + readIndividualLocationUS(fieldList, + alignFederatedFlag, + alignFederatedScale); + } else if (front == "US:EM") { + readIndividualEmissionUS(fieldList, fwarn); + } else if (front == "US:EN") { + readIndividualEntityUS(fieldList); + } else if (front == "US:MF") { + readIndividualMarketFrequencyUS(fieldList); + } else if (front == "US:CP") { + readIndividualControlPointUS(fieldList); + } else if (front == "US:SG") { + readIndividualSegmentUS(fieldList); + } else if (front == "US:RA") { + readIndividualRASUS(fieldList); + /******************************************************************/ + + /******************************************************************/ + /* Canada Data (CA) */ + /******************************************************************/ + } else if (front == "CA:SD") { + readStationDataCA(fieldList, + fwarn, + alignFederatedFlag, + alignFederatedScale); + } else if (front == "CA:PP") { + readBackToBackPassiveRepeaterCA(fieldList, fwarn); + } else if (front == "CA:PR") { + readReflectorPassiveRepeaterCA(fieldList, fwarn); + } else if (front == "CA:AP") { + // SetToNextLine(fi, fgetc(fi)); + } else if (front == "CA:TA") { + readTransmitterCA(fieldList, fwarn); + /******************************************************************/ + + } else { + errStr << std::string("ERROR: Unable to process inputFile " + "line ") + << linenum << ", unrecognized: \"" << front << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + + break; + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + CORE_DUMP; + break; + } + /**************************************************************************/ + } + + /**************************************************************************/ + /* Create list of authorizationNumbers */ + /**************************************************************************/ + foreach(const StationDataCAClass &station, stations()) + { + authorizationNumberList.insert(station.authorizationNumber); + } + + std::cout << "CA: Total " << authorizationNumberList.size() << " authorization numbers" + << std::endl; + /**************************************************************************/ + + fclose(fi); +} +/**************************************************************************/ + +inline bool isInvalidChar(char c) +{ + bool isComma = (c == ','); + bool isInvalidAscii = (c & 0x80 ? true : false); + return (isComma || isInvalidAscii); +} + +/**************************************************************************/ +/* UlsFileReader::readIndividualPathUS() */ +/**************************************************************************/ +void UlsFileReader::readIndividualPathUS(const std::vector &fieldList) +{ + UlsPath current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 1: + current.systemId = atoll(field.c_str()); + break; + case 4: + strlcpy(current.callsign, field.c_str(), 11); + break; + case 6: + current.pathNumber = atoi(field.c_str()); + break; + case 7: + current.txLocationNumber = atoi(field.c_str()); + break; + case 8: + current.txAntennaNumber = atoi(field.c_str()); + break; + case 9: + current.rxLocationNumber = atoi(field.c_str()); + break; + case 10: + current.rxAntennaNumber = atoi(field.c_str()); + break; + case 12: + strlcpy(current.pathType, field.c_str(), 21); + break; + case 13: + current.passiveReceiver = field.c_str()[0]; + break; + case 14: + strlcpy(current.countryCode, field.c_str(), 4); + break; + case 15: + current.GSOinterference = field.c_str()[0]; + break; + case 16: + strlcpy(current.rxCallsign, field.c_str(), 11); + break; + case 17: + current.angularSeparation = emptyAtof(field.c_str()); + break; + case 20: + current.statusCode = field.c_str()[0]; + break; + case 21: + strlcpy(current.statusDate, field.c_str(), 11); + break; + } + } + + if (fixMissingRxCallsign) { + bool replace = false; + if (strlen(current.rxCallsign) == 0) { + replace = true; + } else { + char buf[11]; + unsigned int i; + + for (i = 0; i < strlen(current.rxCallsign); i++) { + buf[i] = toupper(current.rxCallsign[i]); + } + buf[i] = '\0'; + + if (strstr(current.rxCallsign, "NEW") != NULL) { + replace = true; + } else { + bool empty = true; + for (i = 0; i < strlen(buf); i++) { + if (isspace(buf[i]) == 0) + empty = false; + } + if (empty == true) { + replace = true; + } + } + } + if (replace == true) { + strlcpy(current.rxCallsign, current.callsign, sizeof(current.rxCallsign)); + } + } + + allPaths << current; + pathMap[current.callsign] << current; + return; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFileReader::readIndividualEmissionUS() */ +/**************************************************************************/ +void UlsFileReader::readIndividualEmissionUS(const std::vector &fieldList, FILE *fwarn) +{ + UlsEmission current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 1: + current.systemId = atoll(field.c_str()); + break; + case 4: + strlcpy(current.callsign, field.c_str(), 11); + break; + case 5: + current.locationId = atoi(field.c_str()); + break; + case 6: + current.antennaId = atoi(field.c_str()); + break; + case 7: + current.frequency = emptyAtof(field.c_str()); + break; + case 9: + strlcpy(current.desig, field.c_str(), 11); + break; + case 10: + current.modRate = emptyAtof(field.c_str()); + break; + case 11: + current.modCode = field; + break; + case 12: + current.frequencyId = atoi(field.c_str()); + break; + } + } + + std::string origModCode = current.modCode; + std::string modCode = origModCode; + + modCode.erase(std::remove_if(modCode.begin(), modCode.end(), isInvalidChar), modCode.end()); + + if (modCode != origModCode) { + if (fwarn) { + fprintf(fwarn, "WARNING: Mod Code \""); + for (int i = 0; i < (int)origModCode.length(); ++i) { + char ch = origModCode[i]; + if (isInvalidChar(ch)) { + fprintf(fwarn, "\\x%2X", (unsigned char)ch); + } else { + fprintf(fwarn, "%c", ch); + } + } + fprintf(fwarn, + "\" contains invalid characters, replaced with \"%s\"\n", + modCode.c_str()); + } + current.modCode = modCode; + } + + allEmissions << current; + emissionMap[current.callsign] << current; + + return; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFileReader::readIndividualMarketFrequencyUS() */ +/**************************************************************************/ +void UlsFileReader::readIndividualMarketFrequencyUS(const std::vector &fieldList) +{ + UlsMarketFrequency current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 1: + current.systemId = atoll(field.c_str()); + break; + case 4: + strlcpy(current.callsign, field.c_str(), 11); + break; + case 5: + strlcpy(current.partitionSeq, field.c_str(), 7); + break; + case 6: + current.lowerFreq = emptyAtof(field.c_str()); + break; + case 7: + current.upperFreq = emptyAtof(field.c_str()); + break; + } + } + + allMarketFrequencies << current; + + return; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFileReader::readIndividualEntityUS() */ +/**************************************************************************/ +void UlsFileReader::readIndividualEntityUS(const std::vector &fieldList) +{ + UlsEntity current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 1: + current.systemId = atoll(field.c_str()); + break; + case 4: + strlcpy(current.callsign, field.c_str(), 11); + break; + case 5: + strlcpy(current.entityType, field.c_str(), 3); + break; + case 6: + strlcpy(current.licenseeId, field.c_str(), 10); + break; + case 7: + strlcpy(current.entityName, field.c_str(), 201); + break; + case 22: + strlcpy(current.frn, field.c_str(), 11); + break; + } + } + + allEntities << current; + entityMap[current.callsign] << current; + + return; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFileReader::readIndividualLocationUS() */ +/**************************************************************************/ +void UlsFileReader::readIndividualLocationUS(const std::vector &fieldList, + bool alignFederatedFlag, + double alignFederatedScale) +{ + UlsLocation current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 1: + current.systemId = atoll(field.c_str()); + break; + case 4: + strlcpy(current.callsign, field.c_str(), 11); + break; + + case 5: + current.locationAction = field.c_str()[0]; + break; + case 6: + current.locationType = field.c_str()[0]; + break; + case 7: + current.locationClass = field.c_str()[0]; + break; + case 8: + current.locationNumber = atoi(field.c_str()); + break; + case 9: + current.siteStatus = field.c_str()[0]; + break; + case 10: + current.correspondingFixedLocation = atoi(field.c_str()); + break; + case 11: + strlcpy(current.locationAddress, field.c_str(), 81); + break; + case 12: + strlcpy(current.locationCity, field.c_str(), 21); + break; + case 13: + strlcpy(current.locationCounty, field.c_str(), 61); + break; + case 14: + strlcpy(current.locationState, field.c_str(), 3); + break; + case 15: + current.radius = emptyAtof(field.c_str()); + break; + case 16: + current.areaOperationCode = field.c_str()[0]; + break; + case 17: + current.clearanceIndication = field.c_str()[0]; + break; + case 18: + current.groundElevation = emptyAtof(field.c_str()); + break; + case 19: + current.latitude = atoi(field.c_str()); + current.latitudeDeg = atoi(field.c_str()); + break; + case 20: + current.latitude = current.latitude + atoi(field.c_str()) / 60.0; + current.latitudeMinutes = atoi(field.c_str()); + break; + case 21: + current.latitude = current.latitude + + emptyAtof(field.c_str()) / 3600.0; + current.latitudeSeconds = emptyAtof(field.c_str()); + break; + case 22: + if (field.c_str()[0] == 'S') { + current.latitude = current.latitude * -1; + } + current.latitudeDirection = field.c_str()[0]; + break; + case 23: + current.longitude = emptyAtof(field.c_str()); + current.longitudeDeg = atoi(field.c_str()); + break; + case 24: + current.longitude = current.longitude + + emptyAtof(field.c_str()) / 60.0; + current.longitudeMinutes = atoi(field.c_str()); + break; + case 25: + current.longitude = current.longitude + + emptyAtof(field.c_str()) / 3600.0; + current.longitudeSeconds = atoi(field.c_str()); + break; + case 26: + if (field.c_str()[0] == 'W') { + current.longitude = current.longitude * -1; + } + current.longitudeDirection = field.c_str()[0]; + break; + case 35: + current.nepa = field.c_str()[0]; + break; + case 38: + current.supportHeight = emptyAtof(field.c_str()); + break; + case 39: + current.overallHeight = emptyAtof(field.c_str()); + break; + case 40: + strlcpy(current.structureType, field.c_str(), 7); + break; + case 41: + strlcpy(current.airportId, field.c_str(), 5); + break; + case 42: + strlcpy(current.locationName, field.c_str(), 21); + break; + case 48: + current.statusCode = field.c_str()[0]; + break; + case 49: + strlcpy(current.statusDate, field.c_str(), 11); + break; + case 50: + current.earthStationAgreement = field.c_str()[0]; + break; + } + } + + if (alignFederatedFlag) { + current.longitude = std::floor(current.longitude * alignFederatedScale + 0.5) / + alignFederatedScale; + current.latitude = std::floor(current.latitude * alignFederatedScale + 0.5) / + alignFederatedScale; + } + + allLocations << current; + locationMap[current.callsign] << current; + + return; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFileReader::readIndividualAntennaUS() */ +/**************************************************************************/ +void UlsFileReader::readIndividualAntennaUS(const std::vector &fieldList, FILE *fwarn) +{ + UlsAntenna current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 1: + current.systemId = atoll(field.c_str()); + break; + case 4: + strlcpy(current.callsign, field.c_str(), 11); + break; + + case 6: + current.antennaNumber = atoi(field.c_str()); + break; + case 7: + current.locationNumber = atoi(field.c_str()); + break; + case 8: + strlcpy(current.recvZoneCode, field.c_str(), 6); + break; + case 9: + current.antennaType = field.c_str()[0]; + break; + case 10: + current.heightToTip = emptyAtof(field.c_str()); + break; + case 11: + current.heightToCenterRAAT = emptyAtof(field.c_str()); + break; + case 12: + current.antennaMake = field; + break; + case 13: + current.antennaModel = field; + break; + case 14: + current.tilt = emptyAtof(field.c_str()); + break; + case 15: + strlcpy(current.polarizationCode, field.c_str(), 5); + break; + case 16: + current.beamwidth = emptyAtof(field.c_str()); + break; + case 17: + current.gain = emptyAtof(field.c_str()); + break; + case 18: + current.azimuth = emptyAtof(field.c_str()); + break; + case 19: + current.heightAboveAverageTerrain = emptyAtof(field.c_str()); + break; + case 20: + current.diversityHeight = emptyAtof(field.c_str()); + break; + case 21: + current.diversityGain = emptyAtof(field.c_str()); + break; + case 22: + current.diversityBeam = emptyAtof(field.c_str()); + break; + case 23: + current.reflectorHeight = emptyAtof(field.c_str()); + break; + case 24: + current.reflectorWidth = emptyAtof(field.c_str()); + break; + case 25: + current.reflectorSeparation = emptyAtof(field.c_str()); + break; + case 26: + current.passiveRepeaterNumber = atoi(field.c_str()); + break; + case 27: + current.backtobackTxGain = emptyAtof(field.c_str()); + break; + case 28: + current.backtobackRxGain = emptyAtof(field.c_str()); + break; + case 29: + strlcpy(current.locationName, field.c_str(), 20); + break; + case 30: + current.passiveRepeaterSequenceId = atoi(field.c_str()); + break; + case 31: + current.alternativeCGSA = field.c_str()[0]; + break; + case 32: + current.pathNumber = atoi(field.c_str()); + break; + case 33: + current.lineLoss = emptyAtof(field.c_str()); + break; + + case 34: + current.statusCode = field.c_str()[0]; + break; + case 35: + strlcpy(current.statusDate, field.c_str(), 11); + break; + } + } + + std::string origAntennaModel = current.antennaModel; + std::string antennaModel = origAntennaModel; + + antennaModel.erase(std::remove_if(antennaModel.begin(), antennaModel.end(), isInvalidChar), + antennaModel.end()); + + if (antennaModel != origAntennaModel) { + if (fwarn) { + fprintf(fwarn, "WARNING: Antenna model \""); + for (int i = 0; i < (int)origAntennaModel.length(); ++i) { + char ch = origAntennaModel[i]; + if (isInvalidChar(ch)) { + fprintf(fwarn, "\\x%2X", (unsigned char)ch); + } else { + fprintf(fwarn, "%c", ch); + } + } + fprintf(fwarn, + "\" contains invalid characters, replaced with \"%s\"\n", + antennaModel.c_str()); + } + current.antennaModel = antennaModel; + } + + allAntennas << current; + antennaMap[current.callsign] << current; + + return; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFileReader::readIndividualFrequencyUS() */ +/**************************************************************************/ +void UlsFileReader::readIndividualFrequencyUS(const std::vector &fieldList, + FILE *fwarn) +{ + UlsFrequency current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 1: + current.systemId = atoll(field.c_str()); + break; + case 4: + strlcpy(current.callsign, field.c_str(), 11); + break; + + case 6: + current.locationNumber = atoi(field.c_str()); + break; + case 7: + current.antennaNumber = atoi(field.c_str()); + break; + + case 8: + strlcpy(current.classStationCode, field.c_str(), 5); + break; + case 9: + strlcpy(current.opAltitudeCode, field.c_str(), 3); + break; + case 10: + current.frequencyAssigned = emptyAtof(field.c_str()); + break; + case 11: + current.frequencyUpperBand = emptyAtof(field.c_str()); + break; + case 12: + current.frequencyCarrier = emptyAtof(field.c_str()); + break; + case 13: + current.timeBeginOperations = atoi(field.c_str()); + break; + case 14: + current.timeEndOperations = atoi(field.c_str()); + break; + case 15: + current.powerOutput = emptyAtof(field.c_str()); + break; + case 16: + current.powerERP = emptyAtof(field.c_str()); + break; + case 17: + current.tolerance = emptyAtof(field.c_str()); + break; + case 18: + current.frequencyIndicator = field.c_str()[0]; + break; + case 19: + current.status = field.c_str()[0]; + break; + case 20: + current.EIRP = emptyAtof(field.c_str()); + break; + case 21: + strlcpy(current.transmitterMake, field.c_str(), 26); + break; + case 22: + strlcpy(current.transmitterModel, field.c_str(), 26); + break; + case 23: + current.transmitterPowerControl = field.c_str()[0]; + break; + case 24: + current.numberUnits = atoi(field.c_str()); + break; + case 25: + current.numberReceivers = atoi(field.c_str()); + break; + case 26: + current.frequencyNumber = atoi(field.c_str()); + break; + + case 27: + current.statusCode = field.c_str()[0]; + break; + case 28: + strlcpy(current.statusDate, field.c_str(), 11); + break; + } + } + + std::string origTransmitterModel = std::string(current.transmitterModel); + std::string transmitterModel = origTransmitterModel; + + transmitterModel.erase(std::remove_if(transmitterModel.begin(), + transmitterModel.end(), + isInvalidChar), + transmitterModel.end()); + + if (transmitterModel != origTransmitterModel) { + if (fwarn) { + fprintf(fwarn, "WARNING: Transmitter model \""); + for (int i = 0; i < (int)origTransmitterModel.length(); ++i) { + char ch = origTransmitterModel[i]; + if (isInvalidChar(ch)) { + fprintf(fwarn, "\\x%2X", (unsigned char)ch); + } else { + fprintf(fwarn, "%c", ch); + } + } + fprintf(fwarn, + "\" contains invalid characters, replaced with \"%s\"\n", + transmitterModel.c_str()); + } + strlcpy(current.transmitterModel, transmitterModel.c_str(), 26); + } + + // allFrequencies should now contain all the antenna records in the original + // DB. + allFrequencies << current; + + return; +} + +/**************************************************************************/ +/* UlsFileReader::readIndividualHeaderUS() */ +/**************************************************************************/ +void UlsFileReader::readIndividualHeaderUS(const std::vector &fieldList) +{ + UlsHeader current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 1: + current.systemId = atoll(field.c_str()); + break; + case 4: + strlcpy(current.callsign, field.c_str(), 11); + break; + case 5: + current.licenseStatus = field.c_str()[0]; + break; + case 6: + strlcpy(current.radioServiceCode, field.c_str(), 3); + break; + case 7: + strlcpy(current.grantDate, field.c_str(), 14); + break; + case 8: + strlcpy(current.expiredDate, field.c_str(), 14); + break; + case 21: + current.commonCarrier = field.c_str()[0]; + break; + case 22: + current.nonCommonCarrier = field.c_str()[0]; + break; + case 23: + current.privateCarrier = field.c_str()[0]; + break; + case 24: + current.fixed = field.c_str()[0]; + break; + case 25: + current.mobile = field.c_str()[0]; + break; + case 26: + current.radiolocation = field.c_str()[0]; + break; + case 27: + current.satellite = field.c_str()[0]; + break; + case 28: + current.developmental = field.c_str()[0]; + break; + case 29: + current.interconnected = field.c_str()[0]; + break; + case 42: + strlcpy(current.effectiveDate, field.c_str(), 14); + break; + } + } + + // allHeaders should now contain all the header records in the original DB. + allHeaders << current; + headerMap[current.callsign] << current; + + return; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFileReader::readIndividualControlPointUS() */ +/**************************************************************************/ +void UlsFileReader::readIndividualControlPointUS(const std::vector &fieldList) +{ + UlsControlPoint current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 1: + current.systemId = atoll(field.c_str()); + break; + case 4: + strlcpy(current.callsign, field.c_str(), 11); + break; + case 5: + current.controlPointActionPerformed = field.c_str()[0]; + break; + case 6: + current.controlPointNumber = atoi(field.c_str()); + break; + case 7: + strlcpy(current.controlPointAddress, field.c_str(), 81); + break; + case 8: + strlcpy(current.controlPointCity, field.c_str(), 21); + break; + case 9: + strlcpy(current.controlPointState, field.c_str(), 3); + break; + case 10: + strlcpy(current.controlPointPhone, field.c_str(), 11); + break; + case 11: + strlcpy(current.controlPointCounty, field.c_str(), 61); + break; + case 12: + strlcpy(current.controlPointStatus, field.c_str(), 1); + break; + case 13: + strlcpy(current.controlPointStatusDate, field.c_str(), 14); + break; + } + } + + allControlPoints << current; + controlPointMap[current.callsign] << current; + + return; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFileReader::readIndividualSegmentUS() */ +/**************************************************************************/ +void UlsFileReader::readIndividualSegmentUS(const std::vector &fieldList) +{ + UlsSegment current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 1: + current.systemId = atoll(field.c_str()); + break; + case 4: + strlcpy(current.callsign, field.c_str(), 11); + break; + case 6: + current.pathNumber = atoi(field.c_str()); + break; + case 7: + current.txLocationId = atoi(field.c_str()); + break; + case 8: + current.txAntennaId = atoi(field.c_str()); + break; + case 9: + current.rxLocationId = atoi(field.c_str()); + break; + case 10: + current.rxAntennaId = atoi(field.c_str()); + break; + case 11: + current.segmentNumber = atoi(field.c_str()); + break; + case 12: + current.segmentLength = emptyAtof(field.c_str()); + break; + } + } + + allSegments << current; + segmentMap[current.callsign] << current; + + return; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFileReader::readIndividualRASUS() */ +/**************************************************************************/ +void UlsFileReader::readIndividualRASUS(const std::vector &fieldList) +{ + RASClass current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 1: + current.name = field; + break; + case 2: + current.location = field; + break; + case 3: + current.startFreqMHz = emptyAtof(field.c_str()); + break; + case 4: + current.stopFreqMHz = emptyAtof(field.c_str()); + break; + case 5: + current.exclusionZone = field; + break; + case 6: + current.rect1lat1 = UlsFunctionsClass::getAngleFromDMS( + field.c_str()); + break; + case 7: + current.rect1lat2 = UlsFunctionsClass::getAngleFromDMS( + field.c_str()); + break; + case 8: + current.rect1lon1 = UlsFunctionsClass::getAngleFromDMS( + field.c_str()); + break; + case 9: + current.rect1lon2 = UlsFunctionsClass::getAngleFromDMS( + field.c_str()); + break; + case 10: + current.rect2lat1 = UlsFunctionsClass::getAngleFromDMS( + field.c_str()); + break; + case 11: + current.rect2lat2 = UlsFunctionsClass::getAngleFromDMS( + field.c_str()); + break; + case 12: + current.rect2lon1 = UlsFunctionsClass::getAngleFromDMS( + field.c_str()); + break; + case 13: + current.rect2lon2 = UlsFunctionsClass::getAngleFromDMS( + field.c_str()); + break; + + case 14: + current.radiusKm = emptyAtof(field.c_str()); + break; + case 15: + current.centerLat = UlsFunctionsClass::getAngleFromDMS( + field.c_str()); + break; + case 16: + current.centerLon = UlsFunctionsClass::getAngleFromDMS( + field.c_str()); + break; + case 17: + current.heightAGL = emptyAtof(field.c_str()); + break; + } + } + + current.region = "US"; + + RASList << current; + + return; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFileReader::readStationDataCA() */ +/**************************************************************************/ +void UlsFileReader::readStationDataCA(const std::vector &fieldList, + FILE *fwarn, + bool alignFederatedFlag, + double alignFederatedScale) +{ + StationDataCAClass current; + + std::string linceseeName; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 1: + current.service = atoi(field.c_str()); + break; + case 2: + current.subService = atoi(field.c_str()); + break; + case 3: + current.authorizationNumber = field; + break; + case 4: + linceseeName = field; + break; + case 6: + current.callsign = field; + break; + case 7: + current.stationLocation = field; + break; + case 9: + current.latitudeDeg = emptyAtof(field.c_str()); + break; + case 10: + current.longitudeDeg = emptyAtof(field.c_str()); + break; + case 11: + current.groundElevation = emptyAtof(field.c_str()); + break; + case 13: + current.antennaHeightAGL = emptyAtof(field.c_str()); + break; + case 17: + current.emissionsDesignator = field; + break; + case 18: + current.bandwidthMHz = emptyAtof(field.c_str()) / 1000.0; + break; + case 19: + current.centerFreqMHz = emptyAtof(field.c_str()); + break; + case 20: + current.antennaGain = emptyAtof(field.c_str()); + break; + case 21: + current.lineLoss = emptyAtof(field.c_str()); + break; + case 23: + current.antennaManufacturer = field; + break; + case 24: + current.antennaModel = field; + break; + case 25: + current.inServiceDate = field; + break; + case 26: + current.modulation = field; + break; + case 14: + current.azimuthPtg = emptyAtof(field.c_str()); + break; + case 15: + current.elevationPtg = emptyAtof(field.c_str()); + break; + } + } + + if (alignFederatedFlag) { + current.longitudeDeg = std::floor(current.longitudeDeg * alignFederatedScale + + 0.5) / + alignFederatedScale; + current.latitudeDeg = std::floor(current.latitudeDeg * alignFederatedScale + 0.5) / + alignFederatedScale; + } + + if (isnan(current.antennaHeightAGL)) { + current.antennaHeightAGL = 56.0; + } else if (current.antennaHeightAGL < 1.5) { + current.antennaHeightAGL = 1.5; + } + + double heightAMSL_km = (current.groundElevation + current.antennaHeightAGL) / 1000.0; + current.position = EcefModel::geodeticToEcef(current.latitudeDeg, + current.longitudeDeg, + heightAMSL_km); + + current.pointingVec = UlsFunctionsClass::computeHPointingVec(current.position, + current.azimuthPtg, + current.elevationPtg); + + std::string origAntennaModel = std::string(current.antennaModel); + std::string antennaModel = origAntennaModel; + + antennaModel.erase(std::remove_if(antennaModel.begin(), antennaModel.end(), isInvalidChar), + antennaModel.end()); + + if (antennaModel != origAntennaModel) { + if (fwarn) { + fprintf(fwarn, "WARNING: Antenna model \""); + for (int i = 0; i < (int)origAntennaModel.length(); ++i) { + char ch = origAntennaModel[i]; + if (isInvalidChar(ch)) { + fprintf(fwarn, "\\x%2X", (unsigned char)ch); + } else { + fprintf(fwarn, "%c", ch); + } + } + fprintf(fwarn, + "\" contains invalid characters, replaced with \"%s\"\n", + antennaModel.c_str()); + } + current.antennaModel = antennaModel; + } + + // R1-AIP-19-CAN + if ((isnan(current.bandwidthMHz)) || (current.bandwidthMHz == 0.0)) { + current.bandwidthMHz = UlsFunctionsClass::emissionDesignatorToBandwidth( + QString::fromStdString(current.emissionsDesignator)); + } + + if (isnan(current.bandwidthMHz)) { + if (current.centerFreqMHz < 5925.0) { + // Do nothing + } else if (current.centerFreqMHz < 5955.0) { + current.bandwidthMHz = 2 * (current.centerFreqMHz - 5925.0); + } else if (current.centerFreqMHz < 6395.0) { + current.bandwidthMHz = 60; + } else if (current.centerFreqMHz < 6425.0) { + current.bandwidthMHz = 2 * (6425.0 - current.centerFreqMHz); + } else if (current.centerFreqMHz < 6440.0) { + current.bandwidthMHz = 2 * (current.centerFreqMHz - 6425.0); + } else if (current.centerFreqMHz < 6860.0) { + current.bandwidthMHz = 30; + } else if (current.centerFreqMHz < 6875.0) { + current.bandwidthMHz = 2 * (6875.0 - current.centerFreqMHz); + } + } + + if (current.service == 9) { + RASClass ras; + ras.region = "CA"; + ras.name = linceseeName; + ras.location = current.stationLocation; + ras.startFreqMHz = current.centerFreqMHz - current.bandwidthMHz / 2; + ras.stopFreqMHz = current.centerFreqMHz + current.bandwidthMHz / 2; + ras.exclusionZone = "Horizon Distance"; + ras.rect1lat1 = std::numeric_limits::quiet_NaN(); + ras.rect1lat2 = std::numeric_limits::quiet_NaN(); + ras.rect1lon1 = std::numeric_limits::quiet_NaN(); + ras.rect1lon2 = std::numeric_limits::quiet_NaN(); + ras.rect2lat1 = std::numeric_limits::quiet_NaN(); + ras.rect2lat2 = std::numeric_limits::quiet_NaN(); + ras.rect2lon1 = std::numeric_limits::quiet_NaN(); + ras.rect2lon2 = std::numeric_limits::quiet_NaN(); + ras.radiusKm = std::numeric_limits::quiet_NaN(); + ras.centerLat = current.latitudeDeg; + ras.centerLon = current.longitudeDeg; + ras.heightAGL = current.antennaHeightAGL; + RASList << ras; + } else { + allStations << current; + stationMap[current.authorizationNumber.c_str()] << current; + } + + return; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFileReader::readBackToBackPassiveRepeaterCA() */ +/**************************************************************************/ +void UlsFileReader::readBackToBackPassiveRepeaterCA(const std::vector &fieldList, + FILE *fwarn) +{ + BackToBackPassiveRepeaterCAClass current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 2: + current.authorizationNumber = field; + break; + case 6: + current.latitudeDeg = emptyAtof(field.c_str()); + break; + case 7: + current.longitudeDeg = emptyAtof(field.c_str()); + break; + case 8: + current.groundElevation = emptyAtof(field.c_str()); + break; + case 9: + current.heightAGL = emptyAtof(field.c_str()); + break; + case 10: + current.antennaGain = emptyAtof(field.c_str()); + break; + case 11: + current.antennaModel = field; + break; + case 3: + current.azimuthPtg = emptyAtof(field.c_str()); + break; + case 4: + current.elevationPtg = emptyAtof(field.c_str()); + break; + } + } + + std::string origAntennaModel = std::string(current.antennaModel); + std::string antennaModel = origAntennaModel; + + antennaModel.erase(std::remove_if(antennaModel.begin(), antennaModel.end(), isInvalidChar), + antennaModel.end()); + + if (antennaModel != origAntennaModel) { + if (fwarn) { + fprintf(fwarn, "WARNING: Antenna model \""); + for (int i = 0; i < (int)origAntennaModel.length(); ++i) { + char ch = origAntennaModel[i]; + if (isInvalidChar(ch)) { + fprintf(fwarn, "\\x%2X", (unsigned char)ch); + } else { + fprintf(fwarn, "%c", ch); + } + } + fprintf(fwarn, + "\" contains invalid characters, replaced with \"%s\"\n", + antennaModel.c_str()); + } + current.antennaModel = antennaModel; + } + + allBackToBackPassiveRepeaters << current; + backToBackPassiveRepeaterMap[current.authorizationNumber.c_str()] << current; + return; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFileReader::readReflectorPassiveRepeaterCA() */ +/**************************************************************************/ +void UlsFileReader::readReflectorPassiveRepeaterCA(const std::vector &fieldList, + FILE * /* fwarn */) +{ + ReflectorPassiveRepeaterCAClass current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 2: + current.authorizationNumber = field; + break; + case 4: + current.latitudeDeg = emptyAtof(field.c_str()); + break; + case 5: + current.longitudeDeg = emptyAtof(field.c_str()); + break; + case 6: + current.groundElevation = emptyAtof(field.c_str()); + break; + case 7: + current.heightAGL = emptyAtof(field.c_str()); + break; + case 8: + current.azimuthPtg = emptyAtof(field.c_str()); + break; + case 9: + current.elevationPtg = emptyAtof(field.c_str()); + break; + case 10: + current.reflectorHeight = emptyAtof(field.c_str()); + break; + case 11: + current.reflectorWidth = emptyAtof(field.c_str()); + break; + } + } + + if (isnan(current.reflectorHeight) || isnan(current.reflectorWidth)) { + current.reflectorHeight = 7.32; + current.reflectorWidth = 9.14; + } + + allReflectorPassiveRepeaters << current; + reflectorPassiveRepeaterMap[current.authorizationNumber.c_str()] << current; + + return; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFileReader::readTransmitterCA() */ +/**************************************************************************/ +void UlsFileReader::readTransmitterCA(const std::vector &fieldList, FILE *fwarn) +{ + TransmitterCAClass current; + + for (int fieldIdx = 0; fieldIdx < (int)fieldList.size(); ++fieldIdx) { + std::string field = fieldList[fieldIdx]; + + switch (fieldIdx) { + case 49: + current.service = atoi(field.c_str()); + break; + case 50: + current.subService = atoi(field.c_str()); + break; + case 48: + current.authorizationNumber = field; + break; + case 34: + current.callsign = field; + break; + case 41: + current.latitudeDeg = emptyAtof(field.c_str()); + break; + case 42: + current.longitudeDeg = emptyAtof(field.c_str()); + break; + case 43: + current.groundElevation = emptyAtof(field.c_str()); + break; + case 29: + current.antennaHeightAGL = emptyAtof(field.c_str()); + break; +#if 0 +// Not implementing at this time. Ideally ISED would make these fields available in the StationData file. + case 12: + current.emissionsDesignator = field; + break; + case 11: + current.bandwidthMHz = emptyAtof(field.c_str())/1000.0; + break; + case 2: + current.centerFreqMHz = emptyAtof(field.c_str()); + break; + case 24: + current.antennaGain = emptyAtof(field.c_str()); + break; + case 23: + current.antennaModel = field; + break; + case 13: + current.modulation = field; + break; + case 19: + current.modRate = emptyAtof(field.c_str()); + break; +#endif + } + } + + std::string origAntennaModel = std::string(current.antennaModel); + std::string antennaModel = origAntennaModel; + + antennaModel.erase(std::remove_if(antennaModel.begin(), antennaModel.end(), isInvalidChar), + antennaModel.end()); + + if (antennaModel != origAntennaModel) { + if (fwarn) { + fprintf(fwarn, "WARNING: Antenna model \""); + for (int i = 0; i < (int)origAntennaModel.length(); ++i) { + char ch = origAntennaModel[i]; + if (isInvalidChar(ch)) { + fprintf(fwarn, "\\x%2X", (unsigned char)ch); + } else { + fprintf(fwarn, "%c", ch); + } + } + fprintf(fwarn, + "\" contains invalid characters, replaced with \"%s\"\n", + antennaModel.c_str()); + } + current.antennaModel = antennaModel; + } + + allTransmitters << current; + transmitterMap[current.authorizationNumber.c_str()] << current; + return; +} +/**************************************************************************/ + +/******************************************************************************************/ +/**** computeStatisticsUS ****/ +/******************************************************************************************/ +int UlsFileReader::computeStatisticsUS(FreqAssignmentClass &freqAssignment, + bool includeUnii5, + bool includeUnii6, + bool includeUnii7, + bool includeUnii8) +{ + int n = 0; + int maxNumSegment; + std::string maxNumSegmentCallsign = ""; + + const std::vector bwMHzListUnii5 = + {0.4, 0.8, 1.25, 2.5, 3.75, 5.0, 10.0, 30.0, 60.0}; + + const std::vector bwMHzListUnii7 = {0.4, 0.8, 1.25, 2.5, 3.75, 5.0, 10.0, 30.0}; + + foreach(const UlsFrequency &freq, frequencies()) + { + UlsPath path; + bool pathFound = false; + + foreach(const UlsPath &p, pathsMap(freq.callsign)) + { + if (strcmp(p.callsign, freq.callsign) == 0) { + if (freq.locationNumber == p.txLocationNumber && + freq.antennaNumber == p.txAntennaNumber) { + path = p; + pathFound = true; + break; + } + } + } + + if (pathFound == false) { + continue; + } + + /// Find the emissions information. + bool txEmFound = false; + QList allTxEm; + foreach(const UlsEmission &e, emissionsMap(freq.callsign)) + { + if (strcmp(e.callsign, freq.callsign) == 0 && + e.locationId == freq.locationNumber && + e.antennaId == freq.antennaNumber && + e.frequencyId == freq.frequencyNumber) { + allTxEm << e; + txEmFound = true; + } + } + if (!txEmFound) { + UlsEmission txEm; + allTxEm << txEm; // Make sure at least one emission. + } + + /// Find the header. + UlsHeader txHeader; + bool txHeaderFound = false; + foreach(const UlsHeader &h, headersMap(path.callsign)) + { + if (strcmp(h.callsign, path.callsign) == 0) { + txHeader = h; + txHeaderFound = true; + break; + } + } + + if (!txHeaderFound) { + continue; + } else if (txHeader.licenseStatus != 'A' && txHeader.licenseStatus != 'L') { + continue; + } + + // std::cout << freq.callsign << ": " << allTxEm.size() << " emissions" << + // std::endl; + foreach(const UlsEmission &e, allTxEm) + { + bool invalidFlag = false; + double startFreq = std::numeric_limits::quiet_NaN(); + double stopFreq = std::numeric_limits::quiet_NaN(); + double startFreqBand = std::numeric_limits::quiet_NaN(); + double stopFreqBand = std::numeric_limits::quiet_NaN(); + double bwMHz = std::numeric_limits::quiet_NaN(); + + if (isnan(freq.frequencyAssigned)) { + invalidFlag = true; + } else { + if (txEmFound) { + bwMHz = UlsFunctionsClass::emissionDesignatorToBandwidth( + e.desig); + } + if (isnan(bwMHz) || (bwMHz > 60.0) || (bwMHz == 0)) { + bwMHz = freqAssignment.getBandwidthUS( + freq.frequencyAssigned); + } else { + bool unii5Flag = (freq.frequencyAssigned >= + UlsFunctionsClass::unii5StartFreqMHz) && + (freq.frequencyAssigned <= + UlsFunctionsClass::unii5StopFreqMHz); + bool unii7Flag = (freq.frequencyAssigned >= + UlsFunctionsClass::unii7StartFreqMHz) && + (freq.frequencyAssigned <= + UlsFunctionsClass::unii7StopFreqMHz); + const std::vector *fccBWList = + (std::vector *)NULL; + if (unii5Flag) { + fccBWList = &bwMHzListUnii5; + } else if (unii7Flag) { + fccBWList = &bwMHzListUnii7; + } + if (fccBWList) { + bool found = false; + double fccBW; + for (int i = 0; + (i < (int)fccBWList->size()) && (!found); + ++i) { + if (fccBWList->at(i) >= bwMHz) { + found = true; + fccBW = fccBWList->at(i); + } + } + if (found) { + bwMHz = std::min(fccBW, bwMHz * 1.1); + } + } + } + + if ((bwMHz == -1)) { + invalidFlag = true; + } else if (isnan(freq.frequencyUpperBand)) { + startFreq = freq.frequencyAssigned - + bwMHz / 2.0; // Lower Band (MHz) + stopFreq = freq.frequencyAssigned + + bwMHz / 2.0; // Upper Band (MHz) + startFreqBand = startFreq; + stopFreqBand = stopFreq; + } else { + startFreq = freq.frequencyAssigned - + bwMHz / 2.0; // Lower Band (MHz) + stopFreq = freq.frequencyAssigned + + bwMHz / 2.0; // Upper Band (MHz) + startFreqBand = freq.frequencyAssigned; // Lower Band (MHz) + stopFreqBand = startFreqBand + bwMHz; // Upper Band (MHz) + } + } + + if (!invalidFlag) { + // skip if no overlap UNII5 and 7 + bool overlapUnii5 = (stopFreqBand > + UlsFunctionsClass::unii5StartFreqMHz) && + (startFreqBand < + UlsFunctionsClass::unii5StopFreqMHz); + bool overlapUnii6 = (stopFreqBand > + UlsFunctionsClass::unii6StartFreqMHz) && + (startFreqBand < + UlsFunctionsClass::unii6StopFreqMHz); + bool overlapUnii7 = (stopFreqBand > + UlsFunctionsClass::unii7StartFreqMHz) && + (startFreqBand < + UlsFunctionsClass::unii7StopFreqMHz); + bool overlapUnii8 = (stopFreqBand > + UlsFunctionsClass::unii8StartFreqMHz) && + (startFreqBand < + UlsFunctionsClass::unii8StopFreqMHz); + + if (!((includeUnii5 && overlapUnii5) || + (includeUnii6 && overlapUnii6) || + (includeUnii7 && overlapUnii7) || + (includeUnii8 && overlapUnii8))) { + invalidFlag = true; + } + } + if (!invalidFlag) { + foreach(const UlsSegment &segment, segmentsMap(freq.callsign)) + { + int segmentNumber = segment.segmentNumber; + if ((n == 0) || (segmentNumber > maxNumSegment)) { + maxNumSegment = segmentNumber; + maxNumSegmentCallsign = std::string( + segment.callsign); + n++; + } + } + } + } + } + + int maxNumPassiveRepeater = (n ? maxNumSegment - 1 : 0); + + qDebug() << "DATA statistics:"; + qDebug() << "paths" << paths().count(); + qDebug() << "emissions" << emissions().count(); + qDebug() << "antennas" << antennas().count(); + qDebug() << "frequencies" << frequencies().count(); + qDebug() << "locations" << locations().count(); + qDebug() << "headers" << headers().count(); + qDebug() << "market freqs" << marketFrequencies().count(); + qDebug() << "entities" << entities().count(); + qDebug() << "control points" << controlPoints().count(); + qDebug() << "segments" << segments().count(); + qDebug() << "maxNumPassiveRepeater" << maxNumPassiveRepeater + << " callsign: " << QString::fromStdString(maxNumSegmentCallsign); + + return (maxNumPassiveRepeater); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** computeStatisticsCA ****/ +/******************************************************************************************/ +int UlsFileReader::computeStatisticsCA(FILE *fwarn) +{ + int i; + int maxNumPassiveRepeater = 0; + int numMatchedBackToBack = 0; + double epsLonLat = 1.0e-5; + double epsGroundElevation = 0.05; + + /**************************************************************************************/ + /* CA database contains 2 entries for each back to back passive repeater, 1 entry for */ + /* each antenna. Here entries are matched. Entries can be matched if they have the */ + /* same: */ + /* authorizationNumber */ + /* longitude */ + /* latitude */ + /* If entries don't match, there is an error in the database, report in warning file. */ + /**************************************************************************************/ + for (std::string authorizationNumber : authorizationNumberList) { + const QList &bbList = + backToBackPassiveRepeatersMap(authorizationNumber.c_str()); + std::vector idxList; + idxList.clear(); + for (i = 0; i < bbList.size(); ++i) { + idxList.push_back(i); + } + + while (idxList.size()) { + int iiA, iiMatch; + iiA = idxList.size() - 1; + const BackToBackPassiveRepeaterCAClass &bbA = bbList[idxList[iiA]]; + bool found = false; + for (int iiB = 0; (iiB < iiA) && (!found); ++iiB) { + const BackToBackPassiveRepeaterCAClass &bbB = bbList[idxList[iiB]]; + if ((fabs(bbA.longitudeDeg - bbB.longitudeDeg) < epsLonLat) && + (fabs(bbA.latitudeDeg - bbB.latitudeDeg) < epsLonLat) && + (fabs(bbA.groundElevation - bbB.groundElevation) < + epsGroundElevation)) { + found = true; + iiMatch = iiB; + } + } + if (found) { + const BackToBackPassiveRepeaterCAClass &bbB = + bbList[idxList[iiMatch]]; + PassiveRepeaterCAClass pr; + pr.type = PassiveRepeaterCAClass::backToBackAntennaPRType; + pr.authorizationNumber = authorizationNumber; + pr.latitudeDeg = bbA.latitudeDeg; + pr.longitudeDeg = bbA.longitudeDeg; + pr.groundElevation = bbA.groundElevation; + + pr.heightAGLA = bbA.heightAGL; + pr.heightAGLB = bbB.heightAGL; + pr.antennaGainA = bbA.antennaGain; + pr.antennaGainB = bbB.antennaGain; + pr.antennaModelA = bbA.antennaModel; + pr.antennaModelB = bbB.antennaModel; + pr.azimuthPtgA = bbA.azimuthPtg; + pr.azimuthPtgB = bbB.azimuthPtg; + pr.elevationPtgA = bbA.elevationPtg; + pr.elevationPtgB = bbA.elevationPtg; + + pr.positionA = EcefModel::geodeticToEcef( + pr.latitudeDeg, + pr.longitudeDeg, + (pr.groundElevation + pr.heightAGLA) / 1000.0); + pr.positionB = EcefModel::geodeticToEcef( + pr.latitudeDeg, + pr.longitudeDeg, + (pr.groundElevation + pr.heightAGLB) / 1000.0); + + pr.reflectorHeight = std::numeric_limits::quiet_NaN(); + pr.reflectorWidth = std::numeric_limits::quiet_NaN(); + + passiveRepeaterMap[authorizationNumber.c_str()] << pr; + if (iiMatch < iiA - 1) { + idxList[iiMatch] = idxList[iiA - 1]; + } + idxList.pop_back(); + idxList.pop_back(); + numMatchedBackToBack++; + } else { + fprintf(fwarn, + "UNMATCHED BACK-TO-BACK REPEATER: authorizationNumber: %s, " + "LON = %.6f, LAT = %.6f\n", + authorizationNumber.c_str(), + bbA.longitudeDeg, + bbA.latitudeDeg); + idxList.pop_back(); + } + } + + foreach(const ReflectorPassiveRepeaterCAClass &br, + reflectorPassiveRepeatersMap(authorizationNumber.c_str())) + { + PassiveRepeaterCAClass pr; + pr.type = PassiveRepeaterCAClass::billboardReflectorPRType; + pr.authorizationNumber = authorizationNumber; + pr.latitudeDeg = br.latitudeDeg; + pr.longitudeDeg = br.longitudeDeg; + pr.groundElevation = br.groundElevation; + + pr.reflectorHeight = br.reflectorHeight; + pr.reflectorWidth = br.reflectorWidth; + + pr.heightAGLA = br.heightAGL; + pr.heightAGLB = br.heightAGL; + pr.antennaGainA = std::numeric_limits::quiet_NaN(); + pr.antennaGainB = std::numeric_limits::quiet_NaN(); + pr.antennaModelA = ""; + pr.antennaModelB = ""; + pr.azimuthPtgA = std::numeric_limits::quiet_NaN(); + pr.azimuthPtgB = std::numeric_limits::quiet_NaN(); + pr.elevationPtgA = std::numeric_limits::quiet_NaN(); + pr.elevationPtgB = std::numeric_limits::quiet_NaN(); + + pr.reflectorPosition = EcefModel::geodeticToEcef( + pr.latitudeDeg, + pr.longitudeDeg, + (pr.groundElevation + pr.heightAGLA) / 1000.0); + + passiveRepeaterMap[authorizationNumber.c_str()] << pr; + } + + int numPR = passiveRepeaterMap[authorizationNumber.c_str()].size(); + + if (numPR > maxNumPassiveRepeater) { + maxNumPassiveRepeater = numPR; + } + } + /**************************************************************************************/ + + std::cout << "CA: Number of matched back-to-back passive repeaters: " + << numMatchedBackToBack << std::endl; + + return (maxNumPassiveRepeater); +} +/******************************************************************************************/ diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsFileReader.h b/src/coalition_ulsprocessor/src/uls-script/UlsFileReader.h new file mode 100644 index 0000000..fcb9f83 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsFileReader.h @@ -0,0 +1,238 @@ +#ifndef ULS_FILE_READER_H +#define ULS_FILE_READER_H + +#include +#include +#include + +#include "FreqAssignment.h" + +#include "UlsLocation.h" +#include "UlsAntenna.h" +#include "UlsHeader.h" +#include "UlsFrequency.h" +#include "UlsPath.h" +#include "UlsCallsign.h" +#include "UlsHop.h" +#include "UlsEmission.h" +#include "UlsEntity.h" +#include "UlsMarketFrequency.h" +#include "UlsControlPoint.h" +#include "UlsSegment.h" + +#include "StationDataCA.h" +#include "PassiveRepeaterCA.h" +#include "TransmitterCA.h" + +#include "RAS.h" + +#include +#include +#include + +class UlsFileReader +{ + public: + UlsFileReader(const char *filePath, + FILE *fwarn, + bool alignFederatedFlag, + double alignFederatedScale); + + const QList &paths() + { + return allPaths; + } + const QList &emissions() + { + return allEmissions; + } + const QList &antennas() + { + return allAntennas; + } + const QList &frequencies() + { + return allFrequencies; + } + const QList &locations() + { + return allLocations; + } + const QList &headers() + { + return allHeaders; + } + const QList &marketFrequencies() + { + return allMarketFrequencies; + } + const QList &entities() + { + return allEntities; + } + const QList &controlPoints() + { + return allControlPoints; + } + const QList &segments() + { + return allSegments; + } + + const QList &stations() + { + return allStations; + } + const QList &backToBackPassiveRepeaters() + { + return allBackToBackPassiveRepeaters; + } + const QList &reflectorPassiveRepeaters() + { + return allReflectorPassiveRepeaters; + } + const QList &transmitters() + { + return allTransmitters; + } + + const QList antennasMap(const QString &s) const + { + return antennaMap[s]; + } + + const QList segmentsMap(const QString &s) const + { + return segmentMap[s]; + } + + const QList locationsMap(const QString &s) const + { + return locationMap[s]; + } + + const QList emissionsMap(const QString &s) const + { + return emissionMap[s]; + } + + const QList pathsMap(const QString &s) const + { + return pathMap[s]; + } + + const QList entitiesMap(const QString &s) const + { + return entityMap[s]; + } + + const QList headersMap(const QString &s) const + { + return headerMap[s]; + } + + const QList controlPointsMap(const QString &s) const + { + return controlPointMap[s]; + } + + const QList stationsMap(const QString &s) const + { + return stationMap[s]; + } + + const QList backToBackPassiveRepeatersMap( + const QString &s) const + { + return backToBackPassiveRepeaterMap[s]; + } + + const QList reflectorPassiveRepeatersMap( + const QString &s) const + { + return reflectorPassiveRepeaterMap[s]; + } + + const QList passiveRepeatersMap(const QString &s) const + { + return passiveRepeaterMap[s]; + } + + const QList transmittersMap(const QString &s) const + { + return transmitterMap[s]; + } + + std::unordered_set authorizationNumberList; + + int computeStatisticsUS(FreqAssignmentClass &freqAssignment, + bool includeUnii5, + bool includeUnii6, + bool includeUnii7, + bool includeUnii8); + int computeStatisticsCA(FILE *fwarn); + + QList RASList; + + private: + void readIndividualHeaderUS(const std::vector &fieldList); + void readIndividualPathUS(const std::vector &fieldList); + void readIndividualAntennaUS(const std::vector &fieldList, + FILE *fwarn); + void readIndividualFrequencyUS(const std::vector &fieldList, + FILE *fwarn); + void readIndividualLocationUS(const std::vector &fieldList, + bool alignFederatedFlag, + double alignFederatedScale); + void readIndividualEmissionUS(const std::vector &fieldList, + FILE *fwarn); + void readIndividualEntityUS(const std::vector &fieldList); + void readIndividualMarketFrequencyUS(const std::vector &fieldList); + void readIndividualControlPointUS(const std::vector &fieldList); + void readIndividualSegmentUS(const std::vector &fieldList); + void readIndividualRASUS(const std::vector &fieldList); + + void readStationDataCA(const std::vector &fieldList, + FILE *fwarn, + bool alignFederatedFlag, + double alignFederatedScale); + void readBackToBackPassiveRepeaterCA(const std::vector &fieldList, + FILE *fwarn); + void readReflectorPassiveRepeaterCA(const std::vector &fieldList, + FILE *fwarn); + void readTransmitterCA(const std::vector &fieldList, FILE *fwarn); + + QList allPaths; + QList allEmissions; + QList allAntennas; + QList allFrequencies; + QList allLocations; + QList allHeaders; + QList allMarketFrequencies; + QList allEntities; + QList allControlPoints; + QList allSegments; + + QList allStations; + QList allBackToBackPassiveRepeaters; + QList allReflectorPassiveRepeaters; + QList allTransmitters; + + QHash> emissionMap; + QHash> antennaMap; + QHash> segmentMap; + QHash> locationMap; + QHash> pathMap; + QHash> entityMap; + QHash> controlPointMap; + QHash> headerMap; + + QHash> stationMap; + QHash> + backToBackPassiveRepeaterMap; + QHash> reflectorPassiveRepeaterMap; + QHash> passiveRepeaterMap; + QHash> transmitterMap; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsFrequency.h b/src/coalition_ulsprocessor/src/uls-script/UlsFrequency.h new file mode 100644 index 0000000..4068f80 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsFrequency.h @@ -0,0 +1,72 @@ +/* + * This data structure stores the data relevent for a frequency record. + * There is a field in this for each relevent column in the ULS database, regardless of whether + * or not it is populated. + * + */ + +#ifndef ULS_FREQUENCY_H +#define ULS_FREQUENCY_H + +class UlsFrequency +{ + public: + long long systemId; // unique identifier for this record; may not be necessary + char callsign[11]; // this is the 'key' for the owner of this antenna. + int locationNumber; // index among the corresponding UlsLocations + int antennaNumber; // index among the corresponding UlsAntenna + char classStationCode[5]; // unsure what this means + char opAltitudeCode[3]; + double frequencyAssigned; + double frequencyUpperBand; + double frequencyCarrier; + int timeBeginOperations; + int timeEndOperations; + double powerOutput; + double powerERP; + double tolerance; + char frequencyIndicator; + char status; + double EIRP; + char transmitterMake[26]; + char transmitterModel[26]; + char transmitterPowerControl; + int numberUnits; + int numberReceivers; + int frequencyNumber; + char statusCode; + char statusDate[11]; + int pathNumber; + + enum Parameters { + MinFrequencyParameter = 0x02000000, + FrequencySystemID = 0x02000001, + FrequencyCallsign = 0x02000002, + FrequencyLocationNumber = 0x02000003, + FrequencyAntennaNumber = 0x02000004, + FrequencyClassStationCode = 0x02000005, + FrequencyOpAltitudeCode = 0x02000006, + FrequencyAssigned = 0x02000007, + FrequencyUpperBand = 0x02000008, + FrequencyCarrier = 0x02000009, + FrequencyTimeBeginsOperations = 0x0200000a, + FrequencyTimeEndsOperations = 0x0200000b, + FrequencyPowerOutput = 0x0200000c, + FrequencyPowerERP = 0x0200000d, + FrequencyTolerance = 0x0200000e, + FrequencyIndicator = 0x0200000f, + FrequencyStatus = 0x02000010, + FrequencyEIRP = 0x02000011, + FrequencyTransmitterMake = 0x02000012, + FrequencyTransmitterModel = 0x02000013, + FrequencyPowerControl = 0x02000014, + FrequencyNumberUnits = 0x02000015, + FrequencyNumberReceivers = 0x02000016, + FrequencyNumber = 0x02000017, + FrequencyStatusCode = 0x02000018, + FrequencyStatusDate = 0x02000019, + MaxFrequencyParameter = 0x0200001a, + }; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsFunctions.cpp b/src/coalition_ulsprocessor/src/uls-script/UlsFunctions.cpp new file mode 100644 index 0000000..02328a3 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsFunctions.cpp @@ -0,0 +1,441 @@ +#include +#include +#include +#include +#include +#include + +#include "UlsFunctions.h" + +/******************************************************************************************/ +/**** Static Constants ****/ +/******************************************************************************************/ +const double UlsFunctionsClass::speedOfLight = 2.99792458e8; +const double UlsFunctionsClass::earthRadius = 6378.137e3; +const double UlsFunctionsClass::unii5StartFreqMHz = 5925.0; +const double UlsFunctionsClass::unii5StopFreqMHz = 6425.0; +const double UlsFunctionsClass::unii6StartFreqMHz = 6425.0; +const double UlsFunctionsClass::unii6StopFreqMHz = 6525.0; +const double UlsFunctionsClass::unii7StartFreqMHz = 6525.0; +const double UlsFunctionsClass::unii7StopFreqMHz = 6875.0; +const double UlsFunctionsClass::unii8StartFreqMHz = 6875.0; +const double UlsFunctionsClass::unii8StopFreqMHz = 7125.0; +/******************************************************************************************/ + +/**************************************************************************/ +/* UlsFunctionsClass::makeNumber() */ +/**************************************************************************/ +QString UlsFunctionsClass::makeNumber(const double &d) +{ + if (std::isnan(d)) { + return ""; + } else { + std::stringstream stream; + stream << std::fixed << std::setprecision(15) << d; + return QString::fromStdString(stream.str()); + } +} + +QString UlsFunctionsClass::makeNumber(const int &i) +{ + return QString::number(i); +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFunctionsClass::charString() */ +/**************************************************************************/ +QString UlsFunctionsClass::charString(char c) +{ + if (c < 32) { + return ""; + } + return QString(c); +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFunctionsClass::emissionDesignatorToBandwidth() */ +/**************************************************************************/ +double UlsFunctionsClass::emissionDesignatorToBandwidth(const QString &emDesig) +{ + QString frqPart = emDesig.left(4); + double multi; + QString unitS; + + if (frqPart.contains("H")) { + multi = 1; + unitS = "H"; + } else if (frqPart.contains("K")) { + multi = 1000; + unitS = "K"; + } else if (frqPart.contains("M")) { + multi = 1e6; + unitS = "M"; + } else if (frqPart.contains("G")) { + multi = 1e9; + unitS = "G"; + } else { + return (std::numeric_limits::quiet_NaN()); + } + + QString num = frqPart.replace(unitS, "."); + + double number = num.toDouble() * multi; + + return number / 1e6; // Convert to MHz +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFunctionsClass::hasNecessaryFields() */ +/**************************************************************************/ +QString UlsFunctionsClass::hasNecessaryFields(const UlsEmission &e, + UlsPath path, + UlsLocation rxLoc, + UlsLocation txLoc, + UlsAntenna rxAnt, + UlsAntenna txAnt, + UlsHeader txHeader, + QList prLocList, + QList prAntList, + bool removeMobile) +{ + QString failReason = ""; + // check lat/lon degree for rx + if (isnan(rxLoc.latitude) || isnan(rxLoc.longitude)) { + failReason.append("Invalid rx lat degree or long degree, "); + } + // check lat/lon degree for rx + if (isnan(txLoc.latitude) || isnan(txLoc.longitude)) { + failReason.append("Invalid tx lat degree or long degree, "); + } + // check tx and rx not at same position + if ((failReason == "") && (fabs(txLoc.longitude - rxLoc.longitude) <= 1.0e-5) && + (fabs(txLoc.latitude - rxLoc.latitude) <= 1.0e-5)) { + failReason.append("RX and TX at same location, "); + } + // check rx latitude/longitude direction + if (rxLoc.latitudeDirection != 'N' && rxLoc.latitudeDirection != 'S') { + failReason.append("Invalid rx latitude direction, "); + } + if (rxLoc.longitudeDirection != 'E' && rxLoc.longitudeDirection != 'W') { + failReason.append("Invalid rx longitude direction, "); + } + // check tx latitude/longitude direction + if (txLoc.latitudeDirection != 'N' && txLoc.latitudeDirection != 'S') { + failReason.append("Invalid tx latitude direction, "); + } + if (txLoc.longitudeDirection != 'E' && txLoc.longitudeDirection != 'W') { + failReason.append("Invalid tx longitude direction, "); + } + + // mobile + if (removeMobile && (txHeader.mobile == 'Y')) { + failReason.append("Mobile is Y, "); + } + + // radio service code + if (removeMobile && (strcmp(txHeader.radioServiceCode, "TP") == 0)) { + failReason.append("Radio service value of TP, "); + } + + int prIdx; + for (prIdx = 0; prIdx < prLocList.size(); ++prIdx) { + const UlsLocation &prLoc = prLocList[prIdx]; + + // check lat/lon degree for pr + if (isnan(prLoc.latitudeDeg) || isnan(prLoc.longitudeDeg)) { + failReason.append("Invalid passive repeater lat degree or long degree, "); + } + // check pr latitude/longitude direction + if (prLoc.latitudeDirection != 'N' && prLoc.latitudeDirection != 'S') { + failReason.append("Invalid passive repeater latitude direction, "); + } + if (prLoc.longitudeDirection != 'E' && prLoc.longitudeDirection != 'W') { + failReason.append("Invalid passive repeater longitude direction, "); + } + } + + return failReason; +} +/**************************************************************************/ + +/**************************************************************************/ +/* UlsFunctionsClass::SegmentCompare() */ +/**************************************************************************/ +bool UlsFunctionsClass::SegmentCompare(const UlsSegment &segA, const UlsSegment &segB) +{ + return segA.segmentNumber < segB.segmentNumber; +} +/**************************************************************************/ + +/******************************************************************************************/ +/* UlsFunctionsClass::getCSVHeader() */ +/******************************************************************************************/ +QStringList UlsFunctionsClass::getCSVHeader(int numPR) +{ + QStringList header; + header << "Region"; + header << "Callsign"; + header << "Status"; + header << "Radio Service"; + header << "Entity Name"; + header << "FRN"; + header << "Grant"; + header << "Expiration"; + header << "Effective"; + header << "Address"; + header << "City"; + header << "County"; + header << "State"; + header << "Common Carrier"; + header << "Non Common Carrier"; + header << "Private Comm"; + header << "Fixed"; + header << "Mobile"; + header << "Radiolocation"; + header << "Satellite"; + header << "Developmental or STA or Demo"; + header << "Interconnected"; + header << "Path Number"; + header << "Tx Location Number"; + header << "Tx Antenna Number"; + header << "Rx Callsign"; + header << "Rx Location Number"; + header << "Rx Antenna Number"; + header << "Frequency Number"; + header << "1st Segment Length (km)"; + header << "Center Frequency (MHz)"; + header << "Bandwidth (MHz)"; + header << "Lower Band (MHz)"; + header << "Upper Band (MHz)"; + header << "Tolerance (%)"; + header << "Tx EIRP (dBm)"; + header << "Auto Tx Pwr Control"; + header << "Emissions Designator"; + header << "Digital Mod Rate"; + header << "Digital Mod Type"; + header << "Tx Manufacturer"; + header << "Tx Model ULS"; + header << "Tx Model Matched"; + header << "Tx Architecture"; + header << "Tx Location Name"; + header << "Tx Lat Coords"; + header << "Tx Long Coords"; + header << "Tx Ground Elevation (m)"; + header << "Tx Polarization"; + header << "Azimuth Angle Towards Tx (deg)"; + header << "Elevation Angle Towards Tx (deg)"; + header << "Tx Ant Manufacturer"; + header << "Tx Ant Model"; + header << "Tx Ant Model Name Matched"; + header << "Tx Ant Category"; + header << "Tx Ant Diameter (m)"; + header << "Tx Ant Midband Gain (dB)"; + header << "Tx Height to Center RAAT ULS (m)"; + header << "Tx Beamwidth"; + header << "Tx Gain ULS (dBi)"; + header << "Rx Location Name"; + header << "Rx Lat Coords"; + header << "Rx Long Coords"; + header << "Rx Ground Elevation (m)"; + header << "Rx Manufacturer"; + header << "Rx Model"; + header << "Rx Ant Manufacturer"; + header << "Rx Ant Model"; + header << "Rx Ant Model Name Matched"; + header << "Rx Ant Category"; + header << "Rx Ant Diameter (m)"; + header << "Rx Ant Midband Gain (dB)"; + header << "Rx Line Loss (dB)"; + header << "Rx Height to Center RAAT ULS (m)"; + header << "Rx Gain ULS (dBi)"; + header << "Rx Diversity Height to Center RAAT ULS (m)"; + header << "Rx Diversity Ant Diameter (m)"; + header << "Rx Diversity Gain ULS (dBi)"; + header << "Num Passive Repeater"; + for (int prIdx = 1; prIdx <= numPR; ++prIdx) { + header << "Passive Repeater " + QString::number(prIdx) + " Location Name"; + header << "Passive Repeater " + QString::number(prIdx) + " Lat Coords"; + header << "Passive Repeater " + QString::number(prIdx) + " Long Coords"; + header << "Passive Repeater " + QString::number(prIdx) + " Ground Elevation (m)"; + header << "Passive Repeater " + QString::number(prIdx) + " Polarization"; + header << "Passive Repeater " + QString::number(prIdx) + " Azimuth Angle (deg)"; + header << "Passive Repeater " + QString::number(prIdx) + " Elevation Angle (deg)"; + header << "Passive Repeater " + QString::number(prIdx) + " Ant Manufacturer"; + header << "Passive Repeater " + QString::number(prIdx) + " Ant Model"; + header << "Passive Repeater " + QString::number(prIdx) + " Ant Model Name Matched"; + header << "Passive Repeater " + QString::number(prIdx) + " Ant Type"; + header << "Passive Repeater " + QString::number(prIdx) + " Ant Category"; + header << "Passive Repeater " + QString::number(prIdx) + + " ULS Back-to-Back Gain Tx (dBi)"; + header << "Passive Repeater " + QString::number(prIdx) + + " ULS Back-to-Back Gain Rx (dBi)"; + header << "Passive Repeater " + QString::number(prIdx) + + " ULS Reflector Height (m)"; + header << "Passive Repeater " + QString::number(prIdx) + " ULS Reflector Width (m)"; + header << "Passive Repeater " + QString::number(prIdx) + " Ant Model Diameter (m)"; + header << "Passive Repeater " + QString::number(prIdx) + + " Ant Model Midband Gain (dB)"; + header << "Passive Repeater " + QString::number(prIdx) + + " Ant Model Reflector Height (m)"; + header << "Passive Repeater " + QString::number(prIdx) + + " Ant Model Reflector Width (m)"; + header << "Passive Repeater " + QString::number(prIdx) + " Line Loss (dB)"; + header << "Passive Repeater " + QString::number(prIdx) + + " Height to Center RAAT Tx (m)"; + header << "Passive Repeater " + QString::number(prIdx) + + " Height to Center RAAT Rx (m)"; + header << "Passive Repeater " + QString::number(prIdx) + " Beamwidth"; + header << "Segment " + QString::number(prIdx + 1) + " Length (Km)"; + } + + return header; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/* UlsFunctionsClass::getRASHeader() */ +/******************************************************************************************/ +QStringList UlsFunctionsClass::getRASHeader() +{ + QStringList header; + + header << "RASID"; + header << "Region"; + header << "Name"; + header << "Location"; + header << "Start Freq (MHz)"; + header << "End Freq (MHz)"; + header << "Exclusion Zone"; + header << "Rectangle1 Lat 1"; + header << "Rectangle1 Lat 2"; + header << "Rectangle1 Lon 1"; + header << "Rectangle1 Lon 2"; + header << "Rectangle2 Lat 1"; + header << "Rectangle2 Lat 2"; + header << "Rectangle2 Lon 1"; + header << "Rectangle2 Lon 2"; + header << "Circle Radius (km)"; + header << "Circle center Lat"; + header << "Circle center Lon"; + header << "Antenna AGL height (m)"; + + return header; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** UlsFunctionsClass::computeSpectralOverlap() ****/ +/******************************************************************************************/ +double UlsFunctionsClass::computeSpectralOverlap(double sigStartFreq, + double sigStopFreq, + double rxStartFreq, + double rxStopFreq) +{ + double overlap; + + if ((sigStopFreq <= rxStartFreq) || (sigStartFreq >= rxStopFreq)) { + overlap = 0.0; + } else { + double f1 = (sigStartFreq < rxStartFreq ? rxStartFreq : sigStartFreq); + double f2 = (sigStopFreq > rxStopFreq ? rxStopFreq : sigStopFreq); + overlap = (f2 - f1) / (sigStopFreq - sigStartFreq); + } + + return (overlap); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** UlsFunctionsClass::computeHPointingVec() ****/ +/******************************************************************************************/ +Vector3 UlsFunctionsClass::computeHPointingVec(Vector3 position, + double azimuthPtg, + double elevationPtg) +{ + Vector3 ptgVec; + + Vector3 upVec = position.normalized(); + Vector3 zVec = Vector3(0.0, 0.0, 1.0); + Vector3 eastVec = zVec.cross(upVec).normalized(); + Vector3 northVec = upVec.cross(eastVec); + + double ca = cos(azimuthPtg * M_PI / 180.0); + double sa = sin(azimuthPtg * M_PI / 180.0); + double ce = cos(elevationPtg * M_PI / 180.0); + double se = sin(elevationPtg * M_PI / 180.0); + + ptgVec = northVec * ca * ce + eastVec * sa * ce + upVec * se; + + return (ptgVec); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: UlsFunctionsClass::getAngleFromDMS ****/ +/**** Process DMS string and return angle (lat or lon) in deg. ****/ +/******************************************************************************************/ +double UlsFunctionsClass::getAngleFromDMS(std::string dmsStr) +{ + std::ostringstream errStr; + char *chptr; + double angleDeg; + + bool error = false; + + std::size_t dashPosn1; + std::size_t dashPosn2; + std::size_t letterPosn; + + if (dmsStr == "") { + return (std::numeric_limits::quiet_NaN()); + } + + dashPosn1 = dmsStr.find('-'); + if ((dashPosn1 == std::string::npos) || (dashPosn1 == 0)) { + // Angle is in decimal format, not DMS + angleDeg = strtod(dmsStr.c_str(), &chptr); + } else { + if (!error) { + dashPosn2 = dmsStr.find('-', dashPosn1 + 1); + if (dashPosn2 == std::string::npos) { + error = true; + } + } + + double dVal, mVal, sVal; + if (!error) { + letterPosn = dmsStr.find_first_of("NEWS", dashPosn2 + 1); + + std::string dStr = dmsStr.substr(0, dashPosn1); + std::string mStr = dmsStr.substr(dashPosn1 + 1, dashPosn2 - dashPosn1 - 1); + std::string sStr = ((letterPosn == std::string::npos) ? + dmsStr.substr(dashPosn2 + 1) : + dmsStr.substr(dashPosn2 + 1, + letterPosn - dashPosn2 - 1)); + + dVal = strtod(dStr.c_str(), &chptr); + mVal = strtod(mStr.c_str(), &chptr); + sVal = strtod(sStr.c_str(), &chptr); + } + + if (error) { + errStr << "ERROR: Unable to convert DMS string to angle, DMS string = \"" + << dmsStr << "\"" << std::endl; + throw std::runtime_error(errStr.str()); + } + + angleDeg = dVal + (mVal + sVal / 60.0) / 60.0; + + if (letterPosn != std::string::npos) { + if ((dmsStr.at(letterPosn) == 'W') || (dmsStr.at(letterPosn) == 'S')) { + angleDeg *= -1; + } + } + } + + return (angleDeg); +} +/******************************************************************************************/ diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsFunctions.h b/src/coalition_ulsprocessor/src/uls-script/UlsFunctions.h new file mode 100644 index 0000000..d79719e --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsFunctions.h @@ -0,0 +1,62 @@ +#ifndef ULS_FUNCTIONS_H +#define ULS_FUNCTIONS_H + +#include +#include +#include + +#include +#include +#include + +#include "UlsFileReader.h" + +class UlsFunctionsClass +{ + public: + static const double speedOfLight; // speed of light in m/s + static const double earthRadius; // Radius of earth in m + static const double unii5StartFreqMHz; + static const double unii5StopFreqMHz; + static const double unii6StartFreqMHz; + static const double unii6StopFreqMHz; + static const double unii7StartFreqMHz; + static const double unii7StopFreqMHz; + static const double unii8StartFreqMHz; + static const double unii8StopFreqMHz; + + static QString makeNumber(const double &d); + + static QString makeNumber(const int &i); + + static QString charString(char c); + static double emissionDesignatorToBandwidth(const QString &emDesig); + static QString hasNecessaryFields(const UlsEmission &e, + UlsPath path, + UlsLocation rxLoc, + UlsLocation txLoc, + UlsAntenna rxAnt, + UlsAntenna txAnt, + UlsHeader txHeader, + QList prLocList, + QList prAntList, + bool removeMobile); + static bool SegmentCompare(const UlsSegment &segA, const UlsSegment &segB); + + static QStringList getCSVHeader(int numPR); + + static QStringList getRASHeader(); + + static double computeSpectralOverlap(double sigStartFreq, + double sigStopFreq, + double rxStartFreq, + double rxStopFreq); + + static Vector3 computeHPointingVec(Vector3 position, + double azimuthPtg, + double elevationPtg); + + static double getAngleFromDMS(std::string dmsStr); +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsHeader.h b/src/coalition_ulsprocessor/src/uls-script/UlsHeader.h new file mode 100644 index 0000000..3c03049 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsHeader.h @@ -0,0 +1,112 @@ +#ifndef ULS_HEADER_H +#define ULS_HEADER_H + +#include + +class UlsHeader +{ + public: + long long systemId; + char ulsFilenumber[15]; + char ebfNumber[31]; + char callsign[11]; + char licenseStatus; + char radioServiceCode[3]; + char grantDate[14]; + char expiredDate[14]; + char cancellationDate[14]; + char eligibilityNum[11]; + char reserved1; + char alien; + char alienGovernment; + char alienCorporation; + char alienOfficer; + char alienControl; + char revoked; + char convicted; + char adjudged; + char reserved2; + char commonCarrier; + char nonCommonCarrier; + char privateCarrier; + char fixed; + char mobile; + char radiolocation; + char satellite; + char developmental; + char interconnected; + char certifierFirstName[21]; + char certifierMiddleInitial; + char certifierLastName[21]; + char certifierSuffix[3]; + char certifierTitle[41]; + char female; + char blackAfAmerican; + char nativeAmerican; + char hawaiian; + char asian; + char white; + char effectiveDate[14]; + char lastActionDate[14]; + int auctionId; + char broadcastServiceStatus; + char bandManager; + char broadcastType; + char alienRuling; + char licenseeNameChange; + + enum Parameter { + MinHeaderParameter = 0x06000000, + HeaderSystemId = 0x06000001, + HeaderUlsFileNumber = 0x06000002, + HeaderEbfNumber = 0x06000003, + HeaderCallsign = 0x06000004, + HeaderLicenseStatus = 0x06000005, + HeaderRadioServiceCode = 0x06000006, + HeaderGrantDate = 0x06000007, + HeaderExpiredDate = 0x06000008, + HeaderCancellationDate = 0x06000009, + HeaderEligibilityNumber = 0x0600000a, + HeaderReserved1 = 0x0600000b, + HeaderAlien = 0x0600000c, + HeaderAlienGovernment = 0x0600000d, + HeaderAlienCorporation = 0x0600000e, + HeaderAlienOfficer = 0x0600000f, + HeaderAlienControl = 0x06000010, + HeaderRevoked = 0x06000011, + HeaderConvicted = 0x06000012, + HeaderAdjudged = 0x06000013, + HeaderReserved2 = 0x06000014, + HeaderCommonCarrier = 0x06000015, + HeaderNonCommonCarrier = 0x06000016, + HeaderPrivateCarrier = 0x06000017, + HeaderFixed = 0x06000018, + HeaderMobile = 0x06000019, + HeaderRadiolocation = 0x0600001a, + HeaderSatellite = 0x0600001b, + HeaderDevelopmental = 0x0600001c, + HeaderInterconnected = 0x0600001d, + HeaderCertifierFirstName = 0x0600001e, + HeaderCertifierMiddleInitial = 0x0600001f, + HeaderCertifierLastName = 0x06000020, + HeaderCertifierSuffix = 0x06000021, + HeaderCertifierTitle = 0x06000022, + HeaderFemale = 0x06000023, + HeaderBlackAfAmerican = 0x06000024, + HeaderNativeAmerican = 0x06000025, + HeaderHawaiian = 0x06000026, + HeaderAsian = 0x06000027, + HeaderWhite = 0x06000028, + HeaderEffectiveDate = 0x06000029, + HeaderLastActionDate = 0x0600002a, + HeaderAuctionId = 0x0600002b, + HeaderBroadcastServiceStatus = 0x0600002c, + HeaderBandManager = 0x0600002d, + HeaderBroadcastType = 0x0600002e, + HeaderAlienRuling = 0x0600002f, + HeaderLicenseeNameChange = 0x06000030, + MaxHeaderParameter = 0x06000031, + }; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsHop.h b/src/coalition_ulsprocessor/src/uls-script/UlsHop.h new file mode 100644 index 0000000..866586a --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsHop.h @@ -0,0 +1,38 @@ +/* + * This class represents a single hop in a microwave link. This basically assembles the + * information read in from the database and builds a single, larger database with pointers to the + * appropriate locations, antennas and frequencies. + * + */ + +#ifndef ULS_HOP_H +#define ULS_HOP_H + +#include "UlsAntenna.h" +#include "UlsLocation.h" +#include "UlsPath.h" +#include "UlsFrequency.h" + +enum HopType { HopArea, HopFixedLink, HopOther }; + +class UlsHop +{ + public: + enum HopType type; + char callsign[11]; + char unconfirmedReceiver; + + struct UlsAntenna *txAntenna; + struct UlsAntenna *rxAntenna; + struct QList freq; + struct UlsPath *path; + struct UlsLocation *rxLocation; + struct UlsLocation *txLocation; + struct UlsEmission *emission; + struct UlsHeader *header; + + QList txEntities; + QList rxEntities; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsLocation.h b/src/coalition_ulsprocessor/src/uls-script/UlsLocation.h new file mode 100644 index 0000000..d00a563 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsLocation.h @@ -0,0 +1,107 @@ +/* + * This data structure stores the data related to an location. + * There is a field in this for each relevent column in the ULS database, regardless of whether + * or not it is populated. + * + */ + +#ifndef ULS_LOCATION_H +#define ULS_LOCATION_H + +class UlsLocation +{ + public: + long long systemId; // unique identifier for this record; may not be necessary + char callsign[11]; // this is the 'key' for the owner of this antenna. + char locationAction; + char locationType; + char locationClass; + int locationNumber; + char siteStatus; + int correspondingFixedLocation; + char locationAddress[81]; + char locationCity[21]; + char locationCounty[61]; + char locationState[3]; + double radius; + char areaOperationCode; + char clearanceIndication; + double groundElevation; + double latitude; // Negative = south + double longitude; // Negative = west + char nepa; + double supportHeight; + double overallHeight; + char structureType[7]; + char airportId[5]; + char locationName[21]; + char statusCode; + char statusDate[11]; + char earthStationAgreement; + + int latitudeDeg, latitudeMinutes; + double latitudeSeconds; + char latitudeDirection; + int longitudeDeg, longitudeMinutes; + double longitudeSeconds; + char longitudeDirection; + + enum Parameters { + MinLocationParameter = 0x03000000, + LocationSystemId = 0x03000001, + LocationCallsign = 0x03000002, + LocationAction = 0x03000003, + LocationType = 0x03000004, + LocationClass = 0x03000005, + LocationSiteStatus = 0x03000006, + LocationCorrespondingFixed = 0x03000007, + LocationAddress = 0x03000008, + LocationCity = 0x03000009, + LocationCounty = 0x0300000a, + LocationState = 0x0300000b, + LocationRadius = 0x0300000c, + LocationAreaOperationCode = 0x0300000d, + LocationClearanceIndication = 0x0300000e, + LocationGroundElevation = 0x0300000f, + LocationLatitude = 0x03000010, + LocationLongitude = 0x03000011, + LocationNepa = 0x03000012, + LocationSupportHeight = 0x03000013, + LocationOverallHeight = 0x03000014, + LocationStructureType = 0x03000015, + LocationAirportId = 0x03000016, + LocationName = 0x03000017, + LocationStatusCode = 0x03000018, + LocationStatusDate = 0x03000019, + LocationEarthStationAgreement = 0x0300001a, + MaxLocationParameter = 0x0300001b, + ReceiveLocationSystemId = 0x13000001, + ReceiveLocationCallsign = 0x13000002, + ReceiveLocationAction = 0x13000003, + ReceiveLocationType = 0x13000004, + ReceiveLocationClass = 0x13000005, + ReceiveLocationSiteStatus = 0x13000006, + ReceiveLocationCorrespondingFixed = 0x13000007, + ReceiveLocationAddress = 0x13000008, + ReceiveLocationCity = 0x13000009, + ReceiveLocationCounty = 0x1300000a, + ReceiveLocationState = 0x1300000b, + ReceiveLocationRadius = 0x1300000c, + ReceiveLocationAreaOperationCode = 0x1300000d, + ReceiveLocationClearanceIndication = 0x1300000e, + ReceiveLocationGroundElevation = 0x1300000f, + ReceiveLocationLatitude = 0x13000010, + ReceiveLocationLongitude = 0x13000011, + ReceiveLocationNepa = 0x13000012, + ReceiveLocationSupportHeight = 0x13000013, + ReceiveLocationOverallHeight = 0x13000014, + ReceiveLocationStructureType = 0x13000015, + ReceiveLocationAirportId = 0x13000016, + ReceiveLocationName = 0x13000017, + ReceiveLocationStatusCode = 0x13000018, + ReceiveLocationStatusDate = 0x13000019, + ReceiveLocationEarthStationAgreement = 0x1300001a, + }; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsMarketFrequency.h b/src/coalition_ulsprocessor/src/uls-script/UlsMarketFrequency.h new file mode 100644 index 0000000..5d2be03 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsMarketFrequency.h @@ -0,0 +1,14 @@ +#ifndef ULS_MARKET_FREQUENCY_H +#define ULS_MARKET_FREQUENCY_H + +class UlsMarketFrequency +{ + public: + long long int systemId; + char callsign[11]; + char partitionSeq[7]; + double lowerFreq; + double upperFreq; +}; + +#endif \ No newline at end of file diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsPath.h b/src/coalition_ulsprocessor/src/uls-script/UlsPath.h new file mode 100644 index 0000000..2cd6286 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsPath.h @@ -0,0 +1,51 @@ +/* + * This data structure stores the data related to a path. + * There is a field in this for each relevent column in the ULS database, regardless of whether + * or not it is populated. + * + */ + +#ifndef ULS_PATH_H +#define ULS_PATH_H + +class UlsPath +{ + public: + long long systemId; // unique identifier for this record; may not be necessary + char callsign[11]; // this is the 'key' for the owner of this antenna. + int pathNumber; // Path ID. + int txLocationNumber; // This matches a UlsLocation of the same callsign as above. + int txAntennaNumber; // Likewise for a UlsAntenna. + int rxLocationNumber; // This matches a UlsLocation with either the same callsign of + // the one below. + int rxAntennaNumber; // Likewise. + char pathType[21]; + char passiveReceiver; + char countryCode[4]; + char GSOinterference; + char rxCallsign[11]; // If empty in DB, program will make the same as above. + double angularSeparation; + char statusCode; + char statusDate[11]; + + enum Parameter { + MinPathParameter = 0x04000000, + PathCallsign = 0x04000001, + PathNumber = 0x04000002, + PathTxLocationNumber = 0x04000003, + PathTxAntennaNumber = 0x04000004, + PathRxLocationNumber = 0x04000005, + PathRxAntennaNumber = 0x04000006, + PathType = 0x04000007, + PathPassiveReceiver = 0x04000008, + PathCountryCode = 0x04000009, + PathGSOInterference = 0x0400000a, + PathRxCallsign = 0x0400000b, + PathAngularSeparation = 0x0400000c, + PathStatusCode = 0x0400000d, + PathStatusDate = 0x0400000e, + MaxPathParameter = 0x0400000f, + }; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsQuery.h b/src/coalition_ulsprocessor/src/uls-script/UlsQuery.h new file mode 100644 index 0000000..501dbf0 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsQuery.h @@ -0,0 +1,19 @@ +/* + * This class represents a query. Queries can come in several forms, such as String queries, + * Numeric queries, etc. + * + * Created 26 Feb, 2009 by Erik Halvorson. + */ + +#ifndef ULS_QUERY_H +#define ULS_QUERY_H + +struct UlsQuery { + enum { GreaterThan, LessThan, EqualTo } QuerySign; + + struct UlsQuery *next; + QuerySign sgn; + int field; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/UlsSegment.h b/src/coalition_ulsprocessor/src/uls-script/UlsSegment.h new file mode 100644 index 0000000..49202d2 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/UlsSegment.h @@ -0,0 +1,19 @@ +#ifndef ULS_SEGMENT_H +#define ULS_SEGMENT_H + +class UlsSegment +{ + public: + long long systemId; + char callsign[11]; + int pathNumber; + int txLocationId; + int txAntennaId; + ; + int rxLocationId; + int rxAntennaId; + int segmentNumber; + double segmentLength; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/Vector3.h b/src/coalition_ulsprocessor/src/uls-script/Vector3.h new file mode 100644 index 0000000..5c4f614 --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/Vector3.h @@ -0,0 +1,225 @@ +#ifndef VECTOR_H +#define VECTOR_H + +#include +#include +#include +#include + +#include "MathHelpers.h" + +using namespace arma; +using namespace MathHelpers; + +/** + * A class representing a 3-Dimensional Vector in floating point precision + * + */ +class Vector3 +{ + public: + /** + * Construct a vector from its 3 coordinates + * + * @param x X coordinate to set + * @param y Y coordinate to set + * @param z Z coordinate to set + */ + Vector3(double xVal = 0.0, double yVal = 0.0, double zVal = 0.0) + { + data[0] = xVal; + data[1] = yVal; + data[2] = zVal; + } + + /** + * Copy constructor + * + * @param other Vector to copy + */ + Vector3(const Vector3 &other) : data(other.data) + { + } + + /** + * Construct Vector from underlying armadillo vector + * + * @param vec Armadillo 3 point vector + */ + Vector3(const arma::vec3 &vec) : data(vec) + { + } + + /** + * Get the x coordinate + * + * @return x coordinate + */ + inline double x() const + { + return data[0]; + } + + /** + * Get the y coordinate + * + * @return y coordinate + */ + inline double y() const + { + return data[1]; + } + + /** + * Get the z coordinate + * + * @return z coordinate + */ + inline double z() const + { + return data[2]; + } + + inline void normalize() + { + double l = len(); + data /= l; + } + + /** + * Perform a cross-product of vector with other vector and return resulting vector + * + * @param other The other vector to perform cross product with + * + * @return The resulting vector + */ + inline Vector3 cross(const Vector3 &other) const + { + return Vector3(data[1] * other.data[2] - data[2] * other.data[1], + data[2] * other.data[0] - data[0] * other.data[2], + data[0] * other.data[1] - data[1] * other.data[0]); + } + + /** + * Perform a dot-product of vector with other vector and return result + * + * @param other The other vector to perform dot product with + * + * @return The result of dot product + */ + inline double dot(const Vector3 &other) const + { + return arma::dot(this->data, other.data); + } + + /** + * Perform a dot-product of vector with other vector after normalizing each + * to have length one + * + * @param other The other vector to perform dot product with + * + * @return The result of the dot product + */ + inline double normDot(const Vector3 &other) const + { + return arma::norm_dot(this->data, other.data); + } + + /** + * The total length of this vector as a 3-D cartesian vector + * + * @return The length + */ + inline double len() const + { + return sqrt(sqr(data[0]) + sqr(data[1]) + sqr(data[2])); + } + + /** + * Get a copy of this vector but normalized to have length of one + * + * @return normalize vector + */ + inline Vector3 normalized() const + { + return Vector3(data / len()); + } + + /** + * The angle between this vector and other vector + * + * @param other The other vector + * + * @return angle in radians + */ + inline double angleBetween(const Vector3 &other) const + { + return acos(dot(other) / (len() * other.len())); + } + + inline Vector3 operator+(const Vector3 &other) const + { + return Vector3(data + other.data); + } + + inline Vector3 operator-(const Vector3 &other) const + { + return Vector3(data - other.data); + } + + inline Vector3 operator-() const + { + return Vector3(-data); + } + + inline Vector3 operator*(const Vector3 &other) const + { + return Vector3(data % other.data); + } + + inline Vector3 operator/(const Vector3 &other) const + { + return Vector3(data / other.data); + } + + inline Vector3 operator*(const double scalar) const + { + return Vector3(data * scalar); + } + + inline friend Vector3 operator*(const double scalar, const Vector3 &vector) + { + return vector * scalar; + } + + inline Vector3 operator=(const Vector3 &other) + { + data = other.data; + return *this; + } + + inline bool operator==(const Vector3 &other) + { + bool retval = (data[0] == other.data[0]) && (data[1] == other.data[1]) && + (data[2] == other.data[2]); + + return retval; + } + friend std::ostream &operator<<(std::ostream &os, const Vector3 &vector) + { + return os << "(" << vector.x() << ", " << vector.y() << ", " << vector.z() + << ")"; + } + + friend QDebug operator<<(QDebug stream, const Vector3 &vector) + { + stream.nospace() << "(" << vector.x() << ", " << vector.y() << ", " + << vector.z() << ")"; + return stream.space(); + } + + protected: + arma::vec3 data; +}; + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/global_defines.h b/src/coalition_ulsprocessor/src/uls-script/global_defines.h new file mode 100644 index 0000000..3a6020a --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/global_defines.h @@ -0,0 +1,18 @@ +/******************************************************************************************/ +/**** FILE: global_defines.h ****/ +/******************************************************************************************/ + +#ifndef GLOBAL_DEFINES_H +#define GLOBAL_DEFINES_H + +#define MAX_LINE_SIZE 5000 +#define CHDELIM " \t\n" /* Delimiting characters, used for string parsing */ + +#define IVECTOR(nn) (int *)((nn) ? malloc((nn) * sizeof(int)) : NULL) +#define DVECTOR(nn) (double *)((nn) ? malloc((nn) * sizeof(double)) : NULL) +#define CVECTOR(nn) (char *)((nn) ? malloc(((nn) + 1) * sizeof(char)) : NULL) + +#define CORE_DUMP printf("%d", *((int *)NULL)) +/******************************************************************************************/ + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/global_fn.cpp b/src/coalition_ulsprocessor/src/uls-script/global_fn.cpp new file mode 100644 index 0000000..7dbd82f --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/global_fn.cpp @@ -0,0 +1,225 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ + #include +#else + #include + #include + #include + #include +#endif + +#include "global_fn.h" + +/******************************************************************************************/ +/**** Read a line into string s, return length. From "C Programming Language" Pg. 29 ****/ +/**** Modified to be able to read both DOS and UNIX files. ****/ +/**** 2013.03.11: use std::string so not necessary to pre-allocate storage. ****/ +/**** Return value is number of characters read from FILE, which may or may not equal ****/ +/**** the length of string s depending on whether '\r' or '\n' has been removed from ****/ +/**** the string. ****/ +/******************************************************************************************/ +int fgetline(FILE *file, std::string &s, bool keepcr) +{ + int c, i; + + s.clear(); + for (i = 0; (c = fgetc(file)) != EOF && c != '\n'; i++) { + s += c; + } + if ((i >= 1) && (s[i - 1] == '\r')) { + s.erase(i - 1, 1); + // i--; + } + if (c == '\n') { + if (keepcr) { + s += c; + } + i++; + } + return (i); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Read a line into string s, return length. From "C Programming Language" Pg. 29 ****/ +/**** Modified to be able to read both DOS and UNIX files. ****/ +/******************************************************************************************/ +int fgetline(FILE *file, char *s) +{ + int c, i; + + for (i = 0; (c = fgetc(file)) != EOF && c != '\n'; i++) { + s[i] = c; + } + if ((i >= 1) && (s[i - 1] == '\r')) { + i--; + } + if (c == '\n') { + s[i] = c; + i++; + } + s[i] = '\0'; + return (i); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Split string into vector of strings using specified delim. ****/ +/******************************************************************************************/ +std::vector &split(const std::string &s, char delim, std::vector &elems) +{ + std::stringstream ss(s); + std::string item; + while (std::getline(ss, item, delim)) { + elems.push_back(item); + } + return elems; +} + +std::vector split(const std::string &s, char delim) +{ + std::vector elems; + split(s, delim, elems); + return elems; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Split string into vector of strings for each CSV field properly treating fields ****/ +/**** enclosed in double quotes with embedded commas. Also, double quotes can be ****/ +/**** embedded in a field using 2 double quotes "". ****/ +/**** This format is compatible with excel and libreoffice CSV files. ****/ +/******************************************************************************************/ +std::vector splitCSV(const std::string &line) +{ + int i, fieldLength; + int fieldStartIdx = -1; + std::ostringstream s; + std::vector elems; + std::string field; + bool skipChar = false; + + // state 0 = looking for next field + // state 1 = found beginning of field, not ", looking for end of field (comma) + // state 2 = found beginning of field, is ", looking for end of field ("). + // state 3 = found end of field, is ", pass over 0 or more spaces until (comma) + int state = 0; + + for (i = 0; i < (int)line.length(); i++) { + if (skipChar) { + skipChar = false; + } else { + switch (state) { + case 0: + if (line.at(i) == '\"') { + fieldStartIdx = i + 1; + state = 2; + } else if (line.at(i) == ',') { + field.clear(); + elems.push_back(field); + } else if (line.at(i) == ' ') { + // do nothing + } else { + fieldStartIdx = i; + state = 1; + } + break; + case 1: + if (line.at(i) == ',') { + fieldLength = i - fieldStartIdx; + field = line.substr(fieldStartIdx, fieldLength); + + std::size_t start = field.find_first_not_of(" \n" + "\t"); + std::size_t end = field.find_last_not_of(" \n\t"); + if (start == std::string::npos) { + field.clear(); + } else { + field = field.substr(start, + end - start + 1); + } + + elems.push_back(field); + state = 0; + } + break; + case 2: + if (line.at(i) == '\"') { + if ((i + 1 < (int)line.length()) && + (line.at(i + 1) == '\"')) { + skipChar = true; + } else { + fieldLength = i - fieldStartIdx; + field = line.substr(fieldStartIdx, + fieldLength); + std::size_t k = field.find("\"\""); + while (k != std::string::npos) { + field.erase(k, 1); + k = field.find("\"\""); + } + elems.push_back(field); + state = 3; + } + } + break; + case 3: + if (line.at(i) == ' ') { + // do nothing + } else if (line.at(i) == ',') { + state = 0; + } else { + s << "ERROR: Unable to splitCSV() for command \"" + << line << "\" invalid quotes.\n"; + throw std::runtime_error(s.str()); + } + break; + default: + CORE_DUMP; + break; + } + } + if (i == ((int)line.length()) - 1) { + if (state == 0) { + field.clear(); + elems.push_back(field); + } else if (state == 1) { + fieldLength = i - fieldStartIdx + 1; + field = line.substr(fieldStartIdx, fieldLength); + + std::size_t start = field.find_first_not_of(" \n\t"); + std::size_t end = field.find_last_not_of(" \n\t"); + if (start == std::string::npos) { + field.clear(); + } else { + field = field.substr(start, end - start + 1); + } + + elems.push_back(field); + state = 0; + } else if (state == 2) { + s << "ERROR: Unable to splitCSV() for command \"" << line + << "\" unmatched quote.\n"; + throw std::runtime_error(s.str()); + } else if (state == 3) { + state = 0; + } + } + } + + if (state != 0) { + CORE_DUMP; + } + + return elems; +} +/******************************************************************************************/ diff --git a/src/coalition_ulsprocessor/src/uls-script/global_fn.h b/src/coalition_ulsprocessor/src/uls-script/global_fn.h new file mode 100644 index 0000000..bfbc2ca --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/global_fn.h @@ -0,0 +1,18 @@ +/******************************************************************************************/ +/**** FILE: global_fn.h ****/ +/******************************************************************************************/ + +#ifndef GLOBAL_FN_H +#define GLOBAL_FN_H + +#include +#include +#include +#include "global_defines.h" + +int fgetline(FILE *file, std::string &s, bool keepcr = true); +int fgetline(FILE *, char *); +std::vector split(const std::string &s, char delim); +std::vector splitCSV(const std::string &cmd); + +#endif diff --git a/src/coalition_ulsprocessor/src/uls-script/main.cpp b/src/coalition_ulsprocessor/src/uls-script/main.cpp new file mode 100644 index 0000000..cb9714c --- /dev/null +++ b/src/coalition_ulsprocessor/src/uls-script/main.cpp @@ -0,0 +1,2091 @@ +#include +#include +#include +#include +#include + +#include "CsvWriter.h" +#include "UlsFileReader.h" +#include "AntennaModelMap.h" +#include "TransmitterModelMap.h" +#include "FreqAssignment.h" +#include "UlsFunctions.h" + +#define VERSION "1.3.0" + +bool removeMobile = false; +bool includeUnii5US = false; +bool includeUnii6US = false; +bool includeUnii7US = false; +bool includeUnii8US = false; +bool debugFlag = false; +bool combineAntennaRegionFlag = false; + +void testAntennaModelMap(AntennaModelMapClass &antennaModelMap, + std::string inputFile, + std::string outputFile); +void testTransmitterModelMap(TransmitterModelMapClass &transmitterModelMap, + std::string inputFile, + std::string outputFile); +void writeRAS(UlsFileReader &r, std::string filename); +void processUS(UlsFileReader &r, + int maxNumPassiveRepeater, + CsvWriter &wt, + CsvWriter &anomalous, + FILE *fwarn, + AntennaModelMapClass &antennaModelMap, + FreqAssignmentClass &freqAssignment, + TransmitterModelMapClass &transmitterModelMap); +void processCA(UlsFileReader &r, + int maxNumPassiveRepeater, + CsvWriter &wt, + CsvWriter &anomalous, + FILE *fwarn, + AntennaModelMapClass &antennaModelMap); +void makeLink(const StationDataCAClass &station, + const QList &prList, + std::vector &idxList, + double &azimuthPtg, + double &elevationPtg); + +/******************************************************************************************/ +/* This flag will adjust longitude/latitude values to be consistent with what */ +/* Federated/QCOM are doing. Values will be adjusted as if printed with */ +/* numDigit digits, then read back in. */ +/******************************************************************************************/ +bool alignFederatedFlag = true; +double alignFederatedScale = 1.0E8; // 10 ^ numDigit +/******************************************************************************************/ + +int main(int argc, char **argv) +{ + setvbuf(stdout, NULL, _IONBF, 0); + + if (strcmp(argv[1], "--version") == 0) { + printf("Coalition ULS Processing Tool Version %s\n", VERSION); + printf("Copyright 2019 (C) RKF Engineering Solutions\n"); + printf("Compatible with ULS Database Version 4\n"); + printf("Spec: " + "https://www.fcc.gov/sites/default/files/" + "public_access_database_definitions_v4.pdf\n"); + return 0; + } + + printf("Coalition ULS Processing Tool Version %s\n", VERSION); + printf("Copyright 2019 (C) RKF Engineering Solutions\n"); + if (argc != 11) { + fprintf(stderr, + "Syntax: %s [ULS file.csv] [Output FS File.csv] [Output RAS File.csv] " + "[AntModelListFile.csv] [AntPrefixFile.csv] [AntModelMapFile.csv] " + "[freqAssignmentFile] [transmitterModelListFile] [uniiStr] [mode]\n", + argv[0]); + return -1; + } + + char *tstr; + + time_t t1 = time(NULL); + tstr = strdup(ctime(&t1)); + strtok(tstr, "\n"); + std::cout << tstr << " : Begin processing." << std::endl; + free(tstr); + + std::string inputFile = argv[1]; + std::string outputFSFile = argv[2]; + std::string outputRASFile = argv[3]; + std::string antModelListFile = argv[4]; + std::string antPrefixFile = argv[5]; + std::string antModelMapFile = argv[6]; + std::string freqAssignmentFile = argv[7]; + std::string transmitterModelListFile = argv[8]; + std::string uniiStr = argv[9]; + std::string mode = argv[10]; + + FILE *fwarn; + std::string warningFile = "warning_uls.txt"; + if (!(fwarn = fopen(warningFile.c_str(), "wb"))) { + std::cout << std::string("WARNING: Unable to open warningFile \"") + warningFile + + std::string("\"\n"); + } + + AntennaModelMapClass antennaModelMap(antModelListFile, antPrefixFile, antModelMapFile); + + TransmitterModelMapClass transmitterModelMap(transmitterModelListFile); + + FreqAssignmentClass fccFreqAssignment(freqAssignmentFile); + + std::string procPfx = "proc_uls"; + std::string debugStr = "_debug"; + std::string caStr = "_ca"; + if (mode == "test_antenna_model_map") { + testAntennaModelMap(antennaModelMap, inputFile, outputFSFile); + return 0; + } else if (mode == "test_transmitter_model_map") { + testTransmitterModelMap(transmitterModelMap, inputFile, outputFSFile); + return 0; + } else if (mode.compare(0, procPfx.size(), procPfx) == 0) { + std::string modeStr = mode.substr(procPfx.size()); + while (modeStr.size()) { + int n1 = modeStr.size(); + if (modeStr.compare(0, debugStr.size(), debugStr) == 0) { + debugFlag = true; + modeStr = modeStr.substr(debugStr.size()); + } + if (modeStr.compare(0, caStr.size(), caStr) == 0) { + combineAntennaRegionFlag = true; + modeStr = modeStr.substr(caStr.size()); + } + int n2 = modeStr.size(); + if (n1 == n2) { + fprintf(stderr, "ERROR: Invalid mode: %s\n", mode.c_str()); + return -1; + } + } + } else { + fprintf(stderr, "ERROR: Invalid mode: %s\n", mode.c_str()); + return -1; + } + + std::vector uniiList = split(uniiStr, ':'); + for (int i = 0; i < uniiList.size(); ++i) { + if (uniiList[i] == "5") { + includeUnii5US = true; + } else if (uniiList[i] == "6") { + includeUnii6US = true; + } else if (uniiList[i] == "7") { + includeUnii7US = true; + } else if (uniiList[i] == "8") { + includeUnii8US = true; + } else { + fprintf(stderr, "ERROR: Invalid uniiStr: %s\n", uniiStr.c_str()); + return -1; + } + } + + printf("Include UNII-5 US = %s\n", (includeUnii5US ? "true" : "false")); + printf("Include UNII-6 US = %s\n", (includeUnii6US ? "true" : "false")); + printf("Include UNII-7 US = %s\n", (includeUnii7US ? "true" : "false")); + printf("Include UNII-8 US = %s\n", (includeUnii8US ? "true" : "false")); + + if (!(includeUnii5US || includeUnii6US || includeUnii7US || includeUnii8US)) { + fprintf(stderr, "ERROR: No UNII bands selected for US\n"); + return -1; + } + + UlsFileReader r(inputFile.c_str(), fwarn, alignFederatedFlag, alignFederatedScale); + + int maxNumPRUS = r.computeStatisticsUS(fccFreqAssignment, + includeUnii5US, + includeUnii6US, + includeUnii7US, + includeUnii8US); + int maxNumPRCA = r.computeStatisticsCA(fwarn); + int maxNumPassiveRepeater = (maxNumPRUS > maxNumPRCA ? maxNumPRUS : maxNumPRCA); + + std::cout << "US Max Num Passive Repeater: " << maxNumPRUS << std::endl; + std::cout << "CA Max Num Passive Repeater: " << maxNumPRCA << std::endl; + std::cout << "Max Num Passive Repeater: " << maxNumPassiveRepeater << std::endl; + + CsvWriter wt(outputFSFile.c_str()); + { + QStringList header = UlsFunctionsClass::getCSVHeader(maxNumPassiveRepeater); + wt.writeRow(header); + } + + CsvWriter anomalous("anomalous_uls.csv"); + { + QStringList header = UlsFunctionsClass::getCSVHeader(maxNumPassiveRepeater); + header << "Fixed"; + header << "Anomalous Reason"; + anomalous.writeRow(header); + } + + writeRAS(r, outputRASFile); + + processUS(r, + maxNumPassiveRepeater, + wt, + anomalous, + fwarn, + antennaModelMap, + fccFreqAssignment, + transmitterModelMap); + + processCA(r, maxNumPassiveRepeater, wt, anomalous, fwarn, antennaModelMap); + + if (fwarn) { + fclose(fwarn); + } + + time_t t2 = time(NULL); + tstr = strdup(ctime(&t2)); + strtok(tstr, "\n"); + std::cout << tstr << " : Completed processing." << std::endl; + free(tstr); + + int elapsedTime = (int)(t2 - t1); + + int et = elapsedTime; + int elapsedTimeSec = et % 60; + et = et / 60; + int elapsedTimeMin = et % 60; + et = et / 60; + int elapsedTimeHour = et % 24; + et = et / 24; + int elapsedTimeDay = et; + + std::cout << "Elapsed time = " << (t2 - t1) << " sec = "; + if (elapsedTimeDay) { + std::cout << elapsedTimeDay << " days "; + } + if (elapsedTimeDay || elapsedTimeHour) { + std::cout << elapsedTimeHour << " hours "; + } + std::cout << elapsedTimeMin << " min "; + std::cout << elapsedTimeSec << " sec"; + std::cout << std::endl; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** writeRAS ****/ +/******************************************************************************************/ +void writeRAS(UlsFileReader &r, std::string filename) +{ + int rasid = 0; + + CsvWriter fpRAS(filename.c_str()); + { + QStringList header = UlsFunctionsClass::getRASHeader(); + fpRAS.writeRow(header); + } + + foreach(const RASClass &ras, r.RASList) + { + rasid++; + QStringList row; + row << UlsFunctionsClass::makeNumber(rasid); + row << QString::fromStdString(ras.region); + row << QString::fromStdString(ras.name); + row << QString::fromStdString(ras.location); + row << UlsFunctionsClass::makeNumber(ras.startFreqMHz); + row << UlsFunctionsClass::makeNumber(ras.stopFreqMHz); + row << QString::fromStdString(ras.exclusionZone); + row << UlsFunctionsClass::makeNumber(ras.rect1lat1); + row << UlsFunctionsClass::makeNumber(ras.rect1lat2); + row << UlsFunctionsClass::makeNumber(ras.rect1lon1); + row << UlsFunctionsClass::makeNumber(ras.rect1lon2); + row << UlsFunctionsClass::makeNumber(ras.rect2lat1); + row << UlsFunctionsClass::makeNumber(ras.rect2lat2); + row << UlsFunctionsClass::makeNumber(ras.rect2lon1); + row << UlsFunctionsClass::makeNumber(ras.rect2lon2); + row << UlsFunctionsClass::makeNumber(ras.radiusKm); + row << UlsFunctionsClass::makeNumber(ras.centerLat); + row << UlsFunctionsClass::makeNumber(ras.centerLon); + row << UlsFunctionsClass::makeNumber(ras.heightAGL); + + fpRAS.writeRow(row); + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** processUS ****/ +/******************************************************************************************/ +void processUS(UlsFileReader &r, + int maxNumPassiveRepeater, + CsvWriter &wt, + CsvWriter &anomalous, + FILE *fwarn, + AntennaModelMapClass &antennaModelMap, + FreqAssignmentClass &freqAssignment, + TransmitterModelMapClass &transmitterModelMap) +{ + int prIdx; + std::string antPfx = (combineAntennaRegionFlag ? "" : "US:"); + + qDebug() << "--- Beginning path processing"; + + const std::vector bwMHzListUnii5 = + {0.4, 0.8, 1.25, 2.5, 3.75, 5.0, 10.0, 30.0, 60.0}; + + const std::vector bwMHzListUnii7 = {0.4, 0.8, 1.25, 2.5, 3.75, 5.0, 10.0, 30.0}; + + int cnt = 0; + int numRecs = 0; + + int numAntMatch = 0; + int numAntUnmatch = 0; + int numMissingRxAntHeight = 0; + int numMissingTxAntHeight = 0; + int numMissingPRAntHeight = 0; + int numFreqAssignedMissing = 0; + int numUnableGetBandwidth = 0; + int numFreqUpperBandMissing = 0; + int numFreqUpperBandPresent = 0; + int numFreqInconsistent = 0; + int numAssignedStart = 0; // Number of times frequencyAssigned, frequencyUpperBand + // consistent with frequencyAssigned being startFreq + int numAssignedCenter = 0; // Number of times frequencyAssigned, frequencyUpperBand + // consistent with frequencyAssigned being centerFreq + int numAssignedOther = 0; // Number of times frequencyAssigned, frequencyUpperBand not + // consistent with frequencyAssigned being startFreq or centerFreq + + if (debugFlag) { + std::cout << "Item,Callsign,frequencyAssigned,frequencyUpperBand" << std::endl; + } + + foreach(const UlsFrequency &freq, r.frequencies()) + { + // qDebug() << "processing frequency " << cnt << "/" << r.frequencies().count() + // << " callsign " << freq.callsign; + QString anomalousReason = ""; + QString fixedReason = ""; + + QList pathList; + + foreach(const UlsPath &p, r.pathsMap(freq.callsign)) + { + if (strcmp(p.callsign, freq.callsign) == 0) { + if ((freq.locationNumber == p.txLocationNumber) && + (freq.antennaNumber == p.txAntennaNumber)) { + pathList << p; + } + } + } + + if ((pathList.size() == 0) && (fwarn)) { + fprintf(fwarn, + "CALLSIGN: %s, Unable to find path matching TX_LOCATION_NUM = %d " + "TX_ANTENNA_NUM = %d\n", + freq.callsign, + freq.locationNumber, + freq.antennaNumber); + } + + foreach(const UlsPath &path, pathList) + { + cnt++; + + /// Find the associated transmit location. + UlsLocation txLoc; + bool locFound = false; + foreach(const UlsLocation &loc, r.locationsMap(path.callsign)) + { + if (strcmp(loc.callsign, path.callsign) == 0) { + if (path.txLocationNumber == loc.locationNumber) { + txLoc = loc; + locFound = true; + break; + } + } + } + + if (locFound == false) { + if (fwarn) { + fprintf(fwarn, + "CALLSIGN: %s, Unable to find txLoc matching " + "LOCATION_NUM = %d\n", + freq.callsign, + path.txLocationNumber); + } + continue; + } + + /// Find the associated transmit antenna. + UlsAntenna txAnt; + bool txAntFound = false; + foreach(const UlsAntenna &ant, r.antennasMap(path.callsign)) + { + if (strcmp(ant.callsign, path.callsign) == 0) { + // Implicitly matches with an Antenna record with + // locationClass 'T' + if ((ant.locationNumber == txLoc.locationNumber) && + (ant.antennaNumber == path.txAntennaNumber) && + (ant.pathNumber == path.pathNumber)) { + txAnt = ant; + txAntFound = true; + break; + } + } + } + + if (txAntFound == false) { + if (fwarn) { + fprintf(fwarn, + "CALLSIGN: %s, Unable to find txAnt matching " + "LOCATION_NUM = %d ANTENNA_NUM = %d PATH_NUM = " + "%d\n", + freq.callsign, + txLoc.locationNumber, + path.txAntennaNumber, + path.pathNumber); + } + continue; + } + + /// Find the associated frequency. + const UlsFrequency &txFreq = freq; + + /// Find the RX location. + UlsLocation rxLoc; + bool rxLocFound = false; + foreach(const UlsLocation &loc, r.locationsMap(path.callsign)) + { + if (strcmp(loc.callsign, path.callsign) == 0) { + if (loc.locationNumber == path.rxLocationNumber) { + rxLoc = loc; + rxLocFound = true; + break; + } + } + } + + if (rxLocFound == false) { + if (fwarn) { + fprintf(fwarn, + "CALLSIGN: %s, Unable to find rxLoc matching " + "LOCATION_NUM = %d\n", + freq.callsign, + path.rxLocationNumber); + } + continue; + } + + /// Find the RX antenna. + UlsAntenna rxAnt; + bool rxAntFound = false; + foreach(const UlsAntenna &ant, r.antennasMap(path.callsign)) + { + if (strcmp(ant.callsign, path.callsign) == 0) { + // Implicitly matches with an Antenna record with + // locationClass 'R' + if ((ant.locationNumber == rxLoc.locationNumber) && + (ant.antennaNumber == path.rxAntennaNumber) && + (ant.pathNumber == path.pathNumber)) { + rxAnt = ant; + rxAntFound = true; + break; + } + } + } + + if (rxAntFound == false) { + if (fwarn) { + fprintf(fwarn, + "CALLSIGN: %s, Unable to find rxAnt matching " + "LOCATION_NUM = %d ANTENNA_NUM = %d PATH_NUM = " + "%d\n", + freq.callsign, + rxLoc.locationNumber, + path.rxAntennaNumber, + path.pathNumber); + } + continue; + } + + QList prLocList; + QList prAntList; + + /// Create list of segments in link. + QList segList; + foreach(const UlsSegment &s, r.segmentsMap(path.callsign)) + { + if (s.pathNumber == path.pathNumber) { + segList << s; + } + } + qSort(segList.begin(), segList.end(), UlsFunctionsClass::SegmentCompare); + + int prevSegRxLocationId = -1; + for (int segIdx = 0; segIdx < segList.size(); ++segIdx) { + const UlsSegment &s = segList[segIdx]; + if (s.segmentNumber != segIdx + 1) { + anomalousReason.append("Segments missing, "); + qDebug() << "callsign " << path.callsign << " path " + << path.pathNumber << " has missing segments."; + break; + } + if (segIdx == 0) { + if (s.txLocationId != txLoc.locationNumber) { + anomalousReason.append("First segment not at TX, "); + qDebug() << "callsign " << path.callsign << " path " + << path.pathNumber + << " first segment not at TX."; + break; + } + } + if (segIdx == segList.size() - 1) { + if (s.rxLocationId != rxLoc.locationNumber) { + anomalousReason.append("Last segment not at RX, "); + qDebug() << "callsign " << path.callsign << " path " + << path.pathNumber + << " last segment not at RX."; + break; + } + } + if (segIdx) { + if (s.txLocationId != prevSegRxLocationId) { + anomalousReason.append("Segments do not form a " + "path, "); + qDebug() << "callsign " << path.callsign << " path " + << path.pathNumber + << " segments do not form a path."; + break; + } + bool found; + found = false; + foreach(const UlsLocation &loc, + r.locationsMap(path.callsign)) + { + if (loc.locationNumber == s.txLocationId) { + prLocList << loc; + found = true; + break; + } + } + if (!found) { + anomalousReason.append("Segment location not " + "found, "); + qDebug() << "callsign " << path.callsign << " path " + << path.pathNumber + << " segment location not found."; + break; + } + found = false; + foreach(const UlsAntenna &ant, r.antennasMap(path.callsign)) + { + if ((ant.antennaType == 'P') && + (ant.locationNumber == + prLocList.last().locationNumber) && + (ant.pathNumber == path.pathNumber)) { + prAntList << ant; + found = true; + break; + } + } + if (!found) { + anomalousReason.append("Segment antenna not " + "found, "); + qDebug() << "callsign " << path.callsign << " path " + << path.pathNumber + << " segment antenna not found."; + break; + } + } + prevSegRxLocationId = s.rxLocationId; + } + + UlsSegment txSeg; + bool txSegFound = false; + foreach(const UlsSegment &s, r.segmentsMap(path.callsign)) + { + if (strcmp(s.callsign, path.callsign) == 0) { + if (s.pathNumber == path.pathNumber) { + if (s.segmentNumber < 2) { + txSeg = s; + txSegFound = true; + break; + } + } + } + } + + /// Find the emissions information. + bool txEmFound = false; + QList allTxEm; + foreach(const UlsEmission &e, r.emissionsMap(path.callsign)) + { + if (strcmp(e.callsign, path.callsign) == 0) { + if (e.locationId == txLoc.locationNumber && + e.antennaId == txAnt.antennaNumber && + e.frequencyId == txFreq.frequencyNumber) { + allTxEm << e; + txEmFound = true; + } + } + } + if (!txEmFound) { + UlsEmission txEm; + allTxEm << txEm; // Make sure at least one emission. + } + + /// Find the header. + UlsHeader txHeader; + bool txHeaderFound = false; + foreach(const UlsHeader &h, r.headersMap(path.callsign)) + { + if (strcmp(h.callsign, path.callsign) == 0) { + txHeader = h; + txHeaderFound = true; + break; + } + } + + if (!txHeaderFound) { + // qDebug() << "Unable to locate header data for" << path.callsign + // << "path" + // << path.pathNumber; + continue; + } else if (txHeader.licenseStatus != 'A' && txHeader.licenseStatus != 'L') { + // qDebug() << "Skipping non-Active tx for" << path.callsign << + // "path" << path.pathNumber; + continue; + } + + /// Find the entity + UlsEntity txEntity; + bool txEntityFound = false; + foreach(const UlsEntity &e, r.entitiesMap(path.callsign)) + { + if (strcmp(e.callsign, path.callsign) == 0) { + txEntity = e; + txEntityFound = true; + break; + } + } + + if (!txEntityFound) { + // qDebug() << "Unable to locate entity data for" << path.callsign + // << "path" + // << path.pathNumber; + continue; + } + + /// Find the control point. + UlsControlPoint txControlPoint; + bool txControlPointFound = false; + foreach(const UlsControlPoint &ucp, r.controlPointsMap(path.callsign)) + { + if (strcmp(ucp.callsign, path.callsign) == 0) { + txControlPoint = ucp; + txControlPointFound = true; + break; + } + } + + /// Build the actual output. + foreach(const UlsEmission &e, allTxEm) + { + double startFreq = std::numeric_limits::quiet_NaN(); + double stopFreq = std::numeric_limits::quiet_NaN(); + double startFreqBand = std::numeric_limits::quiet_NaN(); + double stopFreqBand = std::numeric_limits::quiet_NaN(); + double bwMHz = std::numeric_limits::quiet_NaN(); + bool freqInconsistentFlag = false; + bool freqUpperBandMissingFlag = false; + + if (isnan(txFreq.frequencyAssigned)) { + numFreqAssignedMissing++; + anomalousReason.append("FrequencyAssigned value missing"); + } else { + if (txEmFound) { + bwMHz = UlsFunctionsClass:: + emissionDesignatorToBandwidth(e.desig); + } + if (isnan(bwMHz) || (bwMHz > 60.0) || (bwMHz == 0)) { + bwMHz = freqAssignment.getBandwidthUS( + txFreq.frequencyAssigned); + } else { + bool unii5Flag = + (txFreq.frequencyAssigned >= + UlsFunctionsClass::unii5StartFreqMHz) && + (txFreq.frequencyAssigned <= + UlsFunctionsClass::unii5StopFreqMHz); + bool unii7Flag = + (txFreq.frequencyAssigned >= + UlsFunctionsClass::unii7StartFreqMHz) && + (txFreq.frequencyAssigned <= + UlsFunctionsClass::unii7StopFreqMHz); + const std::vector *fccBWList = + (std::vector *)NULL; + if (unii5Flag) { + fccBWList = &bwMHzListUnii5; + } else if (unii7Flag) { + fccBWList = &bwMHzListUnii7; + } + if (fccBWList) { + bool found = false; + double fccBW; + for (int i = 0; + (i < (int)fccBWList->size()) && + (!found); + ++i) { + if (fccBWList->at(i) >= bwMHz) { + found = true; + fccBW = fccBWList->at(i); + } + } + if (found) { + bwMHz = std::min(fccBW, + bwMHz * 1.1); + } + } + } + + if (bwMHz == -1.0) { + numUnableGetBandwidth++; + anomalousReason.append("Unable to get bandwidth"); + } else if (isnan(txFreq.frequencyUpperBand)) { + freqUpperBandMissingFlag = true; + startFreq = txFreq.frequencyAssigned - + bwMHz / 2.0; // Lower Band (MHz) + stopFreq = txFreq.frequencyAssigned + + bwMHz / 2.0; // Upper Band (MHz) + startFreqBand = startFreq; + stopFreqBand = stopFreq; + } else { + // frequencyAssigned is taken to be center frequency + // and frequencyUpperBand is ignored as per TS 1014 + startFreq = txFreq.frequencyAssigned - + bwMHz / 2.0; // Lower Band (MHz) + stopFreq = txFreq.frequencyAssigned + + bwMHz / 2.0; // Upper Band (MHz) + + // For the purpuse of determining which UNII band + // the link is in, frequencyAssigned is the start + // freq + startFreqBand = + txFreq.frequencyAssigned; // Lower Band + // (MHz) + stopFreqBand = startFreqBand + + bwMHz; // Upper Band (MHz) + + if (fabs(txFreq.frequencyUpperBand - + txFreq.frequencyAssigned - bwMHz) > + 1.0e-6) { + freqInconsistentFlag = true; + std::stringstream stream; + stream << "frequencyUpperBand = " + << txFreq.frequencyUpperBand + << " inconsistent with " + "frequencyAssigned and " + "bandwidth "; + fixedReason.append(QString::fromStdString( + stream.str())); + stream.str(std::string()); + } + + if (fabs(txFreq.frequencyUpperBand - + txFreq.frequencyAssigned - bwMHz) < + 1.0e-6) { + numAssignedStart++; + } else if (fabs(txFreq.frequencyUpperBand - + txFreq.frequencyAssigned - + bwMHz / 2) < 1.0e-6) { + numAssignedCenter++; + } else { + numAssignedOther++; + } + } + } + + AntennaModel::CategoryEnum category; + AntennaModelClass *rxAntModel = + antennaModelMap.find(antPfx, + rxAnt.antennaModel, + category, + AntennaModel::B1Category); + + AntennaModel::CategoryEnum rxAntennaCategory; + double rxAntennaDiameter; + double rxDiversityDiameter; + double rxAntennaMidbandGain; + std::string rxAntennaModelName; + if (rxAntModel) { + numAntMatch++; + rxAntennaModelName = rxAntModel->name; + rxAntennaCategory = rxAntModel->category; + rxAntennaDiameter = rxAntModel->diameterM; + rxDiversityDiameter = rxAntModel->diameterM; + rxAntennaMidbandGain = rxAntModel->midbandGain; + } else { + numAntUnmatch++; + rxAntennaModelName = ""; + rxAntennaCategory = category; + rxAntennaDiameter = -1.0; + rxDiversityDiameter = -1.0; + rxAntennaMidbandGain = + std::numeric_limits::quiet_NaN(); + fixedReason.append("Rx Antenna Model Unmatched"); + } + + AntennaModelClass *txAntModel = + antennaModelMap.find(antPfx, + txAnt.antennaModel, + category, + AntennaModel::B1Category); + + AntennaModel::CategoryEnum txAntennaCategory; + double txAntennaDiameter; + double txAntennaMidbandGain; + std::string txAntennaModelName; + if (txAntModel) { + numAntMatch++; + txAntennaModelName = txAntModel->name; + txAntennaCategory = txAntModel->category; + txAntennaDiameter = txAntModel->diameterM; + txAntennaMidbandGain = txAntModel->midbandGain; + } else { + numAntUnmatch++; + txAntennaModelName = ""; + txAntennaCategory = category; + txAntennaDiameter = -1.0; + txAntennaMidbandGain = + std::numeric_limits::quiet_NaN(); + fixedReason.append("Tx Antenna Model Unmatched"); + } + + if (isnan(startFreq) || isnan(stopFreq)) { + anomalousReason.append("NaN frequency value, "); + } else { + bool overlapUnii5 = + (stopFreqBand > + UlsFunctionsClass::unii5StartFreqMHz) && + (startFreqBand < + UlsFunctionsClass::unii5StopFreqMHz); + bool overlapUnii6 = + (stopFreqBand > + UlsFunctionsClass::unii6StartFreqMHz) && + (startFreqBand < + UlsFunctionsClass::unii6StopFreqMHz); + bool overlapUnii7 = + (stopFreqBand > + UlsFunctionsClass::unii7StartFreqMHz) && + (startFreqBand < + UlsFunctionsClass::unii7StopFreqMHz); + bool overlapUnii8 = + (stopFreqBand > + UlsFunctionsClass::unii8StartFreqMHz) && + (startFreqBand < + UlsFunctionsClass::unii8StopFreqMHz); + + if (!((includeUnii5US && overlapUnii5) || + (includeUnii6US && overlapUnii6) || + (includeUnii7US && overlapUnii7) || + (includeUnii8US && overlapUnii8))) { + continue; + } else if (overlapUnii5 && overlapUnii7) { + anomalousReason.append("Band overlaps both Unii5 " + "and Unii7, "); + } else if (overlapUnii6 && overlapUnii8) { + anomalousReason.append("Band overlaps both Unii6 " + "and Unii8, "); + } + } + + // R2-AIP-14-b + if (std::isnan(rxAnt.heightToCenterRAAT)) { + rxAnt.heightToCenterRAAT = -1.0; + numMissingRxAntHeight++; + } else if (rxAnt.heightToCenterRAAT < 1.5) { + rxAnt.heightToCenterRAAT = 1.5; + } + + if (std::isnan(txAnt.heightToCenterRAAT)) { + txAnt.heightToCenterRAAT = -1.0; + numMissingTxAntHeight++; + } else if (txAnt.heightToCenterRAAT < 1.5) { + txAnt.heightToCenterRAAT = 1.5; + } + + int prIdx; + for (prIdx = 0; prIdx < prAntList.size(); ++prIdx) { + UlsAntenna &prAnt = prAntList[prIdx]; + if (std::isnan(prAnt.heightToCenterRAAT)) { + prAnt.heightToCenterRAAT = -1.0; + numMissingPRAntHeight++; + } else if (prAnt.heightToCenterRAAT < 1.5) { + prAnt.heightToCenterRAAT = 1.5; + } + } + + // now that we have everything, ensure that inputs have what we need + anomalousReason.append( + UlsFunctionsClass::hasNecessaryFields(e, + path, + rxLoc, + txLoc, + rxAnt, + txAnt, + txHeader, + prLocList, + prAntList, + removeMobile)); + + if (anomalousReason.length() == 0) { + if (freqInconsistentFlag) { + numFreqInconsistent++; + } + if (freqUpperBandMissingFlag) { + numFreqUpperBandMissing++; + } else { + numFreqUpperBandPresent++; + if (debugFlag) { + std::cout << "Inband link with Frequency " + "Upper Band specified," + << path.callsign << "," + << txFreq.frequencyAssigned << "," + << txFreq.frequencyUpperBand + << std::endl; + } + } + } + + TransmitterModelClass *matchedTransmitterModel = + transmitterModelMap.find( + std::string(txFreq.transmitterModel)); + + std::string matchedTransmitterStr; + std::string transmitterArchitectureStr; + if (matchedTransmitterModel) { + matchedTransmitterStr = matchedTransmitterModel->name; + transmitterArchitectureStr = + TransmitterModelClass::architectureStr( + matchedTransmitterModel->architecture); + } else { + matchedTransmitterStr = std::string(""); + transmitterArchitectureStr = "UNKNOWN"; + } + + QStringList row; + row << "US"; // Region + row << path.callsign; // Callsign + row << QString(txHeader.licenseStatus); // Status + row << txHeader.radioServiceCode; // Radio Service + row << txEntity.entityName; // Entity Name + row << txEntity.frn; // FRN: Fcc Registration Number + row << txHeader.grantDate; // Grant + row << txHeader.expiredDate; // Expiration + row << txHeader.effectiveDate; // Effective + if (txControlPointFound) { + row << txControlPoint.controlPointAddress; // Address + row << txControlPoint.controlPointCity; // City + row << txControlPoint.controlPointCounty; // County + row << txControlPoint.controlPointState; // State + } else { + row << "" + << "" + << "" + << ""; + } + row << UlsFunctionsClass::charString( + txHeader.commonCarrier); // Common Carrier + row << UlsFunctionsClass::charString( + txHeader.nonCommonCarrier); // Non Common Carrier + row << UlsFunctionsClass::charString( + txHeader.privateCarrier); // Private Comm + row << UlsFunctionsClass::charString(txHeader.fixed); // Fixed + row << UlsFunctionsClass::charString(txHeader.mobile); // Mobile + row << UlsFunctionsClass::charString( + txHeader.radiolocation); // Radiolocation + row << UlsFunctionsClass::charString( + txHeader.satellite); // Satellite + row << UlsFunctionsClass::charString( + txHeader.developmental); // Developmental or STA or Demo + row << UlsFunctionsClass::charString( + txHeader.interconnected); // Interconnected + row << UlsFunctionsClass::makeNumber( + path.pathNumber); // Path Number + row << UlsFunctionsClass::makeNumber( + path.txLocationNumber); // Tx Location Number + row << UlsFunctionsClass::makeNumber( + path.txAntennaNumber); // Tx Antenna Number + row << path.rxCallsign; // Rx Callsign + row << UlsFunctionsClass::makeNumber( + path.rxLocationNumber); // Rx Location Number + row << UlsFunctionsClass::makeNumber( + path.rxAntennaNumber); // Rx Antenna Number + row << UlsFunctionsClass::makeNumber( + txFreq.frequencyNumber); // Frequency Number + if (txSegFound) { + row << UlsFunctionsClass::makeNumber( + txSeg.segmentLength); // 1st Segment Length (km) + } else { + row << ""; + } + + row << UlsFunctionsClass::makeNumber((startFreq + stopFreq) / + 2); // Center Frequency (MHz) + row << UlsFunctionsClass::makeNumber(bwMHz); // Bandiwdth (MHz) + row << UlsFunctionsClass::makeNumber(startFreq); // Lower Band (MHz) + row << UlsFunctionsClass::makeNumber(stopFreq); // Upper Band (MHz) + + row << UlsFunctionsClass::makeNumber( + txFreq.tolerance); // Tolerance (%) + row << UlsFunctionsClass::makeNumber(txFreq.EIRP); // Tx EIRP (dBm) + row << UlsFunctionsClass::charString( + txFreq.transmitterPowerControl); // Auto Tx Pwr Control + if (txEmFound) { + row << QString(e.desig); // Emissions Designator + row << UlsFunctionsClass::makeNumber( + e.modRate); // Digital Mod Rate + row << QString::fromStdString( + e.modCode); // Digital Mod Type + } else { + row << "" + << "" + << ""; + } + + row << txFreq.transmitterMake; // Tx Manufacturer + row << txFreq.transmitterModel; // Tx Model ULS + row << QString::fromStdString( + matchedTransmitterStr); // Tx Model Matched + row << QString::fromStdString( + transmitterArchitectureStr); // Tx Architecture + row << txLoc.locationName; // Tx Location Name + row << UlsFunctionsClass::makeNumber(txLoc.latitude); + // QString("%1-%2-%3 + // %4").arg(txLoc.latitudeDeg).arg(txLoc.latitudeMinutes).arg(txLoc.latitudeSeconds).arg(txLoc.latitudeDirection); + // // Tx Lat Coords + row << UlsFunctionsClass::makeNumber(txLoc.longitude); + // QString("%1-%2-%3 + // %4").arg(txLoc.longitudeDeg).arg(txLoc.longitudeMinutes).arg(txLoc.longitudeSeconds).arg(txLoc.longitudeDirection); + // // Tx Lon Coords + row << UlsFunctionsClass::makeNumber( + txLoc.groundElevation); // Tx Ground Elevation (m) + row << txAnt.polarizationCode; // Tx Polarization + row << UlsFunctionsClass::makeNumber( + txAnt.azimuth); // Tx Azimuth Angle (deg) + row << UlsFunctionsClass::makeNumber( + txAnt.tilt); // Tx Elevation Angle (deg) + row << QString::fromStdString( + txAnt.antennaMake); // Tx Ant Manufacturer + row << QString::fromStdString(txAnt.antennaModel); // Tx Ant Model + row << txAntennaModelName.c_str(); // Tx Matched antenna model + // (blank if unmatched) + row << AntennaModel::categoryStr(txAntennaCategory) + .c_str(); // Tx Antenna category + row << UlsFunctionsClass::makeNumber( + txAntennaDiameter); // Tx Ant Diameter (m) + row << UlsFunctionsClass::makeNumber( + txAntennaMidbandGain); // Tx Ant Midband Gain (dB) + row << UlsFunctionsClass::makeNumber( + txAnt.heightToCenterRAAT); // Tx Height to Center RAAT (m) + row << UlsFunctionsClass::makeNumber( + txAnt.beamwidth); // Tx Beamwidth + row << UlsFunctionsClass::makeNumber(txAnt.gain); // Tx Gain (dBi) + row << rxLoc.locationName; // Rx Location Name + row << UlsFunctionsClass::makeNumber(rxLoc.latitude); + // QString("%1-%2-%3 + // %4").arg(rxLoc.latitudeDeg).arg(rxLoc.latitudeMinutes).arg(rxLoc.latitudeSeconds).arg(rxLoc.latitudeDirection); + // // Rx Lat Coords + row << UlsFunctionsClass::makeNumber(rxLoc.longitude); + // QString("%1-%2-%3 + // %4").arg(rxLoc.longitudeDeg).arg(rxLoc.longitudeMinutes).arg(rxLoc.longitudeSeconds).arg(rxLoc.longitudeDirection); + // // Rx Lon Coords + row << UlsFunctionsClass::makeNumber( + rxLoc.groundElevation); // Rx Ground Elevation (m) + row << ""; // Rx Manufacturer + row << ""; // Rx Model + row << QString::fromStdString( + rxAnt.antennaMake); // Rx Ant Manufacturer + row << QString::fromStdString(rxAnt.antennaModel); // Rx Ant Model + row << rxAntennaModelName.c_str(); // Rx Matched antenna model + // (blank if unmatched) + row << AntennaModel::categoryStr(rxAntennaCategory) + .c_str(); // Rx Antenna category + row << UlsFunctionsClass::makeNumber( + rxAntennaDiameter); // Rx Ant Diameter (m) + row << UlsFunctionsClass::makeNumber( + rxAntennaMidbandGain); // Rx Ant Midband Gain (dB) + row << UlsFunctionsClass::makeNumber( + rxAnt.lineLoss); // Rx Line Loss (dB) + row << UlsFunctionsClass::makeNumber( + rxAnt.heightToCenterRAAT); // Rx Height to Center RAAT (m) + row << UlsFunctionsClass::makeNumber(rxAnt.gain); // Rx Gain (dBi) + row << UlsFunctionsClass::makeNumber( + rxAnt.diversityHeight); // Rx Diveristy Height (m) + row << UlsFunctionsClass::makeNumber( + rxDiversityDiameter); // Rx Diversity Diameter (m) + row << UlsFunctionsClass::makeNumber( + rxAnt.diversityGain); // Rx Diversity Gain (dBi) + + row << QString::number(prLocList.size()); + for (prIdx = 1; prIdx <= maxNumPassiveRepeater; ++prIdx) { + if ((prIdx <= prLocList.size()) && + (prIdx <= prAntList.size())) { + UlsLocation &prLoc = prLocList[prIdx - 1]; + UlsAntenna &prAnt = prAntList[prIdx - 1]; + UlsSegment &segment = segList[prIdx]; + + AntennaModelClass *prAntModel = + antennaModelMap.find( + antPfx, + prAnt.antennaModel, + category, + AntennaModel::B1Category); + + AntennaModel::TypeEnum prAntennaType; + AntennaModel::CategoryEnum prAntennaCategory; + double prAntennaDiameter; + double prAntennaMidbandGain; + double prAntennaReflectorWidth; + double prAntennaReflectorHeight; + std::string prAntennaModelName; + + if (prAntModel) { + numAntMatch++; + prAntennaModelName = prAntModel->name; + prAntennaType = prAntModel->type; + prAntennaCategory = prAntModel->category; + prAntennaDiameter = prAntModel->diameterM; + prAntennaMidbandGain = + prAntModel->midbandGain; + prAntennaReflectorWidth = + prAntModel->reflectorWidthM; + prAntennaReflectorHeight = + prAntModel->reflectorHeightM; + } else { + numAntUnmatch++; + prAntennaModelName = ""; + prAntennaType = AntennaModel::UnknownType; + prAntennaCategory = category; + prAntennaDiameter = -1.0; + prAntennaMidbandGain = std::numeric_limits< + double>::quiet_NaN(); + prAntennaReflectorWidth = -1.0; + prAntennaReflectorHeight = -1.0; + fixedReason.append("PR Antenna Model " + "Unmatched"); + } + + row << prLoc.locationName; // Passive Repeater + // Location Name + row << UlsFunctionsClass::makeNumber( + prLoc.latitude); // Passive Repeater Lat + // Coords + row << UlsFunctionsClass::makeNumber( + prLoc.longitude); // Passive Repeater Lon + // Coords + row << UlsFunctionsClass::makeNumber( + prLoc.groundElevation); // Passive Repeater + // Ground Elevation + row << prAnt.polarizationCode; // Passive Repeater + // Polarization + row << UlsFunctionsClass::makeNumber( + prAnt.azimuth); // Passive Repeater Azimuth + // Angle + row << UlsFunctionsClass::makeNumber( + prAnt.tilt); // Passive Repeater Elevation + // Angle + row << QString::fromStdString( + prAnt.antennaMake); // Passive Repeater Ant + // Make + row << QString::fromStdString( + prAnt.antennaModel); // Passive Repeater Ant + // Model + row << prAntennaModelName + .c_str(); // Passive Repeater + // antenna model (blank if + // unmatched) + row << AntennaModel::typeStr(prAntennaType) + .c_str(); // Passive Repeater Ant + // Type + row << AntennaModel::categoryStr(prAntennaCategory) + .c_str(); // Passive Repeater Ant + // Category + row << UlsFunctionsClass::makeNumber( + prAnt.backtobackTxGain); // Passive Repeater + // Back-To-Back Tx + // Gain + row << UlsFunctionsClass::makeNumber( + prAnt.backtobackRxGain); // Passive Repeater + // Back-To-Back Rx + // Gain + row << UlsFunctionsClass::makeNumber( + prAnt.reflectorHeight); // Passive Repeater + // ULS Reflector + // Height + row << UlsFunctionsClass::makeNumber( + prAnt.reflectorWidth); // Passive Repeater + // ULS Reflector + // Width + row << UlsFunctionsClass::makeNumber( + prAntennaDiameter); // Passive Repeater Ant + // Model Diameter (m) + row << UlsFunctionsClass::makeNumber( + prAntennaMidbandGain); // Passive Repeater + // Ant Model Midband + // Gain (dB) + row << UlsFunctionsClass::makeNumber( + prAntennaReflectorHeight); // Passive + // Repeater Ant + // Model + // Reflector + // Height + row << UlsFunctionsClass::makeNumber( + prAntennaReflectorWidth); // Passive + // Repeater Ant + // Model Reflector + // Width + row << UlsFunctionsClass::makeNumber( + prAnt.lineLoss); // Passive Repeater Line + // Loss + row << UlsFunctionsClass::makeNumber( + prAnt.heightToCenterRAAT); // Passive + // Repeater + // Height to + // Center RAAT Tx + row << UlsFunctionsClass::makeNumber( + prAnt.heightToCenterRAAT); // Passive + // Repeater + // Height to + // Center RAAT Rx + row << UlsFunctionsClass::makeNumber( + prAnt.beamwidth); // Passive Repeater + // Beamwidth + row << UlsFunctionsClass::makeNumber( + segment.segmentLength); // Segment Length + // (km) + } else { + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + } + } + if (anomalousReason.length() > 0) { + row << "0" << anomalousReason; + anomalous.writeRow(row); + anomalousReason = ""; + } else { + wt.writeRow(row); + if (fixedReason.length() > 0) { + row << "1" << fixedReason; + anomalous.writeRow(row); + } + numRecs++; + } + fixedReason = ""; + } + } + } + + std::cout << "US Num Antenna Matched: " << numAntMatch << std::endl; + std::cout << "US Num Antenna Not Matched: " << numAntUnmatch << std::endl; + std::cout << "US NUM Missing Rx Antenna Height: " << numMissingRxAntHeight << std::endl; + std::cout << "US NUM Missing Tx Antenna Height: " << numMissingTxAntHeight << std::endl; + std::cout << "US NUM Missing PR Antenna Height: " << numMissingPRAntHeight << std::endl; + + std::cout << "US Num Frequency Assigned Missing: " << numFreqAssignedMissing << std::endl; + std::cout << "US Num Unable To Get Bandwidth: " << numUnableGetBandwidth << std::endl; + std::cout << "US Num Frequency Upper Band Missing: " << numFreqUpperBandMissing + << std::endl; + std::cout << "US Num Frequency Upper Band Present: " << numFreqUpperBandPresent + << std::endl; + std::cout << "US Num Frequency Inconsistent: " << numFreqInconsistent << std::endl; + std::cout << "US Num Frequency Assigned = Start: " << numAssignedStart << std::endl; + std::cout << "US Num Frequency Assigned = Center: " << numAssignedCenter << std::endl; + std::cout << "US Num Frequency Assigned = Other: " << numAssignedOther << std::endl; + + std::cout << "US Processed " << r.frequencies().count() + << " frequency records and output to file; a total of " << numRecs << " output" + << '\n'; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** processCA ****/ +/******************************************************************************************/ +void processCA(UlsFileReader &r, + int maxNumPassiveRepeater, + CsvWriter &wt, + CsvWriter &anomalous, + FILE * /* fwarn */, + AntennaModelMapClass &antennaModelMap) +{ + int prIdx; + std::string antPfx = (combineAntennaRegionFlag ? "" : "CA:"); + + qDebug() << "--- Beginning path processing"; + + int numRecs = 0; + + int numAntMatch = 0; + int numAntUnmatch = 0; + + for (std::string authorizationNumber : r.authorizationNumberList) { + const QList &prList = r.passiveRepeatersMap( + authorizationNumber.c_str()); + + int numPR = prList.size(); + std::vector idxList(numPR); + + foreach(const StationDataCAClass &station, + r.stationsMap(authorizationNumber.c_str())) + { + QString anomalousReason = ""; + QString fixedReason = ""; + + switch (station.service) { + case 2: + // Do Nothing + break; + case 9: + // Skip this, satellite service + continue; + break; + default: + // Invalid service put in anomalous file + anomalousReason.append("Invalid Service, "); + break; + } + + double startFreq = station.centerFreqMHz - + station.bandwidthMHz / 2; // Lower Band (MHz) + double stopFreq = station.centerFreqMHz + + station.bandwidthMHz / 2; // Upper Band (MHz) + + if (isnan(startFreq) || isnan(stopFreq)) { + anomalousReason.append("NaN frequency value, "); + } else { + bool overlapUnii5 = (stopFreq > + UlsFunctionsClass::unii5StartFreqMHz) && + (startFreq < + UlsFunctionsClass::unii5StopFreqMHz); + bool overlapUnii6 = (stopFreq > + UlsFunctionsClass::unii6StartFreqMHz) && + (startFreq < + UlsFunctionsClass::unii6StopFreqMHz); + bool overlapUnii7 = (stopFreq > + UlsFunctionsClass::unii7StartFreqMHz) && + (startFreq < + UlsFunctionsClass::unii7StopFreqMHz); + bool overlapUnii8 = (stopFreq > + UlsFunctionsClass::unii8StartFreqMHz) && + (startFreq < + UlsFunctionsClass::unii8StopFreqMHz); + + if (!(overlapUnii5 || overlapUnii6 || overlapUnii7 || + overlapUnii8)) { + anomalousReason.append("Out of band, "); + } else if (overlapUnii5 && overlapUnii7) { + anomalousReason.append("Band overlaps both Unii5 and " + "Unii7, "); + } else if (overlapUnii6 && overlapUnii8) { + anomalousReason.append("Band overlaps both Unii6 and " + "Unii8, "); + } + } + + double azimuthPtg, elevationPtg; + + makeLink(station, prList, idxList, azimuthPtg, elevationPtg); + + AntennaModel::CategoryEnum category; + AntennaModelClass *rxAntModel = + antennaModelMap.find(antPfx, + station.antennaModel, + category, + AntennaModel::UnknownCategory); + + AntennaModel::CategoryEnum rxAntennaCategory; + double rxAntennaDiameter; + double rxDiversityDiameter; + double rxAntennaMidbandGain; + std::string rxAntennaModelName; + if (rxAntModel) { + numAntMatch++; + rxAntennaModelName = rxAntModel->name; + rxAntennaCategory = rxAntModel->category; + rxAntennaDiameter = rxAntModel->diameterM; + rxDiversityDiameter = rxAntModel->diameterM; + rxAntennaMidbandGain = rxAntModel->midbandGain; + } else { + numAntUnmatch++; + rxAntennaModelName = ""; + rxAntennaCategory = category; + rxAntennaDiameter = -1.0; + rxDiversityDiameter = -1.0; + rxAntennaMidbandGain = std::numeric_limits::quiet_NaN(); + fixedReason.append("Rx Antenna Model Unmatched"); + } + + QStringList row; + row << "CA"; // Region + row << QString::fromStdString(station.callsign); // Callsign + row << QString("A"); // Status + row << ""; // Radio Service + row << ""; // Entity Name + row << QString::fromStdString( + authorizationNumber); // FRN: Fcc Registration Number / CA: + // authorizationNumber + row << ""; // Grant + row << ""; // Expiration + row << station.inServiceDate.c_str(); // Effective + row << "" // Address + << "" // City + << "" // County + << ""; // State + row << ""; // Common Carrier + row << ""; // Non Common Carrier + row << ""; // Private Comm + row << "Y"; // Fixed + row << "N"; // Mobile + row << "N"; // Radiolocation + row << "N"; // Satellite + row << "N"; // Developmental or STA or Demo + row << ""; // Interconnected + row << "1"; // Path Number + row << "1"; // Tx Location Number + row << "1"; // Tx Antenna Number + row << ""; // Rx Callsign + row << "1"; // Rx Location Number + row << "1"; // Rx Antenna Number + row << "1"; // Frequency Number + row << ""; // 1st Segment Length (km) + + row << UlsFunctionsClass::makeNumber( + station.centerFreqMHz); // Center Frequency (MHz) + row << UlsFunctionsClass::makeNumber( + station.bandwidthMHz); // Bandiwdth (MHz) + + row << UlsFunctionsClass::makeNumber(station.centerFreqMHz - + station.bandwidthMHz / + 2); // Lower Band (MHz) + row << UlsFunctionsClass::makeNumber(station.centerFreqMHz + + station.bandwidthMHz / + 2); // Upper Band (MHz) + + row << ""; // Tolerance (%) + row << ""; // Tx EIRP (dBm) + row << ""; // Auto Tx Pwr Control + row << station.emissionsDesignator.c_str(); // Emissions Designator + row << ""; // Digital Mod Rate + row << station.modulation.c_str(); // Digital Mod Type + + row << ""; // Tx Manufacturer + row << ""; // Tx Model ULS + row << ""; // Tx Model Matched + row << "UNKNOWN"; // Tx Architecture + row << ""; // Tx Location Name + row << ""; // Tx Latitude + row << ""; // Tx Longitude + row << ""; // Tx Ground Elevation (m) + row << ""; // Tx Polarization + row << UlsFunctionsClass::makeNumber(azimuthPtg); // Tx Azimuth Angle (deg) + row << UlsFunctionsClass::makeNumber( + elevationPtg); // Tx Elevation Angle (deg) + row << ""; // Tx Ant Manufacturer + row << ""; // Tx Ant Model + row << ""; // Tx Matched antenna model (blank if unmatched) + row << ""; // Tx Antenna category + row << ""; // Tx Ant Diameter (m) + row << ""; // Tx Ant Midband Gain (dB) + row << "-1"; // Tx Height to Center RAAT (m) + row << ""; // Tx Beamwidth + row << ""; // Tx Gain (dBi) + row << station.stationLocation.c_str(); // Rx Location Name + row << UlsFunctionsClass::makeNumber( + station.latitudeDeg); // Rx Latitude (deg) + row << UlsFunctionsClass::makeNumber( + station.longitudeDeg); // Rx Longitude (deg) + row << UlsFunctionsClass::makeNumber( + station.groundElevation); // Rx Ground Elevation (m) + row << ""; // Rx Manufacturer + row << ""; // Rx Model + row << station.antennaManufacturer.c_str(); // Rx Ant Manufacturer + row << station.antennaModel.c_str(); // Rx Ant Model + row << rxAntennaModelName + .c_str(); // Rx Matched antenna model (blank if unmatched) + row << AntennaModel::categoryStr(rxAntennaCategory) + .c_str(); // Rx Antenna category + row << UlsFunctionsClass::makeNumber( + rxAntennaDiameter); // Rx Ant Diameter (m) + row << UlsFunctionsClass::makeNumber( + rxAntennaMidbandGain); // Rx Ant Midband Gain (dB) + row << UlsFunctionsClass::makeNumber(station.lineLoss); // Rx Line Loss (dB) + row << UlsFunctionsClass::makeNumber( + station.antennaHeightAGL); // Rx Height to Center RAAT (m) + row << UlsFunctionsClass::makeNumber(station.antennaGain); // Rx Gain (dBi) + row << ""; // Rx Diveristy Height (m) + row << UlsFunctionsClass::makeNumber( + rxDiversityDiameter); // Rx Diversity Diameter (m) + row << ""; // Rx Diversity Gain (dBi) + + row << QString::number(numPR); // Num Passive Repeater + for (int prCount = 1; prCount <= maxNumPassiveRepeater; ++prCount) { + if (prCount <= numPR) { + int idxVal = idxList[numPR - prCount]; + + bool repFlag = (idxVal & 0x01 ? false : true); + + prIdx = idxVal / 2; + + const PassiveRepeaterCAClass &pr = prList[prIdx]; + + AntennaModelClass *prAntModel = + antennaModelMap.find(antPfx, + pr.antennaModelA, + category, + AntennaModel::UnknownCategory); + + PassiveRepeaterCAClass::PRTypeEnum prAntennaType; + AntennaModel::CategoryEnum prAntennaCategory; + double prAntennaDiameter; + double prAntennaMidbandGain; + double prAntennaReflectorWidth; + double prAntennaReflectorHeight; + std::string prAntennaModelName; + + if (prAntModel) { + numAntMatch++; + prAntennaModelName = prAntModel->name; + prAntennaCategory = prAntModel->category; + prAntennaDiameter = prAntModel->diameterM; + prAntennaMidbandGain = prAntModel->midbandGain; + prAntennaReflectorWidth = + prAntModel->reflectorWidthM; + prAntennaReflectorHeight = + prAntModel->reflectorHeightM; + } else { + numAntUnmatch++; + prAntennaModelName = ""; + prAntennaCategory = category; + prAntennaDiameter = -1.0; + prAntennaMidbandGain = + std::numeric_limits::quiet_NaN(); + prAntennaReflectorWidth = -1.0; + prAntennaReflectorHeight = -1.0; + fixedReason.append("PR Antenna Model Unmatched"); + } + + prAntennaType = pr.type; + + std::string prAntennaTypeStr; + switch (prAntennaType) { + case PassiveRepeaterCAClass:: + backToBackAntennaPRType: + prAntennaTypeStr = "Ant"; + break; + case PassiveRepeaterCAClass:: + billboardReflectorPRType: + prAntennaTypeStr = "Ref"; + break; + case PassiveRepeaterCAClass::unknownPRType: + prAntennaTypeStr = "UNKNOWN"; + break; + default: + break; + } + + row << ""; // Passive Repeater Location Name + row << UlsFunctionsClass::makeNumber( + pr.latitudeDeg); // Passive Repeater Lat Coords + row << UlsFunctionsClass::makeNumber( + pr.longitudeDeg); // Passive Repeater Lon Coords + row << UlsFunctionsClass::makeNumber( + pr.groundElevation); // Passive Repeater Ground + // Elevation + row << ""; // Passive Repeater Polarization + row << ""; // Passive Repeater Azimuth Angle + row << ""; // Passive Repeater Elevation Angle + row << ""; // Passive Repeater Ant Make + row << pr.antennaModelA + .c_str(); // Passive Repeater Ant Model + row << prAntennaModelName + .c_str(); // Passive Repeater antenna model + // (blank if unmatched) + row << prAntennaTypeStr + .c_str(); // Passive Repeater Ant Type + row << AntennaModel::categoryStr(prAntennaCategory) + .c_str(); // Passive Repeater Ant Category + row << UlsFunctionsClass::makeNumber( + repFlag ? pr.antennaGainA : + pr.antennaGainB); // Passive Repeater + // Back-To-Back Tx Gain + row << UlsFunctionsClass::makeNumber( + repFlag ? pr.antennaGainB : + pr.antennaGainA); // Passive Repeater + // Back-To-Back Rx Gain + row << UlsFunctionsClass::makeNumber( + pr.reflectorHeight); // Passive Repeater ULS + // Reflector Height + row << UlsFunctionsClass::makeNumber( + pr.reflectorWidth); // Passive Repeater ULS + // Reflector Width + row << UlsFunctionsClass::makeNumber( + prAntennaDiameter); // Passive Repeater Ant Model + // Diameter (m) + row << UlsFunctionsClass::makeNumber( + prAntennaMidbandGain); // Passive Repeater Ant Model + // Midband Gain (dB) + row << UlsFunctionsClass::makeNumber( + prAntennaReflectorHeight); // Passive Repeater Ant + // Model Reflector Height + row << UlsFunctionsClass::makeNumber( + prAntennaReflectorWidth); // Passive Repeater Ant + // Model Reflector Width + row << ""; // Passive Repeater Line Loss + row << UlsFunctionsClass::makeNumber( + repFlag ? pr.heightAGLA : + pr.heightAGLB); // Passive Repeater Height + // to Center RAAT Tx + row << UlsFunctionsClass::makeNumber( + repFlag ? pr.heightAGLB : + pr.heightAGLA); // Passive Repeater Height + // to Center RAAT Rx + row << ""; // Passive Repeater Beamwidth + row << ""; // Segment Length (km) + } else { + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + row << ""; + } + } + if (anomalousReason.length() > 0) { + row << "0" << anomalousReason; + anomalous.writeRow(row); + anomalousReason = ""; + } else { + wt.writeRow(row); + if (fixedReason.length() > 0) { + row << "1" << fixedReason; + anomalous.writeRow(row); + } + numRecs++; + } + fixedReason = ""; + } + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** makeLink ****/ +/******************************************************************************************/ +void makeLink(const StationDataCAClass &station, + const QList &prList, + std::vector &idxList, + double &azimuthPtg, + double &elevationPtg) +{ + idxList.clear(); + + std::vector unassignedIdxList; + for (int prIdx = 0; prIdx < prList.size(); ++prIdx) { + unassignedIdxList.push_back(prIdx); + } + + Vector3 position = station.position; + Vector3 pointingVec = station.pointingVec; + azimuthPtg = station.azimuthPtg; + elevationPtg = station.elevationPtg; + while (unassignedIdxList.size()) { + int prIdx, selUIdx; + double maxMetric; + for (int uIdx = 0; uIdx < (int)unassignedIdxList.size(); ++uIdx) { + prIdx = unassignedIdxList[uIdx]; + Vector3 prPosition; + const PassiveRepeaterCAClass &pr = prList[prIdx]; + if (pr.type == PassiveRepeaterCAClass::backToBackAntennaPRType) { + prPosition = (pr.positionA + pr.positionB) / 2; + } else if (pr.type == PassiveRepeaterCAClass::billboardReflectorPRType) { + prPosition = pr.reflectorPosition; + } else { + CORE_DUMP; + } + Vector3 pVec = (prPosition - position).normalized(); + double metric = pVec.dot(pointingVec); + if ((uIdx == 0) || (metric > maxMetric)) { + maxMetric = metric; + selUIdx = uIdx; + } + } + prIdx = unassignedIdxList[selUIdx]; + const PassiveRepeaterCAClass &pr = prList[prIdx]; + if (selUIdx < (int)unassignedIdxList.size() - 1) { + unassignedIdxList[selUIdx] = + unassignedIdxList[unassignedIdxList.size() - 1]; + } + unassignedIdxList.pop_back(); + Vector3 prPosition; + if (pr.type == PassiveRepeaterCAClass::backToBackAntennaPRType) { + prPosition = (pr.positionA + pr.positionB) / 2; + Vector3 pVec = (position - prPosition).normalized(); + double metricA = pVec.dot(pr.pointingVecA); + double metricB = pVec.dot(pr.pointingVecB); + if (metricA > metricB) { + idxList.push_back(2 * prIdx); + pointingVec = pr.pointingVecB; + azimuthPtg = pr.azimuthPtgB; + elevationPtg = pr.elevationPtgB; + } else { + idxList.push_back(2 * prIdx + 1); + pointingVec = pr.pointingVecA; + azimuthPtg = pr.azimuthPtgA; + elevationPtg = pr.elevationPtgA; + } + } else if (pr.type == PassiveRepeaterCAClass::billboardReflectorPRType) { + idxList.push_back(2 * prIdx); + prPosition = pr.reflectorPosition; + Vector3 pVec = (position - prPosition).normalized(); + pointingVec = 2 * pVec.dot(pr.reflectorPointingVec) * + pr.reflectorPointingVec - + pVec; + + Vector3 upVec = prPosition.normalized(); + Vector3 zVec = Vector3(0.0, 0.0, 1.0); + Vector3 eastVec = zVec.cross(upVec).normalized(); + Vector3 northVec = upVec.cross(eastVec); + + elevationPtg = asin(pointingVec.dot(upVec)) * 180.0 / M_PI; + azimuthPtg = atan2(pointingVec.dot(eastVec), pointingVec.dot(northVec)) * + 180.0 / M_PI; + } else { + CORE_DUMP; + } + + position = prPosition; + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** testAntennaModelMap ****/ +/******************************************************************************************/ +void testAntennaModelMap(AntennaModelMapClass &antennaModelMap, + std::string inputFile, + std::string outputFile) +{ + std::ostringstream errStr; + FILE *fin, *fout; + + std::string antPfx = (combineAntennaRegionFlag ? "" : "US:"); + + if (!(fin = fopen(inputFile.c_str(), "rb"))) { + errStr << std::string("ERROR: Unable to open inputFile: \"") << inputFile << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + + if (!(fout = fopen(outputFile.c_str(), "wb"))) { + errStr << std::string("ERROR: Unable to open outputFile: \"") << outputFile << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + + int linenum, fIdx; + std::string line, strval; + + int antennaModelFieldIdx = -1; + + std::vector fieldIdxList; + std::vector fieldLabelList; + fieldIdxList.push_back(&antennaModelFieldIdx); + fieldLabelList.push_back("antennaModel"); + + int fieldIdx; + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + linenum = 0; + bool foundLabelLine = false; + while (fgetline(fin, line, false)) { + linenum++; + std::vector fieldList = splitCSV(line); + std::string fixedStr = ""; + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if ((lineType == unknownLineType) && (!foundLabelLine)) { + lineType = labelLineType; + foundLabelLine = 1; + } + if ((lineType == unknownLineType) && (foundLabelLine)) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + bool found; + std::string field; + switch (lineType) { + case labelLineType: + for (fieldIdx = 0; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + + // std::cout << "FIELD: \"" << field << "\"" << std::endl; + + found = false; + for (fIdx = 0; + (fIdx < (int)fieldLabelList.size()) && (!found); + fIdx++) { + if (field == fieldLabelList.at(fIdx)) { + *fieldIdxList.at(fIdx) = fieldIdx; + found = true; + } + } + } + + for (fIdx = 0; fIdx < (int)fieldIdxList.size(); fIdx++) { + if (*fieldIdxList.at(fIdx) == -1) { + errStr << "ERROR: Invalid input file \"" + << inputFile << "\" label line missing \"" + << fieldLabelList.at(fIdx) << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + } + + fprintf(fout, "%s,matchedAntennaModel\n", line.c_str()); + + break; + case dataLineType: { + strval = fieldList.at(antennaModelFieldIdx); + + AntennaModel::CategoryEnum category; + AntennaModelClass *antModel = + antennaModelMap.find(antPfx, + strval, + category, + AntennaModel::UnknownCategory); + + std::string matchedModelName; + if (antModel) { + matchedModelName = antModel->name; + } else { + matchedModelName = ""; + } + + fprintf(fout, "%s,%s\n", line.c_str(), matchedModelName.c_str()); + + } break; + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + CORE_DUMP; + break; + } + } + + if (fin) { + fclose(fin); + } + if (fout) { + fclose(fout); + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** testTransmitterModelMap ****/ +/******************************************************************************************/ +void testTransmitterModelMap(TransmitterModelMapClass &transmitterModelMap, + std::string inputFile, + std::string outputFile) +{ + std::ostringstream errStr; + FILE *fin, *fout; + + transmitterModelMap.checkPrefixValues(); + + int numTX = 0; + int numMatch = 0; + + if (!(fin = fopen(inputFile.c_str(), "rb"))) { + errStr << std::string("ERROR: Unable to open inputFile: \"") << inputFile << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + + if (!(fout = fopen(outputFile.c_str(), "wb"))) { + errStr << std::string("ERROR: Unable to open outputFile: \"") << outputFile << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + + int linenum, fIdx; + std::string line, strval; + + int transmitterModelFieldIdx = -1; + + std::vector fieldIdxList; + std::vector fieldLabelList; + fieldIdxList.push_back(&transmitterModelFieldIdx); + fieldLabelList.push_back("transmitterModel"); + + int fieldIdx; + + enum LineTypeEnum { labelLineType, dataLineType, ignoreLineType, unknownLineType }; + + LineTypeEnum lineType; + + linenum = 0; + bool foundLabelLine = false; + while (fgetline(fin, line, false)) { + linenum++; + std::vector fieldList = splitCSV(line); + std::string fixedStr = ""; + + lineType = unknownLineType; + /**************************************************************************/ + /**** Determine line type ****/ + /**************************************************************************/ + if (fieldList.size() == 0) { + lineType = ignoreLineType; + } else { + fIdx = fieldList[0].find_first_not_of(' '); + if (fIdx == (int)std::string::npos) { + if (fieldList.size() == 1) { + lineType = ignoreLineType; + } + } else { + if (fieldList[0].at(fIdx) == '#') { + lineType = ignoreLineType; + } + } + } + + if ((lineType == unknownLineType) && (!foundLabelLine)) { + lineType = labelLineType; + foundLabelLine = 1; + } + if ((lineType == unknownLineType) && (foundLabelLine)) { + lineType = dataLineType; + } + /**************************************************************************/ + + /**************************************************************************/ + /**** Process Line ****/ + /**************************************************************************/ + bool found; + std::string field; + switch (lineType) { + case labelLineType: + for (fieldIdx = 0; fieldIdx < (int)fieldList.size(); fieldIdx++) { + field = fieldList.at(fieldIdx); + + // std::cout << "FIELD: \"" << field << "\"" << std::endl; + + found = false; + for (fIdx = 0; + (fIdx < (int)fieldLabelList.size()) && (!found); + fIdx++) { + if (field == fieldLabelList.at(fIdx)) { + *fieldIdxList.at(fIdx) = fieldIdx; + found = true; + } + } + } + + for (fIdx = 0; fIdx < (int)fieldIdxList.size(); fIdx++) { + if (*fieldIdxList.at(fIdx) == -1) { + errStr << "ERROR: Invalid input file \"" + << inputFile << "\" label line missing \"" + << fieldLabelList.at(fIdx) << "\"" + << std::endl; + throw std::runtime_error(errStr.str()); + } + } + + fprintf(fout, "%s,matchedTransmitterModel\n", line.c_str()); + + break; + case dataLineType: { + numTX++; + strval = fieldList.at(transmitterModelFieldIdx); + + TransmitterModelClass *transmitterModel = transmitterModelMap.find( + strval); + + std::string matchedModelName; + if (transmitterModel) { + matchedModelName = transmitterModel->name; + numMatch++; + } else { + matchedModelName = ""; + } + + fprintf(fout, "%s,%s\n", line.c_str(), matchedModelName.c_str()); + + } break; + case ignoreLineType: + case unknownLineType: + // do nothing + break; + default: + CORE_DUMP; + break; + } + } + + if (fin) { + fclose(fin); + } + if (fout) { + fclose(fout); + } + + std::cout << "NUM TX: " << numTX << std::endl; + std::cout << "NUM MATCH: " << numMatch << std::endl; +} +/******************************************************************************************/ diff --git a/src/coalition_ulsprocessor/version.txt b/src/coalition_ulsprocessor/version.txt new file mode 100644 index 0000000..f0bb29e --- /dev/null +++ b/src/coalition_ulsprocessor/version.txt @@ -0,0 +1 @@ +1.3.0 diff --git a/src/ratapi/CMakeLists.txt b/src/ratapi/CMakeLists.txt new file mode 100644 index 0000000..be8a45f --- /dev/null +++ b/src/ratapi/CMakeLists.txt @@ -0,0 +1,9 @@ +# Python packaging +set(TGT_NAME "ratapi") + +add_dist_pythonlibrary( + TARGET ${TGT_NAME} + SETUP_TEMPLATE setup.py.in + SOURCEDIR "${CMAKE_CURRENT_SOURCE_DIR}" + COMPONENT runtime +) diff --git a/src/ratapi/ratapi/__init__.py b/src/ratapi/ratapi/__init__.py new file mode 100644 index 0000000..67e297b --- /dev/null +++ b/src/ratapi/ratapi/__init__.py @@ -0,0 +1,13 @@ +''' Package and app definition. +''' + +from .app import create_app + +if True: + # Workaround for apache/mod_ssl leaving OpenSSL errors queued + try: + from cryptography.hazmat.bindings._openssl import lib as libopenssl + except ImportError: + from cryptography.hazmat.bindings._rust import _openssl + libopenssl = _openssl.lib + libopenssl.ERR_clear_error() diff --git a/src/ratapi/ratapi/app.py b/src/ratapi/ratapi/app.py new file mode 100644 index 0000000..258f440 --- /dev/null +++ b/src/ratapi/ratapi/app.py @@ -0,0 +1,421 @@ +# This Python file uses the following encoding: utf-8 +# +# Portions copyright © 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate +# affiliate that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy +# of which is included with this software program. +# + +''' Flask application generation. +''' +import appcfg +import sys +import os +import datetime +import logging +import flask +import requests +import platform +from sqlalchemy import exc +from fst import DataIf +from afcmodels.base import db +from afcmodels.aaa import User +import als +import prometheus_utils +import prometheus_client + +#: Logger for this module +LOGGER = logging.getLogger(__name__) + +#: Current file path +owndir = os.path.abspath(os.path.dirname(__file__)) + +# Metrics for autoscaling +prometheus_metric_flask_workers = \ + prometheus_client.Gauge('msghnd_flask_workers', + 'Total number of Flask workers in container', + ['host'], multiprocess_mode='max') +prometheus_metric_flask_workers.labels(host=platform.node()) +prometheus_metric_flask_workers = \ + prometheus_metric_flask_workers.labels(host=platform.node()).\ + set(os.environ.get('AFC_MSGHND_WORKERS', 0)) +prometheus_metric_flask_active_reqs = \ + prometheus_client.Gauge('msghnd_flask_active_reqs', + 'Number of currently processed Flask requests', + ['host'], multiprocess_mode='sum') +prometheus_metric_flask_active_reqs = \ + prometheus_metric_flask_active_reqs.labels(host=platform.node()) + + +def create_app(config_override=None): + ''' Build up a WSGI application for this server. + + :param config_override: Individual variables from `config` to override. + :type config_override: dict + :return: A Flask application object. + :rtype: :py:cls:`flask.Flask` + ''' + from flask_migrate import Migrate + from xdg import BaseDirectory + + # Child members + from . import views, util + from flask_wtf.csrf import CSRFProtect + + flaskapp = flask.Flask(__name__.split('.')[0]) + flaskapp.response_class = util.Response + + # default config state from module + flaskapp.config.from_object(appcfg) + flaskapp.config.from_object(appcfg.BrokerConfigurator()) + flaskapp.config.from_object(appcfg.ObjstConfig()) + flaskapp.config.from_object(appcfg.OIDCConfigurator()) + flaskapp.config.from_object(appcfg.RatApiConfigurator()) + + # initial override from system config + config_path = BaseDirectory.load_first_config('fbrat', 'ratapi.conf') + if config_path: + flaskapp.config.from_pyfile(config_path) + # final overrides for this instance + if config_override: + flaskapp.config.update(config_override) + + # always autoescape + flaskapp.select_jinja_autoescape = lambda _filename: True + + # remove any existing flaskapp-specific handlers + del flaskapp.logger.handlers[:] + # Logging just after config + root_logger = logging.getLogger() + # Root logging level + root_logger.setLevel(flaskapp.config['AFC_RATAPI_LOG_LEVEL']) + # Apply handlers to logger + for handler in flaskapp.config['LOG_HANDLERS']: + root_logger.addHandler(handler) + LOGGER.info('Logging at level %s', flaskapp.config['AFC_RATAPI_LOG_LEVEL']) + + als.als_initialize() + + LOGGER.debug('BROKER_URL %s', flaskapp.config['BROKER_URL']) + + db.init_app(flaskapp) + Migrate( + flaskapp, db, directory=os.path.join(owndir, 'migrations')) + + if flaskapp.config['OIDC_LOGIN']: + from flask_login import LoginManager + login_manager = LoginManager() + login_manager.init_app(flaskapp) + + if (flaskapp.config['OIDC_DISCOVERY_URL']): + endpoints = requests.get( + flaskapp.config['OIDC_DISCOVERY_URL'], headers={ + 'Accept': 'application/json'}).json() + flaskapp.config['OIDC_ORG_AUTH_URL'] = endpoints['authorization_endpoint'] + flaskapp.config['OIDC_ORG_TOKEN_URL'] = endpoints['token_endpoint'] + flaskapp.config['OIDC_ORG_USER_INFO_URL'] = endpoints['userinfo_endpoint'] + + @login_manager.user_loader + def load_user(_id): + ''' Load user invoked from flask login + ''' + return User.get(_id) + else: + # Non OIDC login. + from flask_user import UserManager + user_manager = UserManager(flaskapp, db, User) + + @flaskapp.before_request + def log_user_access(): + if flask.request.endpoint == 'user.logout': + from flask_login import current_user + try: + LOGGER.debug('user:%s logout ', current_user.username) + als.als_json_log('user_access', + {'action': 'logout', + 'user': current_user.username, + 'from': flask.request.remote_addr}) + except BaseException: + LOGGER.debug('user:%s logout ', 'unknown') + als.als_json_log( + 'user_access', { + 'action': 'logout', 'user': 'unknown', 'from': flask.request.remote_addr}) + + @flaskapp.after_request + def log_user_accessed(response): + if flask.request.method == 'POST' and flask.request.endpoint == 'user.login': + LOGGER.debug( + 'user:%s login status %d', + flask.request.form['username'], + response.status_code) + if response.status_code != 302: + als.als_json_log('user_access', + {'action': 'login', + 'user': flask.request.form['username'], + 'from': flask.request.remote_addr, + 'status': response.status_code}) + else: + als.als_json_log('user_access', + {'action': 'login', + 'user': flask.request.form['username'], + 'from': flask.request.remote_addr, + 'status': 'success'}) + + return response + + # Check configuration + state_path = flaskapp.config['STATE_ROOT_PATH'] + nfs_mount_path = flaskapp.config['NFS_MOUNT_PATH'] + if not os.path.exists(state_path): + try: + os.makedirs(state_path) + except OSError: + LOGGER.error('Failed creating state directory "%s"', state_path) + if not os.path.exists(flaskapp.config['TASK_QUEUE']): + raise Exception('Missing task directory') + + # Static file dispatchers + if flaskapp.config['AFC_APP_TYPE'] == 'server': + csrf = CSRFProtect(flaskapp) + + @flaskapp.before_request + def check_csrf(): + csrf.protect() + + if not os.path.exists(os.path.join( + nfs_mount_path, 'rat_transfer', 'frequency_bands')): + os.makedirs(os.path.join(nfs_mount_path, + 'rat_transfer', 'frequency_bands')) + + from werkzeug.middleware.dispatcher import DispatcherMiddleware + from wsgidav import wsgidav_app + from wsgidav.fs_dav_provider import FilesystemProvider + + # get static web file location + webdata_paths = BaseDirectory.load_data_paths('fbrat', 'www') + # Temporary solution, do not raise exception while web module + # not installed. + if not webdata_paths: + raise RuntimeError( + 'Web data directory "fbrat/www" is not available') + + # get uls database directory + uls_databases = os.path.join( + flaskapp.config['NFS_MOUNT_PATH'], 'rat_transfer', 'ULS_Database') + if not os.path.exists(uls_databases): + os.makedirs(uls_databases) + + # get static uls data path + if flaskapp.config['DEFAULT_ULS_DIR'] is None: + LOGGER.error("No default ULS directory found in path search") + + # get static antenna patterns directory + antenna_patterns = os.path.join( + flaskapp.config['NFS_MOUNT_PATH'], + 'rat_transfer', + 'Antenna_Patterns') + if not os.path.exists(antenna_patterns): + os.makedirs(antenna_patterns) + + # List of (URL paths from root URL, absolute local filesystem paths, + # read-only boolean) + dav_trees = ( + ('/www', next(webdata_paths), True), + ('/ratapi/v1/files/uls_db', uls_databases, False), + ('/ratapi/v1/files/antenna_pattern', antenna_patterns, False) + ) + + dav_wsgi_apps = {} + for (url_path, fs_path, read_only) in dav_trees: + if fs_path is None: + flaskapp.logger.debug( + 'skipping dav export: {0}'.format(url_path)) + continue + if not os.path.isdir(fs_path): + flaskapp.logger.error( + 'Missing DAV export path "{0}"'.format(fs_path)) + continue + + dav_config = wsgidav_app.DEFAULT_CONFIG.copy() + dav_config.update({ + # Absolute root path for HREFs + 'mount_path': flaskapp.config['APPLICATION_ROOT'] + url_path, + 'provider_mapping': { + '/': FilesystemProvider(fs_path, readonly=read_only), + }, + 'http_authenticator.trusted_auth_header': 'REMOTE_USER', + 'verbose': (0, 1)[flaskapp.config['DEBUG']], + 'enable_loggers': ['wsgidav'], + 'property_manager': False, # True: use property_manager.PropertyManager + 'lock_manager': True, # True: use lock_manager.LockManager + # None: domain_controller.WsgiDAVDomainController(user_mapping) + 'http_authenticator.domaincontroller': None, + }) + # dav_wsgi_apps[app_sub_path] = wsgidav_app.WsgiDAVApp(dav_config) + dav_wsgi_apps[url_path] = wsgidav_app.WsgiDAVApp(dav_config) + # Join together all sub-path DAV apps + flaskapp.wsgi_app = DispatcherMiddleware( + flaskapp.wsgi_app, dav_wsgi_apps) + + # set prefix middleware + flaskapp.wsgi_app = util.PrefixMiddleware( + flaskapp.wsgi_app, prefix=flaskapp.config['APPLICATION_ROOT']) + # set header middleware + flaskapp.wsgi_app = util.HeadersMiddleware( + flaskapp.wsgi_app) + + # User authentication wraps all others + if False: + # JWK wrapper + # This is the same procesing as done by jwcrypto v0.4+ but not present + # in 0.3 + with open(flaskapp.config['SIGNING_KEY_FILE'], 'rb') as pemfile: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + from jwcrypto.common import base64url_encode + from binascii import unhexlify + from jwcrypto import jwk + + def encode_int(val): + intg = hex(val).rstrip('L').lstrip('0x') + return base64url_encode( + unhexlify((len(intg) % 2) * '0' + intg)) + + keydata = serialization.load_pem_private_key( + pemfile.read(), password=None, backend=default_backend()) + pn = keydata.private_numbers() + params = dict( + kty='RSA', + n=encode_int(pn.public_numbers.n), + e=encode_int(pn.public_numbers.e), + d=encode_int(pn.d), + p=encode_int(pn.p), + q=encode_int(pn.q), + dp=encode_int(pn.dmp1), + dq=encode_int(pn.dmq1), + qi=encode_int(pn.iqmp) + ) + privkey = jwk.JWK(**params) + + unsec = [ + '^/$', + '^/cpoinfo$', + '^/favicon.ico$', + '^/static/?', + ] + auth_cfg = dict( + dbus_conn=flaskapp.extensions['dbus'], + db_sess=flaskapp.extensions['dbsessmgr'], + privkey=privkey, + sess_idle_expire=flaskapp.config['SESSION_IDLE_EXPIRE'], + unsecured=unsec, + allow_cookie=flaskapp.config['AUTHN_USE_COOKIE'], + ) + authn_middle = views.authn.AuthRequiredMiddleware( # pylint: disable=no-member + flaskapp, **auth_cfg) + flaskapp.extensions['authn_middle'] = authn_middle + flaskapp.wsgi_app = authn_middle + auth_lookup = authn_middle.get_path_dict() + else: + flaskapp.extensions['authn_middle'] = None + # Dummy data needed for cpoinfo + auth_lookup = { + 'auth.login': '', + 'auth.logout': '', + 'auth.info': '', + } + + #: full set of external dotted names + ext_lookup = dict(auth_lookup) + ext_lookup.update({ + 'www.index': '/www/index.html', + 'files.uls_db': '/ratapi/v1/files/uls_db', + 'files.antenna_pattern': '/ratapi/v1/files/antenna_pattern', + }) + + def external_url_handler(error, endpoint, _values): + ''' Looks up an external URL when `url_for` cannot build a URL. + + :param endpoint: the endpoint of the URL (name of the function) + :param values: the variable arguments of the URL rule + :return: The full URL + ''' + LOGGER.debug("looking for endpoint: %s", endpoint) + url = ext_lookup.get(endpoint, None) + if url is None: + # External lookup did not have a URL. + # Re-raise the BuildError, in context of original traceback. + exc_type, exc_value, traceback = sys.exc_info() + if exc_value is error: + raise exc_type(exc_value).with_traceback(traceback) + else: + raise error + # url_for will use this result, instead of raising BuildError. + val = flaskapp.config['APPLICATION_ROOT'] + url + LOGGER.debug("found endpoint: %s", val) + return val + + flaskapp.url_build_error_handlers.append(external_url_handler) + + def redirector(name, code=301): + ''' A view redirector function. + + :param name: The endpoint name to redirect to. + :param code: The redirect code. + :return: The view function, which passes all kwargs to the view. + ''' + + def view(**kwargs): + from .util import redirect + return redirect(flask.url_for(name, **kwargs), code=code) + + return view + + # check database + with flaskapp.app_context(): + try: + user = db.session.query(User).first() # pylint: disable=no-member + + except exc.SQLAlchemyError as e: + if 'relation "aaa_user" does not exist' in str(e.args): + LOGGER.error("ERROR - Missing users in the database.\n" + "Create using following command sequense:\n" + " rat-manage-api db-create\n") + else: + LOGGER.error("Database is in old format.\n" + "Upgrade using following command sequence:\n" + " rat-manage-api db-upgrade") + flaskapp.config['UPGRADE_REQ'] = True + + # Actual resources + flaskapp.add_url_rule( + '/', 'root', view_func=redirector('www.index', code=302)) + if flaskapp.config['AFC_APP_TYPE'] == 'server': + views.paws.create_handler(flaskapp, '/paws') + flaskapp.add_url_rule('/paws', 'paws.browse', + view_func=redirector('browse.index', code=302)) + if ('AFC_MSGHND_WORKERS' in os.environ) and \ + (prometheus_utils.multiprocess_prometheus_configured()): + flaskapp.add_url_rule( + '/metrics', view_func=prometheus_utils.multiprocess_flask_metrics) + + @flaskapp.before_request + def inc_active_counter_metric(): + prometheus_metric_flask_active_reqs.inc() + + @flaskapp.after_request + def dec_active_counter_metric(response): + prometheus_metric_flask_active_reqs.dec() + return response + + flaskapp.register_blueprint(views.ratapi.module, url_prefix='/ratapi/v1') + flaskapp.register_blueprint(views.ratafc.module, url_prefix='/ap-afc') + flaskapp.register_blueprint(views.auth.module, url_prefix='/auth') + flaskapp.register_blueprint(views.admin.module, url_prefix='/admin') + # catch all invalid paths and redirect + if not flaskapp.config['DEBUG']: + flaskapp.add_url_rule('/', 'any', + view_func=redirector('www.index', code=302)) + + return flaskapp diff --git a/src/ratapi/ratapi/cmd_utils.py b/src/ratapi/ratapi/cmd_utils.py new file mode 100644 index 0000000..5095e0d --- /dev/null +++ b/src/ratapi/ratapi/cmd_utils.py @@ -0,0 +1,511 @@ +''' Utilities related to command argument parsing and sub-commands. +''' + +import argparse +import logging + +#: logger for this module +LOGGER = logging.getLogger(__name__) + + +def packageversion(pkg_name): + ''' Get the version identifier for a python package. + + :param str pkg_name: The package name to get the version for. + :return: The version identifier or None. + :rtype: str + ''' + import pkg_resources + try: + return pkg_resources.require(pkg_name)[0].version + except Exception as err: + LOGGER.error('Failed to fetch package %s version: %s', pkg_name, err) + return None + + +class CmdMixin(object): + ''' Interface for command mixin classes. + + Mixins provide argument parser manipulation and helper functions + based on those arguments. + ''' + + #: List of function names from this class to donate + donated_funcs = [] + #: List of other mixin classes depended-upon by this class + depend_mixins = [] + + def __init__(self): + self.__cmd = None + + def __str__(self, *args, **kwargs): + ''' Display the type of this mixin. + ''' + return str(self.__class__.__name__) + + def set_command(self, cmd): + ''' Register this mixin for a specific command. + + :param cmd: The command which is using this mixin object. + :type cmd: :py:cls:`Command` + ''' + self.__cmd = cmd + + def get_command(self): + ''' Get the command associated with this mixin. + + :return: The associated command, which may be None. + :rtype: :py:cls:`Command` + ''' + return self.__cmd + + def get_logger(self): + ''' Get the command logger for this mixin. + + :return: A logger object. + ''' + if self.__cmd: + return self.__cmd.logger + else: + return LOGGER + + def donate_funcs(self): + ''' Get a set of functions to be donated to the parent Command object. + + :return: A dictionary of function names to callables. + :rtype: dict + ''' + funcs = dict() + for name in self.donated_funcs: + funcs[name] = getattr(self, name) + return funcs + + def config_args(self, parser): + ''' Add options to the command parser object. + + The default behavior is to add no command-specific options or arguments. + :param parser: The command-line parser to manipulate. + :type parser: :py:class:`argparse.ArgumentParser` + ''' + pass + + +class Command(object): + ''' Abstract base class for sub-command handler. + + Class attributes useful to define commands are: + + .. py:attribute:: action_name + The command-line name of this command, also used for the instance + logger name. + + .. py:attribute:: action_help + Text for the auto-generated command-line help information. + + .. py:attribute:: logger + An instance-level :py:class:`logging.logger` object usable by + derived classes. + + Constructor arguments are: + :param action_name: The per-instance action name, defaults to + :py:attr:`Command.action_name` class attribute. + ''' + + #: Default action name for all instances of this class + action_name = None + #: Default action help for all instances of this class + action_help = None + #: Initial set of mixins for this class + mixin_classes = [] + + def __init__(self, *args, **kwargs): + action_name = kwargs.get('action_name') + if action_name: + self.action_name = action_name + if not self.action_name: + raise RuntimeError( + 'Command {0} must define an "action_name"'.format(self)) + self.logger = logging.getLogger('action.' + self.action_name) + + self._mixins = [] + + # scan mixin dependencies + unchecked_classes = set(self.mixin_classes) + all_mixin_classes = set() + while unchecked_classes: + # iterate on copy + add_classes = list(unchecked_classes) + unchecked_classes = set() + for cls in add_classes: + all_mixin_classes.add(cls) + unchecked_classes = unchecked_classes.union( + set(cls.depend_mixins)) + self.logger.debug('Using command mixins: %s', [ + str(obj) for obj in all_mixin_classes]) + + # actually instantiate the mixins + for cls in all_mixin_classes: + self.add_mixin(cls()) + + def add_mixin(self, obj): + ''' Add a new mixin to this command. + + :param obj: The mixin object to add. + :type obj: :py:cls:`CmdMixin` or similar + ''' + self._mixins.append(obj) + obj.set_command(self) + + for (name, func) in obj.donate_funcs().items(): + setattr(self, name, func) + + def apply_mixin_args(self, parser): + ''' Apply all mixin augmentation to a command parser. + + :param parser: The sub-parser to manipulate. + :type parser: :py:class:`argparse.ArgumentParser` + ''' + for obj in self._mixins: + obj.config_args(parser) + + def config_args(self, parser): + ''' Add options to the command sub-parser object. + + The default behavior is to add no command-specific options or arguments. + :param parser: The sub-parser to manipulate. + :type parser: :py:class:`argparse.ArgumentParser` + ''' + self.apply_mixin_args(parser) + + def run(self, args): + ''' Execute the command. + :param args: The full set of command-line arguments. + :type args: :py:cls:`argparse.Namespace` + :return: The status code for this command run. + :rtype: int or None + ''' + raise NotImplementedError() + + +class CommandSet(object): + ''' A collection of :py:class:`Command` definition objects. + The initial command set is empty (see :py:meth:`add_command`). + + :param init_parser: An initial argument parser to append per-command + sub-parsers. + :type init_parser: :py:class:`argparse.ArgumentParser` + ''' + + def __init__(self, dest_suffix=None): + if dest_suffix is None: + dest_suffix = 'root' + self.action_dest = 'action-' + dest_suffix + + self._cmdobjs = {} + self._cmddefns = [] + + def add_command(self, cmd): + ''' Add a new commad action to this set. + This must be called before :py:func:`config_args` is used. + + :param cmd: The command object to add. + :type cmd: :py:class:`Command` + ''' + name = cmd.action_name + if not name: + raise ValueError('Undefined action_name') + if name in self._cmdobjs: + raise KeyError('Duplicate command name "{0}"'.format(name)) + + # find first non-None help text + helpsources = [cmd.action_help, cmd.__doc__] + helptext = next( + (item for item in helpsources if item is not None), None) + + self._cmdobjs[name] = cmd + self._cmddefns.append({ + 'name': name, + 'helptext': helptext, + 'command': cmd, + }) + + def config_args(self, parser): + ''' Add options to the command parser object. + + The default behavior is to add no command-specific options or arguments. + :param parser: The command-line parser to manipulate. + :type parser: :py:class:`argparse.ArgumentParser` + ''' + subparsers = parser.add_subparsers( + dest=self.action_dest, + metavar='SUB_COMMAND', + help='The maintenance action to perform.') + + for defn in self._cmddefns: + sub_parser = subparsers.add_parser( + defn['name'], help=defn['helptext']) + defn['command'].config_args(sub_parser) + + def run(self, args): + ''' Execute the command. + :param args: The full set of command-line arguments. + :type args: :py:cls:`argparse.Namespace` + :return: The status code for this command run. + :rtype: int or None + ''' + try: + cmdname = getattr(args, self.action_dest) + except AttributeError: + raise ValueError('No command given') + try: + cmd = self._cmdobjs[cmdname] + except KeyError: + raise ValueError('Unknown command "{0}"'.format(args.action)) + + return cmd.run(args) + + +class NestingCommand(Command): + ''' A command which is itself contains a command set. + + The default set of sub-commands present are defined by the + :py:cvar:`sub_classes` variable. + ''' + + #: Set of classes to be instantiated as sub-commands + sub_classes = [] + + def __init__(self, *args, **kwargs): + Command.__init__(self, *args, **kwargs) + + self._cset = CommandSet(dest_suffix=self.action_name) + + for cls in self.sub_classes: + self._cset.add_command(cls()) + + def config_args(self, parser): + self._cset.config_args(parser) + + def run(self, args): + return self._cset.run(args) + + +class RootCommandSet(CommandSet): + ''' Shared top-level options for administrative utility programs. + ''' + + def __init__(self, *args, **kwargs): + self.version_name = kwargs.pop('version_name', None) + CommandSet.__init__(self, *args, **kwargs) + + def parse_and_run(self, argv): + ''' Parse command-line arguments and execute command actions. + + :param argv: The full set of command-line arguments. + :type argv: list of str + :return: The status code for this command run. + :rtype: int or None + ''' + import sys + from .cmd_log import configure_logging # pylint: disable=import-error + + parser = argparse.ArgumentParser() + if self.version_name is not None: + dispver = '%(prog)s {0}'.format(self.version_name) + parser.add_argument('--version', action='version', + version=dispver) + parser.add_argument('--log-level', dest='log_level', default='info', + metavar='LEVEL', + help='Console logging lowest level displayed.') + self.config_args(parser) + + args = parser.parse_args(argv[1:]) + log_level_name = args.log_level.upper() + configure_logging(level=log_level_name, stream=sys.stderr) + if log_level_name == 'DEBUG': + logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) + + LOGGER.debug('Running with args: {0}'.format(args)) + try: + return self.run(args) + except Exception as err: + LOGGER.error('Failed to run: ({0}) {1}'.format( + err.__class__.__name__, err)) + if log_level_name == 'DEBUG': + raise + return 1 + + +class DbusMixin(CmdMixin): + ''' Add an argument to override dbus address and a function to + obtain a dbus connection. + ''' + donated_funcs = ['_dbus_conn'] + + def config_args(self, sub_parser): + sub_parser.add_argument( + '--dbus-address', + type=str, + default=None, + help='D-Bus address to use instead of system bus') + + def _dbus_conn_addr(self, args): + ''' Get the D-Bus address (or standard bus type) to connect to. + + :param args: Command argument namespace. + :type args: object + :return: The bus address. + ''' + import dbus + + addr = args.dbus_address + if not addr: + addr = dbus.bus.BusConnection.TYPE_SESSION + return addr + + def _dbus_conn(self, args): + ''' Open a D-Bus connection. + + :param args: Command argument namespace. + :type args: object + :return: The bus connection. + :rtype: :py:cls:`dbus.bus.BusConnection` + ''' + import dbus + + addr = self._dbus_conn_addr(args) + self.get_logger().debug('Connecting to dbus address "%s"', addr) + return dbus.bus.BusConnection(addr) + + +class ConfFileMixin(CmdMixin): + ''' Add an argument to override config path and a function to + obtain and cache config file contents. + ''' + donated_funcs = ['_get_config_xml'] + + def __init__(self, *args, **kwargs): + import lxml.etree as etree + + CmdMixin.__init__(self, *args, **kwargs) + self._conffiles = {} + self.parser = etree.XMLParser() + + def config_args(self, sub_parser): + sub_parser.add_argument( + '--configfile', + type=str, + default=self.config_file_path, + help='Daemon config file to read from instead of system default') + + def _get_config_xml(self, args): + ''' Read and cache a configuration file as XML document. + + :param args: The parsed command arguments. + :type args: :py:cls:`argparse.Namespace` + :return: An XML DOM document. + ''' + import lxml.etree as etree + + filepath = args.configfile + if filepath in self._conffiles: + return self._conffiles[filepath] + + self.get_logger().debug('Loading config from "%s"...', filepath) + try: + tree = etree.parse(filepath, parser=self.parser) + except Exception as err: + self.get_logger().debug('Failed with: %s', err) + raise + self.get_logger().debug('Loaded as "%s"', tree.getroot().tag) + + self._conffiles[filepath] = tree + return tree + + +class RdbMixin(CmdMixin): + ''' Add an argument to override config path and a function to + extract database context manager from config. + ''' + depend_mixins = [ConfFileMixin] + donated_funcs = ['_db_contextmgr'] + + #: The XML element (etree.QName object) to extract from the configuration file + config_file_element = None + + def _db_contextmgr(self, args): + ''' Get a DB connection context manager. + + :param args: The parsed command arguments. + :type args: :py:cls:`argparse.Namespace` + :return: A context manager for DB connections. + ''' + from cpodb.utils import session_context_manager # pylint: disable=import-error + + tree = self.get_command()._get_config_xml(args) + try: + db_uri = tree.find(self.config_file_element).text + except BaseException: + raise RuntimeError('Unable to find config parameter "{0}"'.format( + self.config_file_element)) + return session_context_manager(db_uri) + + +class FileMixin(CmdMixin): + ''' Handle file names including stdin/out default. + ''' + donated_funcs = ['_open_file'] + + def _open_file(self, file_path, mode): + ''' Open a file on the local filesystem, or stdin/stdout. + + :param file_path: The file to open, or None for stdin/stdout. + :type file_path: str or None + ''' + if file_path is None: + import sys + if 'r' in mode: + return sys.stdin + elif 'w' in mode: + return sys.stdout + return open(file_path, mode) + + +def value_format(val): + ''' Provide standard formatting for output values. + + :param val: The value to format. + :return: The formatted value + ''' + import numbers + import datetime + from .time_utils import TIME_FORMAT_EXTENDED # pylint: disable=import-error + + if isinstance(val, numbers.Integral): + return '{:,}'.format(val) + if isinstance(val, datetime.datetime): + return datetime.datetime.strftime(val, TIME_FORMAT_EXTENDED) + else: + return val + + +class ColDefn(object): + ''' Represent a table column schema for use with :py:mod:`prettytable`. + + :param key: The dictionary key for the column. + :param heading: The human-readable text heading for the column. + ''' + + def __init__(self, key, heading=None, align=None): + self.key = key + if not heading: + heading = key + self.heading = heading + if not align: + align = 'r' + self.align = align + + def format(self, val): + ''' Apply locale-aware number formatting. + ''' + return value_format(val) diff --git a/src/ratapi/ratapi/db/.gitignore b/src/ratapi/ratapi/db/.gitignore new file mode 100644 index 0000000..0904be9 --- /dev/null +++ b/src/ratapi/ratapi/db/.gitignore @@ -0,0 +1,5 @@ +*.pyc +log.txt +run_python.py +mfk_* + diff --git a/src/ratapi/ratapi/db/__init__.py b/src/ratapi/ratapi/db/__init__.py new file mode 100644 index 0000000..6cea0f5 --- /dev/null +++ b/src/ratapi/ratapi/db/__init__.py @@ -0,0 +1,2 @@ +''' Modules with functions and datastructures to manage and manipulate afc-engine data files. ''' +from . import models, generators diff --git a/src/ratapi/ratapi/db/antenna_model_list.csv b/src/ratapi/ratapi/db/antenna_model_list.csv new file mode 100644 index 0000000..e69de29 diff --git a/src/ratapi/ratapi/db/antenna_model_map.csv b/src/ratapi/ratapi/db/antenna_model_map.csv new file mode 100644 index 0000000..b485d31 --- /dev/null +++ b/src/ratapi/ratapi/db/antenna_model_map.csv @@ -0,0 +1 @@ +regex,Ant Model diff --git a/src/ratapi/ratapi/db/csvToSqliteULS.py b/src/ratapi/ratapi/db/csvToSqliteULS.py new file mode 100644 index 0000000..aa190c3 --- /dev/null +++ b/src/ratapi/ratapi/db/csvToSqliteULS.py @@ -0,0 +1,638 @@ +import csv +import os +import sqlalchemy as sa +from sqlalchemy import Column +from sqlalchemy.sql.elements import and_, or_ +from sqlalchemy.sql.expression import tuple_ +from sqlalchemy.types import TypeDecorator, String, Boolean, Integer, Float, Unicode, DECIMAL +from numpy import loadtxt +from sqlalchemy.orm import sessionmaker, load_only +import sqlalchemy.ext.declarative as declarative + +#: Base class for declarative models +Base = declarative.declarative_base() + + +class CoerceUTF8(TypeDecorator): + impl = Unicode + + def process_bind_param(self, value, dialect): + if isinstance(value, bytes): + value = value.decode('utf-8') + return value + + +class PR(Base): + ''' Passive Repeater table + ''' + + __tablename__ = 'pr' + + id = Column(Integer, primary_key=True) + + #: FSID + fsid = Column(Integer, nullable=False, index=True) + + #: PR IDX + prSeq = Column(Integer) + + pr_lat_deg = Column(Float) + pr_lon_deg = Column(Float) + pr_height_to_center_raat_tx_m = Column(Float) + pr_height_to_center_raat_rx_m = Column(Float) + pr_ant_type = Column(String(3), nullable=False) + pr_ant_category = Column(String(10)) + pr_ant_model_idx = Column(Integer) + pr_ant_model = Column(CoerceUTF8(64)) + pr_line_loss = Column(Float) + pr_reflector_width_m = Column(Float) + pr_reflector_height_m = Column(Float) + pr_back_to_back_gain_tx = Column(Float) + pr_back_to_back_gain_rx = Column(Float) + pr_ant_diameter_tx = Column(Float) + pr_ant_diameter_rx = Column(Float) + + +class ULS(Base): + ''' ULS Database table + ''' + + __tablename__ = 'uls' + __table_args__ = ( + sa.UniqueConstraint('fsid'), + ) + + #: FSID + fsid = Column(Integer, nullable=False, index=True, primary_key=True) + + #: Region + region = Column(String(16), nullable=False) + + #: Callsign + callsign = Column(String(16), nullable=False) + + #: Status + status = Column(String(1), nullable=False) + + #: Radio Service + radio_service = Column(String(4), nullable=False) + + #: Entity Name + name = Column(CoerceUTF8(256), nullable=False) + + #: Common Carrier + common_carrier = Column(Boolean) + + #: Mobile + mobile = Column(Boolean) + + #: Rx Callsign + rx_callsign = Column(String(16), nullable=False) + + #: Rx Location Number + # rx_location_num = Column(Integer, nullable=False) + + #: Rx Antenna Number + rx_antenna_num = Column(Integer, nullable=False) + + #: Center Frequency (MHz) + freq_assigned_start_mhz = Column(Float, nullable=False) + freq_assigned_end_mhz = Column(Float, nullable=False) + + #: TX EIRP (dBm) + tx_eirp = Column(Float) + + #: Tx Lat Coords + tx_lat_deg = Column(Float) + + #: Tx Long Coords + tx_long_deg = Column(Float) + + #: Tx Ground Elevation (m) + tx_ground_elev_m = Column(Float) + + #: Tx Polarization + tx_polarization = Column(String(1), nullable=False) + + #: Tx Height to Center RAAT (m) + tx_height_to_center_raat_m = Column(Float) + + #: Tx Architecture (IDU, ODU, UNKNOWN) + tx_architecture = Column(String(8), nullable=False) + + # Azimuth Angle Towards Tx (deg) + azimuth_angle_to_tx = Column(Float) + + # Elevation Angle Towards Tx (deg) + elevation_angle_to_tx = Column(Float) + + #: Tx Beamwidth + # tx_beamwidth = Column(Float, nullable=False) + + #: Tx Gain (dBi) + tx_gain = Column(Float) + + #: Rx Lat Coords + rx_lat_deg = Column(Float, nullable=False, index=True) + + #: Rx Long Coords + rx_long_deg = Column(Float, nullable=False, index=True) + + #: Rx Ground Elevation (m) + rx_ground_elev_m = Column(Float) + + #: Rx Ant Model Idx + rx_ant_model_idx = Column(Integer) + + #: Rx Ant Model + rx_ant_model = Column(CoerceUTF8(64)) + + #: Rx Ant Category + rx_ant_category = Column(CoerceUTF8(64)) + + #: Rx Line Loss (dB) + rx_line_loss = Column(Float) + + #: Rx Height to Center RAAT (m) + rx_height_to_center_raat_m = Column(Float) + + #: Rx Gain (dBi) + rx_gain = Column(Float) + + #: Rx Ant Diameter (m) + rx_ant_diameter = Column(Float) + + #: Rx Near Field Ant Diameter (m) + rx_near_field_ant_diameter = Column(Float) + + #: Rx Near Field Dist Limit (m) + rx_near_field_dist_limit = Column(Float) + + #: Rx Near Field Ant Efficiency + rx_near_field_ant_efficiency = Column(Float) + + #: Rx Diversity Height to Center RAAT (m) + rx_diversity_height_to_center_raat_m = Column(Float) + + #: Rx Gain (dBi) + rx_diversity_gain = Column(Float) + + #: Rx Ant Diameter (m) + rx_diversity_ant_diameter = Column(Float) + + p_rp_num = Column(Integer) + + path_number = Column(Integer, nullable=False) + + +class RAS(Base): + ''' ULS Database table + ''' + + __tablename__ = 'ras' + + #: RASID + rasid = Column(Integer, nullable=False, index=True, primary_key=True) + + region = Column(String(16), nullable=False) + name = Column(CoerceUTF8(64)) + location = Column(CoerceUTF8(64)) + startFreqMHz = Column(Float) + stopFreqMHz = Column(Float) + exclusionZone = Column(String(16)) + rect1lat1 = Column(Float) + rect1lat2 = Column(Float) + rect1lon1 = Column(Float) + rect1lon2 = Column(Float) + rect2lat1 = Column(Float) + rect2lat2 = Column(Float) + rect2lon1 = Column(Float) + rect2lon2 = Column(Float) + radiusKm = Column(Float) + centerLat = Column(Float) + centerLon = Column(Float) + heightAGL = Column(Float) + + +class ANTAOB(Base): + ''' Antenna Angle Off Boresight + ''' + __tablename__ = 'antaob' + aob_idx = Column(Integer, primary_key=True) + aob_deg = Column(Float) + + +class ANTNAME(Base): + ''' Antenna Name + ''' + __tablename__ = 'antname' + ant_idx = Column(Integer, primary_key=True) + ant_name = Column(CoerceUTF8(64)) + + +class ANTGAIN(Base): + ''' Antenna Gain + ''' + __tablename__ = 'antgain' + id = Column(Integer, nullable=False, index=True, primary_key=True) + gain_db = Column(Float) + + +def _as_bool(s): + if s == 'Y': + return True + if s == 'N': + return False + return None + + +def _as_float(s): + if s == '' or s is None: + return None + return float(s) + + +def _as_int(s): + if s == '': + return None + return int(s) + + +def truncate(num, n): + integer = int(num * (10**n)) / (10**n) + return float(integer) + + +def load_csv_data(file_name, headers=None): + ''' Loads csv file into python objects + + :param filename: csv file to load + + :param haders: ((name, type), ...) + + :rtype: (iterable rows, file handle or None) + ''' + if headers is None: + file_handle = open(file_name, 'r') + return (csv.DictReader(file_handle), file_handle) + + (names, formats) = tuple(zip(*headers)) + return (loadtxt(file_name, + skiprows=1, + dtype={ + 'names': names, + 'formats': formats, + }, + delimiter=','), None) + + +def convertULS(fsDataFile, rasDataFile, antennaPatternFile, + state_root, logFile, outputSQL): + logFile.write('Converting ULS csv to sqlite' + '\n') + + logFile.write('Converting CSV file to SQLITE\n') + logFile.write('FS Data File: ' + fsDataFile + '\n') + logFile.write('RAS Data File: ' + rasDataFile + '\n') + logFile.write('Antenna Pattern File: ' + antennaPatternFile + '\n') + logFile.write('SQLITE: ' + outputSQL + '\n') + + if os.path.exists(outputSQL): + logFile.write('WARNING: sqlite file ' + outputSQL + + ' already exists, deleting\n') + os.remove(outputSQL) + if os.path.exists(outputSQL): + logFile.write('ERROR: unable to delete file ' + outputSQL + '\n') + else: + logFile.write('successfully deleted file ' + outputSQL + '\n') + + # the new sqlite for the ULS + # today_engine = sa.create_engine('sqlite:///' + outputSQL, convert_unicode=True) + today_engine = sa.create_engine('sqlite:///' + outputSQL) + today_session = sessionmaker(bind=today_engine) + s = today_session() + + # create tables: ULS, RAS, PR, ANTAOB, ANTNAME, ANTGAIN + Base.metadata.create_all( + today_engine, + tables=[ + ULS.__table__, + PR.__table__, + RAS.__table__, + ANTAOB.__table__, + ANTNAME.__table__, + ANTGAIN.__table__]) + try: + + antIdxMap = {} + + ####################################################################### + # Process antennaPatternFile # + ####################################################################### + (antennaPatternData, file_handle) = load_csv_data(antennaPatternFile) + + antPatternCount = 0 + for fieldIdx, field in enumerate(antennaPatternData.fieldnames): + if fieldIdx == 0: + if field != 'Off-axis angle (deg)': + sys.exit( + 'ERROR: Invalid antennaPatternFile: ' + + antennaPatternFile + + ' Especting "Off-axis angle (deg)", found ' + + field + + '\n') + else: + antIdx = fieldIdx - 1 + antname = ANTNAME( + ant_idx=antIdx, + ant_name=field + ) + s.add(antname) + antPatternCount += 1 + if field in antIdxMap: + sys.exit('ERROR: Invalid antennaPatternFile: ' + + antennaPatternFile + ': ' + field + ' defined multiple times\n') + antIdxMap[field] = antIdx + + antennaPatternFileRows = list(antennaPatternData) + numAOB = len(antennaPatternFileRows) + + count = 0 + for aobIdx, row in enumerate(antennaPatternFileRows): + try: + for fieldIdx, field in enumerate( + antennaPatternData.fieldnames): + if fieldIdx == 0: + antaob = ANTAOB( + aob_idx=aobIdx, + aob_deg=float(row[field]) + ) + s.add(antaob) + else: + antIdx = fieldIdx - 1 + antgain = ANTGAIN( + id=numAOB * antIdx + aobIdx, + gain_db=float(row[field]) + ) + s.add(antgain) + count += 1 + if (count) % 10000 == 0: + logFile.write( + 'CSV to sqlite Up to ANT PATTERN entry ' + str(count) + '\n') + + except Exception as e: + errMsg = 'ERROR processing antennaPatternFile: ' + \ + str(e) + '\n' + logFile.write(errMsg) + sys.exit(errMsg) + + if not (file_handle is None): + file_handle.close() + ####################################################################### + + ####################################################################### + # Process RAS Data # + ####################################################################### + (rasData, file_handle) = load_csv_data(rasDataFile) + + ras = None + + for count, row in enumerate(rasData): + try: + ras = RAS( + rasid=int(row['RASID']), + + region=str(row['Region']), + name=str(row['Name']), + location=str(row['Location']), + startFreqMHz=_as_float(row['Start Freq (MHz)']), + stopFreqMHz=_as_float(row['End Freq (MHz)']), + exclusionZone=str(row['Exclusion Zone']), + rect1lat1=_as_float(row['Rectangle1 Lat 1']), + rect1lat2=_as_float(row['Rectangle1 Lat 2']), + rect1lon1=_as_float(row['Rectangle1 Lon 1']), + rect1lon2=_as_float(row['Rectangle1 Lon 2']), + rect2lat1=_as_float(row['Rectangle2 Lat 1']), + rect2lat2=_as_float(row['Rectangle2 Lat 2']), + rect2lon1=_as_float(row['Rectangle2 Lon 1']), + rect2lon2=_as_float(row['Rectangle2 Lon 2']), + radiusKm=_as_float(row['Circle Radius (km)']), + centerLat=_as_float(row['Circle center Lat']), + centerLon=_as_float(row['Circle center Lon']), + heightAGL=_as_float(row['Antenna AGL height (m)']) + ) + s.add(ras) + if (count) % 10000 == 0: + logFile.write( + 'CSV to sqlite Up to RAS entry ' + str(count) + '\n') + + except Exception as e: + errMsg = 'ERROR processing rasDataFile: ' + str(e) + '\n' + logFile.write(errMsg) + sys.exit(errMsg) + + if not (file_handle is None): + file_handle.close() + ####################################################################### + + ####################################################################### + # Process FS Data # + ####################################################################### + (data, file_handle) = load_csv_data(fsDataFile) + + # generate queries in chunks to reduce memory footprint + to_save = [] + invalid_rows = 0 + prCount = 0 + errors = [] + uls = None + pr = None + antaob = None + antname = None + antgain = None + + for count, row in enumerate(data): + try: + numPR = _as_int(row.get('Num Passive Repeater')) + fsidVal = int(row.get('FSID') or count + 2) + if row['Rx Ant Model Name Matched'] in antIdxMap: + rxAntIdx = antIdxMap[row['Rx Ant Model Name Matched']] + else: + rxAntIdx = -1 + uls = ULS( + #: FSID + fsid=fsidVal, + #: Callsign + region=str(row['Region']), + #: Callsign + callsign=str(row['Callsign']), + #: Status + status=str(row['Status']), + #: Radio Service + radio_service=str(row['Radio Service']), + #: Entity Name + name=str(row['Entity Name']), + #: Mobile + mobile=_as_bool(row['Mobile']), + #: Rx Callsign + rx_callsign=str(row['Rx Callsign']), + #: Rx Antenna Number + rx_antenna_num=int(row['Rx Antenna Number'] or '0'), + #: Center Frequency (MHz) + freq_assigned_start_mhz=_as_float(row['Lower Band (MHz)']), + freq_assigned_end_mhz=_as_float(row['Upper Band (MHz)']), + #: Tx EIRP (dBm) + tx_eirp=_as_float(row['Tx EIRP (dBm)']), + #: Tx Lat Coords + tx_lat_deg=_as_float(row['Tx Lat Coords']), + #: Tx Long Coords + tx_long_deg=_as_float(row['Tx Long Coords']), + #: Tx Ground Elevation (m) + tx_ground_elev_m=_as_float(row['Tx Ground Elevation (m)']), + #: Tx Polarization + tx_polarization=str(row['Tx Polarization']), + #: Tx Height to Center RAAT (m) + tx_height_to_center_raat_m=_as_float( + row['Tx Height to Center RAAT (m)']), + #: Tx Architecture + tx_architecture=str(row['Tx Architecture']), + # Azimuth Angle Towards Tx (deg) + azimuth_angle_to_tx=_as_float( + row['Azimuth Angle Towards Tx (deg)']), + # Elevation Angle Towards Tx (deg) + elevation_angle_to_tx=_as_float( + row['Elevation Angle Towards Tx (deg)']), + #: Tx Gain (dBi) + tx_gain=_as_float(row['Tx Gain (dBi)']), + #: Rx Lat Coords + rx_lat_deg=_as_float(row['Rx Lat Coords']), + #: Rx Long Coords + rx_long_deg=_as_float(row['Rx Long Coords']), + #: Rx Ground Elevation (m) + rx_ground_elev_m=_as_float(row['Rx Ground Elevation (m)']), + + #: Rx Ant Model Idx + rx_ant_model_idx=rxAntIdx, + + #: Rx Ant Model + rx_ant_model=str(row['Rx Ant Model Name Matched']), + + #: Rx Ant Category + rx_ant_category=str(row['Rx Ant Category']), + #: Rx Line Loss (dB) + rx_line_loss=_as_float(row['Rx Line Loss (dB)']), + #: Rx Height to Center RAAT (m) + rx_height_to_center_raat_m=_as_float( + row['Rx Height to Center RAAT (m)']), + #: Rx Gain (dBi) + rx_gain=_as_float(row['Rx Gain (dBi)']), + #: Rx Ant Diameter (m) + rx_ant_diameter=_as_float(row['Rx Ant Diameter (m)']), + + #: Rx Near Field Ant Diameter (m) + rx_near_field_ant_diameter=_as_float( + row['Rx Near Field Ant Diameter (m)']), + #: Rx Near Field Dist Limit (m) + rx_near_field_dist_limit=_as_float( + row['Rx Near Field Dist Limit (m)']), + #: Rx Near Field Ant Efficiency + rx_near_field_ant_efficiency=_as_float( + row['Rx Near Field Ant Efficiency']), + + #: Rx Diversity Height to Center RAAT (m) + rx_diversity_height_to_center_raat_m=_as_float( + row['Rx Diversity Height to Center RAAT (m)']), + #: Rx Gain (dBi) + rx_diversity_gain=_as_float( + row['Rx Diversity Gain (dBi)']), + #: Rx Ant Diameter (m) + rx_diversity_ant_diameter=_as_float( + row['Rx Diversity Ant Diameter (m)']), + + #: Number of passive repeaters + p_rp_num=numPR, + + # Path number + path_number=int(row['Path Number']) + ) + + s.add(uls) + # print "FSID = " + str(uls.fsid) + for idx in range(1, numPR + 1): + if row['Passive Repeater ' + + str(idx) + ' Ant Model Name Matched'] in antIdxMap: + prAntIdx = antIdxMap[row['Passive Repeater ' + + str(idx) + ' Ant Model Name Matched']] + else: + prAntIdx = -1 + pr = PR( + id=prCount, + fsid=fsidVal, + prSeq=idx, + pr_lat_deg=_as_float( + row['Passive Repeater ' + str(idx) + ' Lat Coords']), + pr_lon_deg=_as_float( + row['Passive Repeater ' + str(idx) + ' Long Coords']), + pr_height_to_center_raat_tx_m=_as_float( + row['Passive Repeater ' + str(idx) + ' Height to Center RAAT Tx (m)']), + pr_height_to_center_raat_rx_m=_as_float( + row['Passive Repeater ' + str(idx) + ' Height to Center RAAT Rx (m)']), + pr_ant_type=str( + row['Passive Repeater ' + str(idx) + ' Ant Type']), + pr_ant_category=str( + row['Passive Repeater ' + str(idx) + ' Ant Category']), + pr_ant_model_idx=prAntIdx, + pr_ant_model=str( + row['Passive Repeater ' + str(idx) + ' Ant Model Name Matched']), + pr_line_loss=_as_float( + row['Passive Repeater ' + str(idx) + ' Line Loss (dB)']), + pr_reflector_height_m=_as_float( + row['Passive Repeater ' + str(idx) + ' Reflector Height (m)']), + pr_reflector_width_m=_as_float( + row['Passive Repeater ' + str(idx) + ' Reflector Width (m)']), + pr_back_to_back_gain_tx=_as_float( + row['Passive Repeater ' + str(idx) + ' Back-to-Back Gain Tx (dBi)']), + pr_back_to_back_gain_rx=_as_float( + row['Passive Repeater ' + str(idx) + ' Back-to-Back Gain Rx (dBi)']), + pr_ant_diameter_tx=_as_float( + row['Passive Repeater ' + str(idx) + ' Tx Ant Diameter (m)']), + pr_ant_diameter_rx=_as_float( + row['Passive Repeater ' + str(idx) + ' Rx Ant Diameter (m)']) + ) + prCount = prCount + 1 + s.add(pr) + + # to_save.append(uls) + except Exception as e: + logFile.write('ERROR: ' + str(e) + '\n') + invalid_rows = invalid_rows + 1 + if invalid_rows > 50: + # errors.append('{}\nData: {}'.format(str(e), str(row))) + logFile.write('WARN: > 50 invalid rows' + '\n') + + if count % 10000 == 0: + logFile.write('CSV to sqlite Up to FS entry ' + + str(count) + '\n') + + if not (file_handle is None): + file_handle.close() + ####################################################################### + + s.commit() # only commit after DB opertation are completed + logFile.write( + 'File ' + + str(outputSQL) + + ' created with ' + + str(count) + + ' FS entries, ' + + str(prCount) + + ' passive repeaters, ' + + str(antPatternCount) + + ' antennaPatterns.\n') + + return + + except Exception as e: + s.rollback() + raise e + finally: + s.close() diff --git a/src/ratapi/ratapi/db/daily_uls_parse.py b/src/ratapi/ratapi/db/daily_uls_parse.py new file mode 100755 index 0000000..afc5830 --- /dev/null +++ b/src/ratapi/ratapi/db/daily_uls_parse.py @@ -0,0 +1,1167 @@ +#!/usr/bin/env python +import os +import datetime +import zipfile +import shutil +import subprocess +import sys +import glob +import argparse +import csv +import urllib.request +import urllib.parse +import urllib.error +from collections import OrderedDict +import ssl +from urllib.error import URLError +from processAntennaCSVs import processAntFiles +from csvToSqliteULS import convertULS +# from sort_callsigns_all import sortCallsigns +from sort_callsigns_addfsid import sortCallsignsAddFSID +from fix_bps import fixBPS +from fix_params import fixParams +import sqlalchemy as sa +import hashlib +import fnmatch + +ssl._create_default_https_context = ssl._create_unverified_context + +# file types we need to consider along with their # of | symbols (i.e. # +# of cols - 1) +neededFilesUS = {} +neededFilesUS[0] = { + 'AN.dat': 37, + 'CP.dat': 13, + 'EM.dat': 15, + 'EN.dat': 29, + 'FR.dat': 29, + 'HD.dat': 58, + 'LO.dat': 50, + 'PA.dat': 21, + 'SG.dat': 14} +neededFilesUS[1] = { + 'AN.dat': 37, + 'CP.dat': 13, + 'EM.dat': 15, + 'EN.dat': 29, + 'FR.dat': 29, + 'HD.dat': 58, + 'LO.dat': 50, + 'PA.dat': 23, + 'SG.dat': 14} + +# Version changed AUG 18, 2022 +versionTime = datetime.datetime(2022, 8, 18, 0, 0, 0) + +# map to reuse weekday in loops +dayMap = OrderedDict() # ordered dictionary so order is mainatined +dayMap[6] = 'sun' +dayMap[0] = 'mon' +dayMap[1] = 'tue' +dayMap[2] = 'wed' +dayMap[3] = 'thu' +dayMap[4] = 'fri' +dayMap[5] = 'sat' +# map to reuse for converting month strings to ints +monthMap = { + 'jan': 1, + 'feb': 2, + 'mar': 3, + 'apr': 4, + 'may': 5, + 'jun': 6, + 'jul': 7, + 'aug': 8, + 'sep': 9, + 'oct': 10, + 'nov': 11, + 'dec': 12 +} + +############################################################################### +# Download data files for each region (US, CA) # +# Currently in fullPathTempDir # +############################################################################### + + +def downloadFiles(region, logFile, currentWeekday, fullPathTempDir): + regionDataDir = fullPathTempDir + '/' + region + if (not os.path.isdir(regionDataDir)): + os.mkdir(regionDataDir) + logFile.write('Downloading data files for ' + + region + ' into ' + regionDataDir + '\n') + if region == 'US': + # download the latest Weekly Update + weeklyURL = 'https://data.fcc.gov/download/pub/uls/complete/l_micro.zip' + logFile.write('Downloading weekly' + '\n') + urllib.request.urlretrieve(weeklyURL, regionDataDir + '/weekly.zip') + + # download all the daily updates starting from Sunday up to that day + # example: on Wednesday morning, we will download the weekly update + # PLUS Sun, Mon, Tue and Wed daily updates + for key, day in dayMap.items(): + dayStr = day + dailyURL = 'https://data.fcc.gov/download/pub/uls/daily/l_mw_' + dayStr + '.zip' + logFile.write('Downloading ' + dayStr + '\n') + urllib.request.urlretrieve( + dailyURL, regionDataDir + '/' + dayStr + '.zip') + # Exit after processing today's file + if (key == currentWeekday) and (day != 'sun'): + break + elif region == 'CA': + urllib.request.urlretrieve( + 'https://www.ic.gc.ca/engineering/Stations_Data_Extracts.csv', + regionDataDir + '/SD.csv') + urllib.request.urlretrieve( + 'https://www.ic.gc.ca/engineering/Passive_Repeater_data_extract.csv', + regionDataDir + '/PP.csv') + urllib.request.urlretrieve( + 'https://www.ic.gc.ca/engineering/Passive_Reflectors_Data_Extract.csv', + regionDataDir + '/PR.csv') + + # Not processing transmitters for CA + # urllib.request.urlretrieve('https://www.ic.gc.ca/engineering/SMS_TAFL_Files/TAFL_LTAF_Fixe.zip', 'TAFL_LTAF_Fixe.zip') + # zip_file = zipfile.ZipFile("TAFL_LTAF_Fixe.zip") # zip object + # zip_file.extractall(regionDataDir) + # zip_file.close() + # if os.path.isfile(regionDataDir + '/TAFL_LTAF_Fixe.csv'): + # os.rename(regionDataDir + '/TAFL_LTAF_Fixe.csv', regionDataDir + '/TA.csv') + # elif os.path.isfile(regionDataDir + '/IC_TAFL_File_fixed.csv'): + # os.rename(regionDataDir + '/IC_TAFL_File_fixed.csv', regionDataDir + '/TA.csv') + # else: + # raise Exception('ERROR: Unable to process CA file {}'.format("TAFL_LTAF_Fixe.zip")) + + cmd = 'echo "Antenna Manufacturer,Antenna Model Number,Antenna Gain [dBi],Antenna Diameter,Beamwidth [deg],Last Updated,Pattern Type,Pattern Azimuth [deg],Pattern Attenuation [dB]" >| ' + regionDataDir + '/AP.csv' # noqa + os.system(cmd) + urllib.request.urlretrieve( + 'https://www.ic.gc.ca/engineering/Antenna_Patterns_6GHz.csv', + regionDataDir + '/Antenna_Patterns_6GHz_orig.csv') + cmd = 'cat ' + regionDataDir + \ + '/Antenna_Patterns_6GHz_orig.csv >> ' + regionDataDir + '/AP.csv' + os.system(cmd) + os.remove(regionDataDir + '/Antenna_Patterns_6GHz_orig.csv') +############################################################################### + +# Downloads antenna files + + +def prepareAFCGitHubFiles(rawDir, destDir, logFile): + # Files in rawDir are downloaded from + # https://raw.githubusercontent.com/Wireless-Innovation-Forum/6-GHz-AFC/main/data/common_data/ + # (can also be viewed at https://github.com/Wireless-Innovation-Forum/6-GHz-AFC/tree/main/data/common_data) + # This function brings them to palatable state in destDir + dataFileList = ['antenna_model_diameter_gain.csv', + 'billboard_reflector.csv', + 'category_b1_antennas.csv', + 'high_performance_antennas.csv', + 'fcc_fixed_service_channelization.csv', + 'transmit_radio_unit_architecture.csv', + ] + + for dataFile in dataFileList: + srcFile = os.path.join(rawDir, dataFile) + dstFile = os.path.join(destDir, dataFile) + # Remove control characters. + cmd = 'tr -d \'\\200-\\377\\015\' < ' + srcFile + ' ' + # Remove blank lines. + # cmd += '| gawk -F "," \'($2 != "") { print }\' ' \ + # Fix spelling error. + # cmd += '| sed \'s/daimeter/diameter/\' ' \ + + cmd += '>| ' + dstFile + os.system(cmd) + if dataFile == "fcc_fixed_service_channelization.csv": + cmd = 'echo -e "5967.4375,30,\n' \ + + '6056.3875,30,\n' \ + + '6189.8275,30,\n' \ + + '6219.4775,30,\n' \ + + '6308.4275,30," >> ' + dstFile + os.system(cmd) + +# Extracts all the zip files into sub-directories + + +def extractZips(logFile, directory): + logFile.write('Extracting zips for directory ' + directory + '\n') + # unzip into sub-directories of directory + for tempZip in os.listdir(directory): # unzip every zip + if (tempZip.endswith('.zip')): + logFile.write('Extracting ' + tempZip + '\n') + fileName = os.path.abspath( + directory + '/' + tempZip) # get full path + zip_file = zipfile.ZipFile(fileName) # zip object + subDirName = fileName.replace('.zip', '') + os.mkdir(subDirName) # sub-directory to extract to + zip_file.extractall(subDirName) + zip_file.close() + +# Returns the datetime object based on the counts file + + +def verifyCountsFile(directory): + # check weekly date + with open(directory + '/counts', 'r') as countsFile: + line = countsFile.readline() + # FCC Format for the line is: + # File Creation Date: Sun Oct 3 17:59:04 EDT 2021 + # remove non-date part of string + dateStr = line.replace('File Creation Date: ', '') + dateData = dateStr.split() # split into word array + weekday = dateData[0] + # convert month string to an int between 1 and 12 + month = monthMap.get(dateData[1].lower(), 'err') + day = int(dateData[2]) # day of the month as a Number + time = dateData[3] + timeZone = dateData[4] + year = int(dateData[5]) + # convert string time to numbers array + timeData = [int(string) for string in time.split(':')] + hours = timeData[0] + mins = timeData[1] + sec = timeData[2] + if (month != 'err'): + fileCreationDate = datetime.datetime( + year, month, day, hours, mins, sec) + return fileCreationDate + else: + raise Exception( + 'ERROR: Could not parse month of FCC string in counts file for ' + + directory + + ' update') + + +# Removes any record with the given id from the given file +def removeFromCombinedFile( + fileName, directory, ids_to_remove, day, versionIdx): + weeklyAndDailyPath = directory + '/weekly/' + fileName + '_withDaily' + + if (day == 'weekly'): + # create file that contains weekly and daily + with open(weeklyAndDailyPath, 'w', encoding='utf8') as withDaily: + # open weekly file + with open(directory + '/weekly/' + fileName, 'r', encoding='utf8') as weekly: + record = '' + symbolCount = 0 + numExpectedCols = neededFilesUS[versionIdx][fileName] + for line in weekly: + # remove newline characters from line + line = line.replace('\n', '') + line = line.replace('\r', '') + # the line was just newline character(s), skip it + if (line == '' or line == ' '): + continue + # the line is a single piece of data that does not contain + # the | character + elif ('|' not in line): + record += line + continue + else: + # this many | were in the line + symbolCount += line.count('|') + record += line + # the record is complete if the number of | symbols is + # equal to the number of expected cols + if (symbolCount == numExpectedCols): + cols = record.split('|') + fileType = cols[0] + # Ensure we need this entry + if (fileType + + ".dat" in list(neededFilesUS[versionIdx].keys())): + # only write when the id is not in the list of ids + if (not cols[1] in ids_to_remove): + record += "\r\n" # add newline + withDaily.write(record) + # reset for the next record + record = '' + symbolCount = 0 + elif (symbolCount > numExpectedCols): + e = Exception( + 'ERROR: Could not process record. More columns than expected in weekly file') + raise e + else: + # open new file that contains weekly and daily + with open(weeklyAndDailyPath + '_temp', 'w', encoding='utf8') as withDaily: + # open older file + with open(weeklyAndDailyPath, 'r', encoding='utf8') as weekly: + for line in weekly: + cols = line.split('|') + # only write when the id is not in the list of ids + if (not cols[1] in ids_to_remove): + withDaily.write(line) + # remove old combined, move new one to right place + os.remove(weeklyAndDailyPath) + os.rename(weeklyAndDailyPath + '_temp', weeklyAndDailyPath) + +# Update specific datafile with daily data + + +def updateIndividualFile(dayFile, directory, lineBuffer): + weeklyAndDailyPath = directory + '/weekly/' + dayFile + '_withDaily' + if (os.path.isfile(weeklyAndDailyPath)): + # open file that contains weekly and daily + with open(weeklyAndDailyPath, 'a', encoding='utf8') as withDaily: + withDaily.write(lineBuffer) + else: + e = Exception('Combined file ' + + weeklyAndDailyPath + ' does not exist') + raise e + +# Reads the file and creates well formed entries from FCC data. +# The ONLY thing that consititutes a valid entry is the number of | +# characters (e.g. AN files have 38 columns and thus 37 | per record) + + +def readEntries(dayFile, directory, day, versionIdx): + recordBuffer = '' # buffer to limit number of file open() calls + idsToRemove = [] + with open(directory + '/' + day + '/' + dayFile, encoding='utf8') as infile: + numExpectedCols = neededFilesUS[versionIdx][dayFile] + record = '' + symbolCount = 0 + # Iterate over the lines in the file + linenum = 0 + for line in infile: + linenum += 1 + # remove newline characters from line + line = line.replace('\n', '') + line = line.replace('\r', '') + + # the line we were given was just newline character(s) or + # whitespace, skip it + if (line == '' or line == ' '): + continue + # the line is a single piece of data that does not contain the | + # character + elif ('|' not in line): + record += line + continue + else: + symbolCount += line.count('|') # this many | were in the line + record += line + # the record is complete if the number of | symbols is equal to the + # number of expected cols + if (symbolCount == numExpectedCols): + cols = record.split('|') + # FCC unique system identifier should always be this index + fccId = cols[1] + # only need to remove an ID once per file per day + if (fccId not in idsToRemove): + idsToRemove.append(fccId) + # store the record to the buffer with proper newline for csv + # format + recordBuffer += record + '\r\n' + record = '' # reset the record + symbolCount = 0 + elif (symbolCount > numExpectedCols): + raise Exception( + 'ERROR: Could not process record more columns than expected: ' + + day + + '/' + + dayFile + + ':' + + str(linenum)) + removeFromCombinedFile(dayFile, directory, idsToRemove, day, versionIdx) + updateIndividualFile(dayFile, directory, recordBuffer) + +# Processes the daily files, replacing weekly entries when needed +# Returns datetime of ULS data upload (ULS data identity) + + +def processDailyFiles(weeklyCreation, logFile, directory, currentWeekday): + logFile.write('Processing daily files' + '\n') + + # Most recent timetag from 'counts' + upload_time = weeklyCreation + + # Process weekly file + if (weeklyCreation >= versionTime): + versionIdx = 1 + else: + versionIdx = 0 + for file in list(neededFilesUS[versionIdx].keys()): + # removeFromCombinedFile() will fix any formatting in FCC data + # passing an empty list for second arg means no records will be removed + removeFromCombinedFile(file, directory, [], 'weekly', versionIdx) + + for key, day in list(dayMap.items()): + dayDirectory = directory + '/' + day + + # ensure counts file is newer than weekly + fileCreationDate = verifyCountsFile(dayDirectory) + timeDiff = fileCreationDate - weeklyCreation + if (timeDiff.total_seconds() > 0): + upload_time = fileCreationDate + if (fileCreationDate >= versionTime): + versionIdx = 1 + else: + versionIdx = 0 + for dailyFile in os.listdir(dayDirectory): + if (dailyFile in list(neededFilesUS[versionIdx].keys())): + logFile.write('Processing ' + dailyFile + + ' for: ' + day + '\n') + readEntries(dailyFile, directory, day, versionIdx) + else: + logFile.write( + 'INFO: Skipping ' + + day + + ' files because they are older than the weekly file' + + '\n') + + # Exit after processing yesterdays file + if (key == currentWeekday): + break + return upload_time + +# Generates the combined text file that the coalition processor uses. + + +def generateUlsScriptInputUS(directory, logFile, genFilename): + logFile.write('Appending US data to ' + genFilename + + ' as input for uls script' + '\n') + with open(genFilename, 'a', encoding='utf8') as combined: + for weeklyFile in os.listdir(directory): + if "withDaily" in weeklyFile: + logFile.write('Adding ' + directory + '/' + + weeklyFile + ' to ' + genFilename + '\n') + with open(directory + '/' + weeklyFile, 'r', encoding='utf8') as infile: + for line in infile: + combined.write('US:' + line) + + +def generateUlsScriptInputCA(directory, logFile, genFilename): + """ Returns identity string of downloaded data """ + logFile.write('Appending CA data to ' + genFilename + + ' as input for uls script' + '\n') + # Names of source files (to compute MD5 of) + sourceFilenames = [] + with open(genFilename, 'a', encoding='utf8') as combined: + for dataFile in os.listdir(directory): + if fnmatch.fnmatch(dataFile, "??.csv") and os.path.isfile( + os.path.join(directory, dataFile)): + sourceFilenames.append(dataFile) + if dataFile != "AP.csv": # skip antenna pattern file, processed separately + logFile.write('Adding ' + directory + '/' + + dataFile + ' to ' + genFilename + '\n') + with open(directory + '/' + dataFile, 'r', encoding='utf8') as csvfile: + code = dataFile.replace('.csv', '') + csvreader = csv.reader(csvfile) + for row in csvreader: + for (i, field) in enumerate(row): + row[i] = field.replace('|', ':') + combined.write('CA:' + code + '|' + + ('|'.join(row)) + '|\n') + if not sourceFilenames: + raise Exception("CA source filenames not found") + sources_md5 = hashlib.md5() + for sourceFilename in sorted(sourceFilenames): + with open(os.path.join(directory, sourceFilename), mode="rb") as f: + sources_md5.update(f.read()) + return sources_md5.hexdigest() + + +def storeDataIdentities(sqlFile, identityDict): + """ Stores region data identities in gewnerated SQLite database. + For FCC ULS identity is upload datetime, for Canada SMS, for now, sadly, + only an MD5 of downloaded files. + + Arguments: + sqlFile -- SQLite file where to add table with region identities + identityDict -- Dictionary of region identities. Keys are region names (US, + CA, ...), values are identity strings + """ + assert os.path.isfile(sqlFile) + engine = sa.create_engine("sqlite:///" + sqlFile) + metadata = sa.MetaData() + metadata.reflect(bind=engine) + conn = engine.connect() + if not sa.inspect(engine).has_table("data_ids"): + dataIdsTable = \ + sa.Table("data_ids", metadata, + sa.Column("region", sa.String(100), primary_key=True), + sa.Column("identity", sa.String(1000), nullable=False)) + metadata.create_all(engine) + idsTable = metadata.tables["data_ids"] + for region in sorted(identityDict.keys()): + conn.execute(sa.insert(idsTable).values(region=region, + identity=identityDict[region])) + conn.close() + + +def daily_uls_parse(state_root, interactive): + startTime = datetime.datetime.now() + nameTime = startTime.isoformat().replace(":", '_') + + nameTime += "_UniiUS" + uniiStr.replace(":", "") + + temp = "/temp" + + root = state_root + "/daily_uls_parse" # root so path is consisent + + ########################################################################### + # If interactive, prompt to set root path # + ########################################################################### + if interactive: + print("Specify full path for root daily_uls_parse dir") + value = input("Enter Directory (" + root + "): ") + if (value != ""): + root = value + print("daily_uls_parse root directory set to " + root) + ########################################################################### + + # weekday() is 0 indexed at monday + currentWeekday = datetime.datetime.today().weekday() + + ########################################################################### + # If interactive, prompt for weekday # + ########################################################################### + if wfaFlag: + currentWeekday = 0 + elif interactive: + print("Enter Current Weekday for FCC files: ") + for key, day in list(dayMap.items()): + print(str(key) + ": " + day) + value = input("Current Weekday (" + str(currentWeekday) + "): ") + if (value != ""): + currentWeekday = int(value) + if (currentWeekday < 0 or currentWeekday > 6): + print("ERROR: currentWeekday = " + + str(currentWeekday) + " invalid, must be in [0,6]") + return + ########################################################################### + + fullPathTempDir = root + temp + + ########################################################################### + # If interactive, prompt for removal of temp directory # + ########################################################################### + if wfaFlag: + removeTempDirFlag = False + elif interactive: + accepted = False + while not accepted: + value = input("Remove temp directory: " + + fullPathTempDir + " ? (y/n): ") + if value == "y": + accepted = True + removeTempDirFlag = True + elif value == "n": + accepted = True + removeTempDirFlag = False + else: + print("ERROR: Invalid input: " + value + ", must be y or n") + else: + removeTempDirFlag = True + ########################################################################### + + ########################################################################### + # If removeTempDirFlag set, remove temp dir, otherwise must already exist # + ########################################################################### + if removeTempDirFlag: + if (os.path.isdir(fullPathTempDir)): + try: + shutil.rmtree(fullPathTempDir) # delete temp folder + except Exception as e: + # LOGGER.error('ERROR: Could not delete old temp directory:') + raise e + # create temp directory to download files to + os.mkdir(fullPathTempDir) + ########################################################################### + + ########################################################################### + # cd to temp dir and begin creating log file # + ########################################################################### + if (not os.path.isdir(fullPathTempDir)): + print("ERROR: " + fullPathTempDir + " does not exist") + return + + os.chdir(fullPathTempDir) # change to temp + logname = fullPathTempDir + "/dailyParse_" + nameTime + ".log" + logFile = open(logname, 'w', 1) + if interactive: + logFile.write('Starting interactive mode update at: ' + + startTime.isoformat() + '\n') + else: + logFile.write('Starting update at: ' + startTime.isoformat() + '\n') + ########################################################################### + + for region in regionList: + + ####################################################################### + # If interactive, prompt for downloading of data files for region # + ####################################################################### + if wfaFlag: + downloadDataFilesFlag = False + elif interactive: + accepted = False + while not accepted: + value = input("Download data files for " + + region + "? (y/n): ") + if value == "y": + accepted = True + downloadDataFilesFlag = True + elif value == "n": + accepted = True + downloadDataFilesFlag = False + else: + print("ERROR: Invalid input: " + + value + ", must be y or n") + else: + downloadDataFilesFlag = True + ####################################################################### + + ####################################################################### + # If downloadDataFilesFlag set, download data files for region # + ####################################################################### + if downloadDataFilesFlag: + downloadFiles(region, logFile, currentWeekday, fullPathTempDir) + ####################################################################### + + regionDataDir = fullPathTempDir + '/' + region + + if region == 'US': + ################################################################### + # If interactive, prompt for extraction of files from zip files # + ################################################################### + if wfaFlag: + extractZipFlag = True + elif interactive: + value = input( + "Extract FCC files from downloaded zip files? (y/n): ") + if value == "y": + extractZipFlag = True + elif value == "n": + extractZipFlag = False + else: + print("ERROR: Invalid input: " + + value + ", must be y or n") + else: + extractZipFlag = True + ################################################################### + + ################################################################### + # If extractZipFlag set, extract files from zip files # + ################################################################### + if extractZipFlag: + extractZips(logFile, regionDataDir) + ################################################################### + + ########################################################################### + # If interactive, prompt for converting AFC GitHub data files # + ########################################################################### + if wfaFlag: + prepareAFCGitHubFilesFlag = False + elif interactive: + accepted = False + while not accepted: + value = input("Prepare AFC GitHub data files? (y/n): ") + if value == "y": + accepted = True + prepareAFCGitHubFilesFlag = True + elif value == "n": + accepted = True + prepareAFCGitHubFilesFlag = False + else: + print("ERROR: Invalid input: " + value + ", must be y or n") + else: + prepareAFCGitHubFilesFlag = True + ########################################################################### + + ########################################################################### + # If prepareAFCGitHubFilesFlag set, prepare AFC GitHub data files # + ########################################################################### + if prepareAFCGitHubFilesFlag: + prepareAFCGitHubFiles( + root + '/raw_wireless_innovation_forum_files', ".", logFile) + ########################################################################### + + ########################################################################### + # If interactive, prompt for creating antenna_model_list.csv # + ########################################################################### + if wfaFlag: + processAntFilesFlag = True + elif interactive: + accepted = False + while not accepted: + value = input( + "Process antenna model files to create antenna_model_list.csv, antenna_prefix_list.csv and antennaPatternFile? (y/n): ") + if value == "y": + accepted = True + processAntFilesFlag = True + elif value == "n": + accepted = True + processAntFilesFlag = False + else: + print("ERROR: Invalid input: " + value + ", must be y or n") + else: + processAntFilesFlag = True + ########################################################################### + + antennaPatternFileFile = 'afc_antenna_patterns_' + nameTime + '.csv' + + ########################################################################### + # If interactive, prompt to set antennaPatternFileFile, note that # + # if processAntFilesFlag is not set, this file should exist for # + # subsequent processing. # + ########################################################################### + if interactive: + if not processAntFilesFlag: + flist = glob.glob( + fullPathTempDir + + "/afc_antenna_patterns_[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]_[0-9][0-9]_[0-9][0-9].[0-9][0-9][0-9][0-9][0-9][0-9].csv") # noqa + if (len(flist)): + antennaPatternFileFile = os.path.basename(flist[-1]) + + value = input("Enter Antenna Pattern filename (" + + antennaPatternFileFile + "): ") + if (value != ""): + antennaPatternFileFile = value + ########################################################################### + + # output filename from uls-script + fullPathAntennaPatternFile = fullPathTempDir + "/" + antennaPatternFileFile + + ########################################################################### + # If processAntFilesFlag set, process data files to create # + # antenna_model_list.csv # + ########################################################################### + if processAntFilesFlag: + processAntFiles(fullPathTempDir, processCA, combineAntennaRegionFlag, + fullPathTempDir + '/antenna_model_list.csv', + fullPathTempDir + '/antenna_prefix_list.csv', + fullPathAntennaPatternFile, logFile) + ########################################################################### + + ########################################################################### + # If interactive, prompt for processing download files for each region # + # to create combined.txt # + ########################################################################### + if interactive: + accepted = False + while not accepted: + value = input( + "Process FCC files and generate file combined.txt to use as input to uls-script? (y/n): ") + if value == "y": + accepted = True + processDownloadFlag = True + elif value == "n": + accepted = True + processDownloadFlag = False + else: + print("ERROR: Invalid input: " + value + ", must be y or n") + else: + processDownloadFlag = True + ########################################################################### + + # input filename for uls-script + fullPathCoalitionScriptInput = fullPathTempDir + "/combined.txt" + + # Per region data identities - upload date, hash, etc. + dataIdentities = {} + + ########################################################################### + # If processDownloadFlag set, process Download files to create combined.txt # + ########################################################################### + if processDownloadFlag: + with open(fullPathCoalitionScriptInput, 'w', encoding='utf8') as combined: + pass # Do nothing, create empty file that will be appended to + + for region in regionList: + + regionDataDir = fullPathTempDir + '/' + region + dataIdentity = None + + if region == 'US': + # US files (FCC) consist of weekly and daily updates. + # get the time creation of weekly file from the counts file + weeklyCreation = verifyCountsFile(regionDataDir + '/weekly') + # process the daily files day by day + uploadTime = processDailyFiles( + weeklyCreation, logFile, regionDataDir, currentWeekday) + # For US identity is FCC ULS upoload datetime + dataIdentity = uploadTime.isoformat() + + rasDataFileUSSrc = root + '/data_files/RASdatabase.dat' + rasDataFileUSTgt = regionDataDir + '/weekly/RA.dat_withDaily' + logFile.write("Copying " + rasDataFileUSSrc + + ' to ' + rasDataFileUSTgt + '\n') + subprocess.call(['cp', rasDataFileUSSrc, rasDataFileUSTgt]) + + # generate the combined csv/txt file for the coalition uls + # processor + generateUlsScriptInputUS( + regionDataDir + '/weekly', + logFile, + fullPathCoalitionScriptInput) + elif region == 'CA': + # For Canada identity is MD5 of downloaded files + dataIdentity = generateUlsScriptInputCA( + regionDataDir, logFile, fullPathCoalitionScriptInput) + else: + logFile.write('ERROR: Invalid region = ' + region) + raise e + assert dataIdentity is not None + dataIdentities[region] = dataIdentity + + ########################################################################### + + ########################################################################### + # If interactive, prompt for running ULS Processor (uls-script) # + ########################################################################### + if wfaFlag: + runULSProcessorFlag = True + elif interactive: + accepted = False + while not accepted: + value = input("Run ULS Processor, uls-script? (y/n): ") + if value == "y": + accepted = True + runULSProcessorFlag = True + elif value == "n": + accepted = True + runULSProcessorFlag = False + else: + print("ERROR: Invalid input: " + value + ", must be y or n") + else: + runULSProcessorFlag = True + ########################################################################### + + # os.chdir(root) # change back to root of this script + coalitionScriptOutputFSFilename = 'FS_' + nameTime + '.csv' + coalitionScriptOutputRASFilename = 'RAS_' + nameTime + '.csv' + + ########################################################################### + # If interactive, prompt to set output file from ULS Processor, note that # + # if the ULS Processor is not going to be run, this file should exist for # + # subsequent processing. # + ########################################################################### + if interactive: + if not runULSProcessorFlag: + flist = glob.glob( + fullPathTempDir + + "/FS_[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]_[0-9][0-9]_[0-9][0-9].[0-9][0-9][0-9][0-9][0-9][0-9].csv") + if (len(flist)): + coalitionScriptOutputFSFilename = os.path.basename(flist[-1]) + flist = glob.glob( + fullPathTempDir + + "/RAS_[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]_[0-9][0-9]_[0-9][0-9].[0-9][0-9][0-9][0-9][0-9][0-9].csv") + if (len(flist)): + coalitionScriptOutputRASFilename = os.path.basename(flist[-1]) + + value = input("Enter ULS Processor output FS filename (" + + coalitionScriptOutputFSFilename + "): ") + if (value != ""): + coalitionScriptOutputFSFilename = value + + value = input("Enter ULS Processor output RAS filename (" + + coalitionScriptOutputRASFilename + "): ") + if (value != ""): + coalitionScriptOutputRASFilename = value + ########################################################################### + + # output filename from uls-script + fullPathCoalitionScriptOutput = fullPathTempDir + \ + "/" + coalitionScriptOutputFSFilename + fullPathRASDabataseFile = fullPathTempDir + \ + "/" + coalitionScriptOutputRASFilename + + ########################################################################### + # If runULSProcessorFlag set, run ULS processor # + ########################################################################### + if runULSProcessorFlag: + mode = "proc_uls" + if combineAntennaRegionFlag: + mode += "_ca" + + # run through the uls processor + logFile.write('Running through ULS processor' + '\n') + try: + subprocess.call([root + '/uls-script', + fullPathTempDir + '/combined.txt', + fullPathCoalitionScriptOutput, + fullPathRASDabataseFile, + fullPathTempDir + '/antenna_model_list.csv', + fullPathTempDir + '/antenna_prefix_list.csv', + root + '/antenna_model_map.csv', + fullPathTempDir + '/fcc_fixed_service_channelization.csv', + fullPathTempDir + '/transmit_radio_unit_architecture.csv', + uniiStr, + mode]) + except Exception as e: + logFile.write('ERROR: ULS processor error:') + raise e + ########################################################################### + + ########################################################################### + # If interactive, prompt for running fixBPS # + ########################################################################### + if wfaFlag: + runFixBPSFlag = True + elif interactive: + accepted = False + while not accepted: + value = input("Run fixBPS? (y/n): ") + if value == "y": + accepted = True + runFixBPSFlag = True + elif value == "n": + accepted = True + runFixBPSFlag = False + else: + print("ERROR: Invalid input: " + value + ", must be y or n") + else: + runFixBPSFlag = True + ########################################################################### + + # output filename from runBPS + bpsScriptOutput = fullPathCoalitionScriptOutput.replace( + '.csv', '_fixedBPS.csv') + modcodFile = root + "/data_files/modcod_bps.csv" + + ########################################################################### + # If runFixBPSFlag set, run fixBPS # + ########################################################################### + if runFixBPSFlag: + logFile.write("Running through BPS script, cwd = " + + os.getcwd() + '\n') + fixBPS(fullPathCoalitionScriptOutput, modcodFile, bpsScriptOutput) + ########################################################################### + + ########################################################################### + # If interactive, prompt for running sortCallsignsAddFSID # + ########################################################################### + if wfaFlag: + runSortCallsignsAddFSIDFlag = True + elif interactive: + accepted = False + while not accepted: + value = input("Run sortCallsignsAddFSID? (y/n): ") + if value == "y": + accepted = True + runSortCallsignsAddFSIDFlag = True + elif value == "n": + accepted = True + runSortCallsignsAddFSIDFlag = False + else: + print("ERROR: Invalid input: " + value + ", must be y or n") + else: + runSortCallsignsAddFSIDFlag = True + ########################################################################### + + # output filename from sortCallsignsAddFSID + # fsidTableFile is a datafile used by sortCallsignsAddFSID + sortedOutput = bpsScriptOutput.replace(".csv", "_sorted.csv") + + ########################################################################### + # If runSortCallsignsAddFSIDFlag set, run sortCallsignsAddFSID # + ########################################################################### + if runSortCallsignsAddFSIDFlag: + fsidTableFile = root + '/data_files/fsid_table.csv' + fsidTableBakFile = root + '/data_files/fsid_table_bak_' + nameTime + '.csv' + logFile.write("Backing up FSID table for to: " + + fsidTableBakFile + '\n') + subprocess.call(['cp', fsidTableFile, fsidTableBakFile]) + logFile.write("Running through sort callsigns add FSID script" + '\n') + sortCallsignsAddFSID( + bpsScriptOutput, fsidTableFile, sortedOutput, logFile) + ########################################################################### + + ########################################################################### + # If interactive, prompt for running fixParams # + ########################################################################### + if wfaFlag: + runFixParamsFlag = True + elif interactive: + accepted = False + while not accepted: + value = input("Run fixParams? (y/n): ") + if value == "y": + accepted = True + runFixParamsFlag = True + elif value == "n": + accepted = True + runFixParamsFlag = False + else: + print("ERROR: Invalid input: " + value + ", must be y or n") + else: + runFixParamsFlag = True + ########################################################################### + + # output filename from fixParams + paramOutput = sortedOutput.replace(".csv", "_param.csv") + + ########################################################################### + # If runFixParamsFlag set, run fixParams # + ########################################################################### + if runFixParamsFlag: + logFile.write("Running fixParams" + '\n') + fixParams(sortedOutput, paramOutput, logFile, False) + ########################################################################### + + ########################################################################### + # If interactive, prompt for running convertULS # + ########################################################################### + if wfaFlag: + runConvertULSFlag = True + elif interactive: + accepted = False + while not accepted: + value = input("Run conversion of CSV file to sqlite? (y/n): ") + if value == "y": + accepted = True + runConvertULSFlag = True + elif value == "n": + accepted = True + runConvertULSFlag = False + else: + print("ERROR: Invalid input: " + value + ", must be y or n") + else: + runConvertULSFlag = True + ########################################################################### + + # output filename from convertULS + outputSQL = paramOutput.replace('.csv', '.sqlite3') + + ########################################################################### + # If runConvertULSFlag set, run convertULS # + ########################################################################### + if runConvertULSFlag: + convertULS(paramOutput, fullPathRASDabataseFile, + fullPathAntennaPatternFile, state_root, logFile, outputSQL) + storeDataIdentities(outputSQL, dataIdentities) + ########################################################################### + + finishTime = datetime.datetime.now() + + ########################################################################### + # Record execution time in logFile and close log file # + ########################################################################### + logFile.write('Update finished at: ' + finishTime.isoformat() + '\n') + timeDiff = finishTime - startTime + logFile.write('Update took ' + + str(timeDiff.total_seconds()) + ' seconds' + '\n') + logFile.close() + ########################################################################### + + os.chdir(root) # change back to root of this script + + ########################################################################### + # If not interactive: # + # * create zip file containing intermediate file for debugging # + # * copy sqlite file to ULS_Database directory for use by afc-engine # + ########################################################################### + if not interactive: + print("Creating and moving debug files\n") + # create debug zip containing final csv, anomalous_uls, and warning_uls + # and move it to where GUI can see + try: + if wfaFlag: + dirName = "WFA_testvector_FS_" + nameTime + else: + dirName = str(nameTime + "_debug") + subprocess.call(['mkdir', dirName]) + + # Get all files in temp dir + for file in os.listdir(fullPathTempDir): + fullPathFile = fullPathTempDir + "/" + file + if (not os.path.isdir(fullPathFile)): + subprocess.call(['cp', fullPathFile, dirName]) + + # anomalousPath = root + '/' + 'anomalous_uls.csv' + # warningPath = root + '/' + 'warning_uls.txt' + # subprocess.call(['mv', anomalousPath, dirName]) + # subprocess.call(['mv', warningPath, dirName]) + # subprocess.call(['cp', paramOutput, dirName]) + + shutil.make_archive(dirName, 'zip', root, dirName) + zipName = dirName + ".zip" + shutil.rmtree(dirName) # delete debug directory + subprocess.call(['mv', zipName, state_root + '/ULS_Database/']) + except Exception as e: + print('Error moving debug files:' + '\n') + raise e + + # copy sqlite to where GUI can see it + print("Copying sqlite file" + '\n') + try: + subprocess.call(['cp', outputSQL, state_root + '/ULS_Database/']) + except Exception as e: + print('Error copying ULS sqlite:' + '\n') + raise e + + with open(root + '/data_files/lastSuccessfulRun.txt', 'w') as timeFile: + timeFile.write(finishTime.isoformat()) + ########################################################################### + + return finishTime.isoformat() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Process FS link data for AFC.') + parser.add_argument('-i', '--interactive', action='store_true') + parser.add_argument('-ca', '--combine_antenna_region', + action='store_true') + parser.add_argument('-wfa', '--wfa', action='store_true') + parser.add_argument('-unii_us', '--unii_us', default='5:7', + help='":" separated list of unii bands for US') + + parser.add_argument('-r', '--region', default='US:CA', + help='":" separated list of regions') + + args = parser.parse_args() + interactive = args.interactive + + uniiStr = args.unii_us + + includeUnii5US = False + includeUnii6US = False + includeUnii7US = False + includeUnii8US = False + uniiList = uniiStr.split(':') + for u in uniiList: + if u == '5': + includeUnii5US = True + elif u == '6': + includeUnii6US = True + elif u == '7': + includeUnii7US = True + elif u == '8': + includeUnii8US = True + else: + raise Exception('ERROR: Unrecognized unii band: ' + u) + + combineAntennaRegionFlag = args.combine_antenna_region + wfaFlag = args.wfa + + print("Interactive = " + str(interactive)) + print("Include UNII-5 US = " + str(includeUnii5US)) + print("Include UNII-6 US = " + str(includeUnii6US)) + print("Include UNII-7 US = " + str(includeUnii7US)) + print("Include UNII-8 US = " + str(includeUnii8US)) + + if not (includeUnii5US or includeUnii6US or includeUnii7US or includeUnii8US): + raise Exception('ERROR: No UNII-Bands specified for US') + + print("Combine Antenna Region = " + str(combineAntennaRegionFlag)) + print("Region = " + args.region) + print("WFA = " + str(wfaFlag)) + + regionList = args.region.split(':') + + processUS = False + processCA = False + for r in regionList: + if r == 'US': + processUS = True + elif r == 'CA': + processCA = True + else: + raise Exception('ERROR: Unrecognized region: ' + r) + + print("Process US = " + str(processUS)) + print("Process CA = " + str(processCA)) + + if not (processUS or processCA): + raise Exception('ERROR: No regions specified') + + daily_uls_parse("/mnt/nfs/rat_transfer", interactive) diff --git a/src/ratapi/ratapi/db/data_files/modcod_bps.csv b/src/ratapi/ratapi/db/data_files/modcod_bps.csv new file mode 100644 index 0000000..0534437 --- /dev/null +++ b/src/ratapi/ratapi/db/data_files/modcod_bps.csv @@ -0,0 +1,299 @@ +ULS Mod,Bits/Symbol +0QPSK,2 +0QPSKS,2 +1024,10 +1024 Q,10 +1024 QA,10 +1024QA,10 +1024 QAM,10 +1024QAM,10 +1027QAM,10 +104QAM,10 +1128QAM,7 +1248 QA,7 +124QAM,10 +1258 QA,7 +126 QAM,7 +127QAM,7 +127 TCM,7 +1280QAM,7 +1285 QA,7 +128,7 +1289QAM,7 +128AM,7 +128Q,7 +128 QA,7 +128QA,7 +128 QAM,7 +128-QAM,7 +128.QAM,7 +128/QAM,7 +128QAM,7 +128 QM,7 +128QM,7 +128 QUA,7 +128QUAM,7 +128 TC,7 +128TC,7 +128 TCM,7 +128-TCM,7 +128.TCM,7 +128/TCM,7 +128TCM,7 +128 TDM,7 +129 QAM,7 +138 TCM,7 +138-TCM,7 +156 QAM,8 +156TCM,8 +164 QAM,7 +16 APSK,4 +16APSK,4 +16AQM,4 +16 QAM,4 +16-QAM,4 +16/QAM,4 +16QAM,4 +16 QAM,4 +16 QUAM,4 +16QUAM,4 +2024QAM,11 +20484QA,11 +2048 QA,11 +2048QA,11 +2048QAM,11 +225 QPR,6 +225QPR,6 +225QPRD,6 +225QPRS,6 +24QPRS,5 +253 QAM,8 +253 TCM,8 +2560QAM,8 +256&64,8 +256,8 +256AM,8 +256 AQM,8 +256Q,8 +256 QAA,8 +256 QAM,8 +256-QAM,8 +256/QAM,8 +256QAM,8 +256 QAN,8 +256 QM,8 +256 TCM,8 +256TCM,8 +25 QPRS,5 +25QPRS,5 +260 QAM,8 +265 QAM,8 +265QAM,8 +32,5 +32 APSK,5 +32APSK,5 +32 QAM,5 +32-QAM,5 +32/QAM,5 +32QAM,5 +32QAM64,6 +32 TCM,5 +32-TCM,5 +32/TCM,5 +32TCM,5 +32 TCMM,5 +336QPRS,9 +34 QAM,5 +356 TCM,8 +4096QA,12 +4096QAM,12 +490PRS,6 +49GPRS,6 +49QPR,6 +49 QPRS,6 +49QPRS,6 +49 QRPS,6 +49 QRS,6 +4 QAM,2 +4/QAM,2 +4 QAM,2 +4QAM,2 +4QPSK,2 +5126QAM,9 +512,9 +512Q,9 +512 QAM,9 +512/QAM,9 +512QAM,9 +512QM,9 +513QAM,9 +51QAM,9 +62 QAM,6 +63 QAM,6 +640 QAM,6 +64,6 +64AM,6 +64AM Q,6 +64Q,6 +64QAD,6 +64 QAM,6 +64-QAM,6 +64.QAM,6 +64/QAM,6 +64QAM,6 +64 QAM,6 +64 QM,6 +64 QQAM,6 +64QQAM,6 +64 QUAM,6 +64QUAM,6 +64STQAM,6 +64 TCM,6 +64TCM,6 +65 QAM,6 +8 PSK,3 +8-PSK,3 +8PSK,3 +8 QAM,3 +8QAM,3 +8 VSB,3 +9 QPRS,4 +9QPRS,4 +APSK/13,5 +APSK/16,4 +APSK/32,5 +ASPK/32,5 +PSK/8,3 +PSK8,3 +QA/128,7 +QA/32,5 +QAAM512,9 +QAM/102,10 +QAM1024,10 +QAM 128,7 +QAM/128,7 +QAM128,7 +QAM/129,7 +QAM/152,9 +QAM 16,4 +QAM /16,4 +QAM 16,4 +QAM/16,4 +QAM16,4 +QAM2048,11 +QAM 256,8 +QAM/256,8 +QAM256,8 +QAM/258,8 +QAM/28,7 +QAM 32,5 +QAM*32,5 +QAM//32,5 +QAM/32,5 +QAM32,5 +QAM4096,12 +QAM 4,2 +QAM/4,2 +QAM4,2 +QAM 512,9 +QAM/512,9 +QAM512,9 +QAM/'6,4 +QAM 64,6 +QAM/ 64,6 +QAM/64,6 +QAM64,6 +QAM 8,3 +QAM/8,3 +QAM8512,9 +QAM,QAM +QAN/128,7 +QAQM32,5 +QPR-9,4 +QPRS225,6 +QPRS/25,5 +QPRS-9,4 +QPRS/9,4 +QPSK 0,2 +QPSK/0,2 +QPSK,2 +QPSSK/0,2 +QSPK/0,2 +QSPK,2 +QUAM/64,6 +TCM/126,7 +TCM 128,7 +TCM/128,7 +TCM128,7 +TCM 256,8 +TCM/256,8 +TCM 32,5 +TCM-32,5 +TCM/32,5 +TCM32,5 +TCM/64,6 +TCM64,6 +TMC/128,7 +TSM/128,7 +WAM/16,4 +WAM/64,6 +?4QAM,6 +126QAM,7 +0.001,7 +161000,7 +14029,5 +182900,8 +27M3D7W,7 +28 QAM,7 +30M0D7W,7 +30M0D7W,6 +30M0D7W,7 +30M0D7W,10 +4,2 +43.3,2 +512000,9 +54QAM,6 +56M0D7W,8 +5M00D7W,7 +64.2,8 +64000,6 +65,7 +66.8,9 +66.9,7 +67.9,6 +QAM/`6,4 +QAM/204,11 +QAM/409,12 +QAM/PSK,10 +QAM56,8 +QM,6 +0BPSK,1 +1024AM,10 +1024Q,10 +1028QAM,10 +1048QAM,10 +116QAM,4 +16AM,4 +2048Q,11 +210000,9 +30M0G7W,2 +32AM,5 +4086QAM,12 +4096Q,12 +512AM,9 +52QAM,9 +64QM,6 +68.2,7 +8VSB,3 +BPSK,1 +DIG,7 +DIG,6 +PSK,3 +QAAM/16,4 +QAM1204,10 +QAM//16,4 +QAM62,6 +QAN,3 +QPRS,6 +TMC,6 +VSB/8,3 diff --git a/src/ratapi/ratapi/db/fix_bps.py b/src/ratapi/ratapi/db/fix_bps.py new file mode 100644 index 0000000..43aa59c --- /dev/null +++ b/src/ratapi/ratapi/db/fix_bps.py @@ -0,0 +1,180 @@ +#!/usr/bin/python + +import csv +import sys +import math + +modmapBPS = {} +cslistBPS = {} + + +def RepresentInt(s): + try: + int(s) + return True + except BaseException: + return False + + +def RepresentFloat(s): + try: + float(s) + return True + except BaseException: + return False + + +def getBPSFromMod(mod_type, cs): + try: + bits_per_sym = int(modmapBPS[mod_type]) + if (len(cslistBPS[mc]) > 0): + if cs not in cslistBPS[mc]: + bits_per_sym = -1 + return bits_per_sym + except BaseException: + return -1 + + +def getEmdegBW(emdeg): + chList = ["H", "K", "M", "G", "T"] + ba = bytearray(bytes(emdeg, encoding='utf8')) + found = False + sVal = 1.0 + for (chIdx, ch) in enumerate(chList): + if not found: + strpos = ba.find(bytes(ch, encoding='utf8')) + if strpos != -1: + ba[strpos] = ord('.') + scale = sVal + found = True + + sVal *= 1000.0 + bandwidth = float(ba) * scale + return bandwidth + + +def fixBPS(inputPath, modcodFile, outputPath): + with open(modcodFile, 'r') as f: + for ln in f: + lns = ln.strip().split(',') + mc = lns.pop(0) + bps = lns.pop(0) + modmapBPS[mc] = bps + cslistBPS[mc] = [] + for cs in lns: + cslistBPS[mc].append(cs) + + csmap = {} + + modmapEmdegScale = {} + modmapEmdegScale["M"] = 1.0e6 + + with open(inputPath, 'r') as f: + with open(outputPath, 'w') as fout: + csvreader = csv.reader(f, delimiter=',') + csvwriter = csv.writer(fout, delimiter=',') + firstRow = True + linenum = 0 + for row in csvreader: + linenum = linenum + 1 + if firstRow: + row.append("mod_rate (BPS)") + row.append("spectral_efficiency (bit/Hz)") + row.append("comment") + csvwriter.writerow(row) + firstRow = False + callsignIdx = -1 + pathIdx = -1 + freqIdx = -1 + emdegIdx = -1 + digitalModRateIdx = -1 + modTypeIdx = -1 + for fieldIdx, field in enumerate(row): + if field == "Callsign": + callsignIdx = fieldIdx + elif field == "Path Number": + pathIdx = fieldIdx + elif field == "Center Frequency (MHz)": + freqIdx = fieldIdx + elif field == "Emissions Designator": + emdegIdx = fieldIdx + elif field == "Digital Mod Rate": + digitalModRateIdx = fieldIdx + elif field == "Digital Mod Type": + modTypeIdx = fieldIdx + if callsignIdx == -1: + sys.exit('ERROR: Callsign not found') + if pathIdx == -1: + sys.exit('ERROR: Path Number not found') + if freqIdx == -1: + sys.exit('ERROR: Center Frequency (MHz) not found') + if emdegIdx == -1: + sys.exit('ERROR: Emissions Designator not found') + if digitalModRateIdx == -1: + sys.exit('ERROR: Digital Mod Rate not found') + if modTypeIdx == -1: + sys.exit('ERROR: Digital Mod Type not found') + else: + cs = row[callsignIdx] + path = int(row[pathIdx]) + freq = row[freqIdx] + emdeg = row[emdegIdx][0:4] + mod_type = row[modTypeIdx] + mod_rate_bps = "" + spectral_efficiency = "" + comment = "" + processFlag = True + removeFlag = False + mod_type = mod_type.replace(',', '') + + if RepresentFloat(row[digitalModRateIdx]): + mod_rate = float(row[digitalModRateIdx]) + if mod_rate == 0.0: + comment = "INVALID Digital Mod Rate = " + \ + str(mod_rate) + # print ("WARN: "+ comment + " for callsign " + cs + " at line "+ str(linenum)) + processFlag = False + else: + processFlag = False + + # if "TCM" in mod_type or "VSB" in mod_type or "FM" in mod_type: + # print linenum, "Removed line, mod_type = " + mod_type + # removeFlag = True + + bits_per_sym = getBPSFromMod(mod_type, cs) + if bits_per_sym == -1: + comment = "INVALID mod_type = " + mod_type + # print ("WARN: "+ comment + " for callsign " + cs + " at line "+ str(linenum)) + processFlag = False + + if emdeg == "": + processFlag = False + + if processFlag: + bw = getEmdegBW(emdeg) + + se = mod_rate / bw + if se == 0.0: + comment = "ERROR: se = 0.0, bw = " + \ + str(bw) + ", mod_rate = " + str(mod_rate) + print(comment + " for callsign " + + cs + " at line " + str(linenum)) + scale = bits_per_sym / se + scaleFactor = 1.0 + if scale > 1.0: + while scale > 10.0: + scaleFactor = scaleFactor * 10 + scale = scale / 10 + else: + while scale < 1.0: + scaleFactor = scaleFactor / 10 + scale = scale * 10 + + mod_rate_bps = mod_rate * scaleFactor + spectral_efficiency = mod_rate_bps / bw + + if not removeFlag: + row.append(mod_rate_bps) + row.append(spectral_efficiency) + row.append(comment) + csvwriter.writerow(row) diff --git a/src/ratapi/ratapi/db/fix_params.py b/src/ratapi/ratapi/db/fix_params.py new file mode 100644 index 0000000..b3d469f --- /dev/null +++ b/src/ratapi/ratapi/db/fix_params.py @@ -0,0 +1,1002 @@ +import csv +import sys +import math +from os.path import exists + +csmap = {} +fsidmap = {} +c = 2.99792458e8 +unii5StartFreqMHz = 5925 +unii5StopFreqMHz = 6425 +unii6StartFreqMHz = 6425 +unii6StopFreqMHz = 6525 +unii7StartFreqMHz = 6525 +unii7StopFreqMHz = 6875 +unii8StartFreqMHz = 6875 +unii8StopFreqMHz = 7125 + +typicalReflectorDimensions = [ + (1.83, 2.44), + (2.44, 3.05), + (2.44, 3.66), + (3.05, 4.57), + (3.05, 4.88), + (3.05, 7.32), + (3.66, 4.88), + (4.27, 4.88), + (4.88, 6.10), + (4.88, 7.32), + (6.10, 7.32), + (6.10, 9.14), + (6.10, 9.75), + (7.32, 9.14), + (9.14, 9.75), + (9.14, 12.19), + (9.14, 14.63), + (12.19, 15.24)] + + +def isTypicalReflectorDimension(height, width): + found = False + for refDim in typicalReflectorDimensions: + typicalHeight = refDim[0] + typicalWidth = refDim[1] + if (abs(typicalHeight - height) <= + 0.1) and (abs(typicalWidth - width) <= 0.1): + found = True + break + return found + + +def fixParams(inputPath, outputPath, logFile, backwardCompatiblePR): + logFile.write('Fixing parameters' + '\n') + logFile.write('inputPath = ' + inputPath + '\n') + logFile.write('outputPath = ' + outputPath + '\n') + logFile.write('backwardCompatiblePR = ' + str(backwardCompatiblePR) + '\n') + + file_handle = open(inputPath, 'r') + csvreader = csv.DictReader(file_handle) + + fieldnames = csvreader.fieldnames + [ + 'return_FSID', + 'Rx Gain (dBi)', + 'Rx Height to Center RAAT (m)', + 'Rx Diversity Gain (dBi)', + 'Rx Diversity Height to Center RAAT (m)', + 'Tx Gain (dBi)', + 'Tx Height to Center RAAT (m)', + 'Rx Near Field Ant Diameter (m)', + 'Rx Near Field Dist Limit (m)', + 'Rx Near Field Ant Efficiency' + ] + + maxNumPR = 0 + for field in csvreader.fieldnames: + wordList = field.split() + if (wordList[0] == 'Passive') and (wordList[1] == 'Repeater'): + n = int(wordList[2]) + if (n > maxNumPR): + maxNumPR = n + print('maxNumPR = {}'.format(str(maxNumPR))) + + for prIdx in range(maxNumPR): + prTxGainULSFixStr = 'Passive Repeater ' + \ + str(prIdx + 1) + ' ULS Fixed Back-to-Back Gain Tx (dBi)' + prRxGainULSFixStr = 'Passive Repeater ' + \ + str(prIdx + 1) + ' ULS Fixed Back-to-Back Gain Rx (dBi)' + prTxGainStr = 'Passive Repeater ' + \ + str(prIdx + 1) + ' Back-to-Back Gain Tx (dBi)' + prRxGainStr = 'Passive Repeater ' + \ + str(prIdx + 1) + ' Back-to-Back Gain Rx (dBi)' + prRxAntDiameterStr = 'Passive Repeater ' + \ + str(prIdx + 1) + ' Rx Ant Diameter (m)' + prTxAntDiameterStr = 'Passive Repeater ' + \ + str(prIdx + 1) + ' Tx Ant Diameter (m)' + + prWidthULSFixStr = 'Passive Repeater ' + \ + str(prIdx + 1) + ' ULS Fixed Reflector Width (m)' + prHeightULSFixStr = 'Passive Repeater ' + \ + str(prIdx + 1) + ' ULS Fixed Reflector Height (m)' + prWidthStr = 'Passive Repeater ' + \ + str(prIdx + 1) + ' Reflector Width (m)' + prHeightStr = 'Passive Repeater ' + \ + str(prIdx + 1) + ' Reflector Height (m)' + + fieldnames += [prTxGainULSFixStr, + prRxGainULSFixStr, + prTxGainStr, + prRxGainStr, + prRxAntDiameterStr, + prTxAntDiameterStr, + prWidthULSFixStr, + prHeightULSFixStr, + prWidthStr, + prHeightStr] + + if backwardCompatiblePR: + fieldnames += ["Passive Receiver Indicator", + "Passive Repeater Lat Coords", + "Passive Repeater Long Coords", + "Passive Repeater Height to Center RAAT (m)"] + + entriesFixed = 0 + with open(outputPath, 'w') as fout: + csvwriter = csv.writer(fout, delimiter=',') + csvwriter = csv.DictWriter(fout, fieldnames) + csvwriter.writeheader() + + for count, row in enumerate(csvreader): + region = row['Region'] + FRN = row['FRN'] + freq = float(row['Center Frequency (MHz)']) + bandwidth = float(row['Bandwidth (MHz)']) + + lowFreq = freq - bandwidth / 2 + highFreq = freq + bandwidth / 2 + + row['return_FSID'] = '' + row['Rx Gain (dBi)'] = row['Rx Gain ULS (dBi)'] + row['Tx Gain (dBi)'] = row['Tx Gain ULS (dBi)'] + + row['Rx Height to Center RAAT (m)'] = row['Rx Height to Center RAAT ULS (m)'] + row['Rx Diversity Height to Center RAAT (m)'] = row['Rx Diversity Height to Center RAAT ULS (m)'] + row['Tx Height to Center RAAT (m)'] = row['Tx Height to Center RAAT ULS (m)'] + + numPR = int(row['Num Passive Repeater']) + for prIdx in range(numPR): + prNum = prIdx + 1 + prTxGainULSFixStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Fixed Back-to-Back Gain Tx (dBi)' + prRxGainULSFixStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Fixed Back-to-Back Gain Rx (dBi)' + prTxGainStr = 'Passive Repeater ' + \ + str(prNum) + ' Back-to-Back Gain Tx (dBi)' + prRxGainStr = 'Passive Repeater ' + \ + str(prNum) + ' Back-to-Back Gain Rx (dBi)' + prRxAntDiameterStr = 'Passive Repeater ' + \ + str(prNum) + ' Rx Ant Diameter (m)' + prTxAntDiameterStr = 'Passive Repeater ' + \ + str(prNum) + ' Tx Ant Diameter (m)' + prWidthULSFixStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Fixed Reflector Width (m)' + prHeightULSFixStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Fixed Reflector Height (m)' + prWidthStr = 'Passive Repeater ' + \ + str(prNum) + ' Reflector Width (m)' + prHeightStr = 'Passive Repeater ' + \ + str(prNum) + ' Reflector Height (m)' + + rxGainULSStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Back-to-Back Gain Rx (dBi)' + txGainULSStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Back-to-Back Gain Tx (dBi)' + prHeightULSStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Reflector Height (m)' + prWidthULSStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Reflector Width (m)' + prAntModelDiameterStr = 'Passive Repeater ' + \ + str(prNum) + ' Ant Model Diameter (m)' + + row[prTxGainULSFixStr] = row[txGainULSStr] + row[prRxGainULSFixStr] = row[rxGainULSStr] + row[prTxGainStr] = row[txGainULSStr] + row[prRxGainStr] = row[rxGainULSStr] + row[prRxAntDiameterStr] = row[prAntModelDiameterStr] + row[prTxAntDiameterStr] = row[prAntModelDiameterStr] + row[prWidthULSFixStr] = row[prWidthULSStr] + row[prHeightULSFixStr] = row[prHeightULSStr] + row[prWidthStr] = row[prWidthULSStr] + row[prHeightStr] = row[prHeightULSStr] + + if region == 'US': + if (highFreq > unii5StartFreqMHz) and ( + lowFreq < unii5StopFreqMHz): + uniiband = 5 + elif (highFreq > unii6StartFreqMHz) and (lowFreq < unii6StopFreqMHz): + uniiband = 6 + elif (highFreq > unii7StartFreqMHz) and (lowFreq < unii7StopFreqMHz): + uniiband = 7 + elif (highFreq > unii8StartFreqMHz) and (lowFreq < unii8StopFreqMHz): + uniiband = 8 + else: + sys.exit( + 'ERROR in fix_params.py: freq found not in UNII-5, UNII-6, UNII-7, UNII-8') + + keyv = tuple([region, FRN, uniiband]) + else: + keyv = tuple([region, FRN]) + + if keyv in csmap: + csmap[keyv].append(row) + else: + csmap[keyv] = [row] + file_handle.close() + + all_cs = list(csmap.keys()) + for keyv in sorted(all_cs): + matchmap = {} + for ri in range(len(csmap[keyv])): + matchmap[ri] = -1 + r = csmap[keyv][ri] + numPR = int(r['Num Passive Repeater']) + for prIdx in range(numPR): + prNum = prIdx + 1 + rxGainULSStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Back-to-Back Gain Rx (dBi)' + txGainULSStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Back-to-Back Gain Tx (dBi)' + rxGainULSFixStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Fixed Back-to-Back Gain Rx (dBi)' + txGainULSFixStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Fixed Back-to-Back Gain Tx (dBi)' + prAntModelDiameterStr = 'Passive Repeater ' + \ + str(prNum) + ' Ant Model Diameter (m)' + rxAntDiameterStr = 'Passive Repeater ' + \ + str(prNum) + ' Rx Ant Diameter (m)' + txAntDiameterStr = 'Passive Repeater ' + \ + str(prNum) + ' Tx Ant Diameter (m)' + + prHeightULSStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Reflector Height (m)' + prWidthULSStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Reflector Width (m)' + prHeightULSFixStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Fixed Reflector Height (m)' + prWidthULSFixStr = 'Passive Repeater ' + \ + str(prNum) + ' ULS Fixed Reflector Width (m)' + + prHeightAntennaStr = 'Passive Repeater ' + \ + str(prNum) + ' Ant Model Reflector Height (m)' + prWidthAntennaStr = 'Passive Repeater ' + \ + str(prNum) + ' Ant Model Reflector Width (m)' + + prHeightStr = 'Passive Repeater ' + \ + str(prNum) + ' Reflector Height (m)' + prWidthStr = 'Passive Repeater ' + \ + str(prNum) + ' Reflector Width (m)' + + prTypeStr = 'Passive Repeater ' + str(prNum) + ' Ant Type' + + if keyv[0] == 'US': + r[rxGainULSFixStr] = r[rxGainULSStr] + r[txGainULSFixStr] = r[txGainULSStr] + r[rxAntDiameterStr] = r[prAntModelDiameterStr] + r[txAntDiameterStr] = r[prAntModelDiameterStr] + r[prHeightULSFixStr] = r[prHeightULSStr] + r[prWidthULSFixStr] = r[prWidthULSStr] + r[prHeightStr] = '' + r[prWidthStr] = '' + + # R2-AIP-30 (a) + if (r[prTypeStr] == 'Ant') or ( + r[prTypeStr] == 'UNKNOWN'): + rxFlag = False + txFlag = False + rxGainULS = 0.0 + txGainULS = 0.0 + if (r[rxGainULSStr].strip() != ''): + rxGainULS = float(r[rxGainULSStr]) + if ((rxGainULS >= 32.0) and (rxGainULS <= 48.0)): + rxFlag = True + if (r[txGainULSStr].strip() != ''): + txGainULS = float(r[txGainULSStr]) + if ((txGainULS >= 32.0) and (txGainULS <= 48.0)): + txFlag = True + if (r[prTypeStr] == 'UNKNOWN'): + if rxFlag or txFlag: + r[prTypeStr] = 'Ant' + else: + r[prTypeStr] = 'Ref' + + if (r[prTypeStr] == 'Ant'): + # R2-AIP-30 (d) + if rxFlag and (not txFlag): + r[txGainULSFixStr] = r[rxGainStrULS] + # R2-AIP-30 (e) + elif txFlag and (not rxFlag): + r[rxGainULSFixStr] = r[txGainULSStr] + + if (r[prTypeStr] == 'Ref'): + # R2-AIP-30 (b) + if (r[prHeightULSStr].strip() == '') or ( + r[prWidthULSStr].strip() == ''): + r[prHeightULSFixStr] = str(4.88) + r[prWidthULSFixStr] = str(6.10) + # 2022.11.08: Need to confirm this + elif (float(r[prHeightULSStr]) + 0.1 < 1.83) or (float(r[prWidthULSStr]) + 0.1 < 2.44): + r[prHeightULSFixStr] = str(4.88) + r[prWidthULSFixStr] = str(6.10) + + prHeightULS = float(r[prHeightULSFixStr]) + prWidthULS = float(r[prWidthULSFixStr]) + # R2-AIP-29-b-iii-1 + if (r[prHeightAntennaStr].strip() == '') or ( + r[prWidthAntennaStr].strip() == ''): + prHeight = prHeightULS + prWidth = prWidthULS + else: + prHeightAnt = float(r[prHeightAntennaStr]) + prWidthAnt = float(r[prWidthAntennaStr]) + + # R2-AIP-29-b-iii-2 + if (abs(prHeightAnt - prHeightULS) <= + 0.1) and (abs(prWidthAnt - prWidthULS) <= 0.1): + prHeight = prHeightULS + prWidth = prWidthULS + else: + ulsTypicalFlag = isTypicalReflectorDimension( + prHeightULS, prWidthULS) + antTypicalFlag = isTypicalReflectorDimension( + prHeightAnt, prWidthAnt) + + # R2-AIP-29-b-iii-3 + # R2-AIP-29-b-iii-5 + if (ulsTypicalFlag == antTypicalFlag): + if (prHeightULS * prWidthULS > + prHeightAnt * prWidthAnt): + prHeight = prHeightULS + prWidth = prWidthULS + else: + prHeight = prHeightAnt + prWidth = prWidthAnt + + # R2-AIP-29-b-iii-4 + elif ulsTypicalFlag: + prHeight = prHeightULS + prWidth = prWidthULS + elif antTypicalFlag: + prHeight = prHeightAnt + prWidth = prWidthAnt + # R2-AIP-29-b-iv + else: + prHeight = prHeightAnt + prWidth = prWidthAnt + + r[prHeightStr] = str(prHeight) + r[prWidthStr] = str(prWidth) + elif keyv[0] == 'CA': + if (r[prTypeStr] == 'Ref'): + # R2-AIP-29-CAN-a + if (r[prHeightStr].strip() == '') or ( + r[prWidthStr].strip() == ''): + r[prHeightStr] = str(7.32) + r[prWidthStr] = str(9.14) + + if backwardCompatiblePR: + if numPR == 0: + r["Passive Receiver Indicator"] = "N" + r["Passive Repeater Lat Coords"] = "" + r["Passive Repeater Long Coords"] = "" + r["Passive Repeater Height to Center RAAT (m)"] = "" + else: + r["Passive Receiver Indicator"] = "Y" + r["Passive Repeater Lat Coords"] = r['Passive Repeater ' + + str(numPR) + ' Lat Coords'] + r["Passive Repeater Long Coords"] = r['Passive Repeater ' + + str(numPR) + ' Long Coords'] + r["Passive Repeater Height to Center RAAT (m)"] = r['Passive Repeater ' + str( + numPR) + ' Height to Center RAAT (m)'] + + for ri in range(len(csmap[keyv])): + r = csmap[keyv][ri] + numPR = int(r['Num Passive Repeater']) + if keyv[0] == 'US': + if (keyv[2] == 5): + Fc_unii = ( + unii5StartFreqMHz + unii5StopFreqMHz) * 0.5e6 + elif (keyv[2] == 6): + Fc_unii = ( + unii6StartFreqMHz + unii6StopFreqMHz) * 0.5e6 + elif (keyv[2] == 7): + Fc_unii = ( + unii7StartFreqMHz + unii7StopFreqMHz) * 0.5e6 + elif (keyv[2] == 8): + Fc_unii = ( + unii8StartFreqMHz + unii8StopFreqMHz) * 0.5e6 + else: + sys.exit( + 'ERROR in fix_params.py: freq found not in UNII-5, UNII-6, UNII-7, UNII-8') + + for prNum in range(numPR + 1): + + if prNum == 0: + numDirection = 1 + antType = 'Ant' + else: + numDirection = 2 + antType = r['Passive Repeater ' + + str(prNum) + ' Ant Type'] + + for direction in range(numDirection): + if direction == 0: + fwd = 'Rx' + ret = 'Tx' + else: + fwd = 'Tx' + ret = 'Rx' + + fwdGain = '' + if prNum == 0: + fwdGainStrULS = 'Rx Gain ULS (dBi)' + fwdGainStr = 'Rx Gain (dBi)' + retGainStrULS = 'Tx Gain ULS (dBi)' + retGainStr = 'Tx Gain (dBi)' + fwdAntDiameterStr = 'Rx Ant Diameter (m)' + retAntDiameterStr = 'Tx Ant Diameter (m)' + fwdHeightStr = 'Rx Height to Center RAAT (m)' + retHeightStr = 'Tx Height to Center RAAT (m)' + fwdAntMidbandGain = 'Rx Ant Midband Gain (dB)' + retAntMidbandGain = 'Tx Ant Midband Gain (dB)' + else: + fwdGainStrULS = 'Passive Repeater ' + \ + str(prNum) + ' ULS Fixed Back-to-Back Gain ' + \ + fwd + ' (dBi)' + fwdGainStr = 'Passive Repeater ' + \ + str(prNum) + ' Back-to-Back Gain ' + \ + fwd + ' (dBi)' + retGainStrULS = 'Passive Repeater ' + \ + str(numPR + 1 - prNum) + \ + ' ULS Fixed Back-to-Back Gain ' + \ + ret + ' (dBi)' + retGainStr = 'Passive Repeater ' + \ + str(numPR + 1 - prNum) + \ + ' Back-to-Back Gain ' + ret + ' (dBi)' + fwdAntDiameterStr = 'Passive Repeater ' + \ + str(prNum) + ' ' + fwd + \ + ' Ant Diameter (m)' + retAntDiameterStr = 'Passive Repeater ' + \ + str(numPR + 1 - prNum) + ' ' + \ + ret + ' Ant Diameter (m)' + fwdHeightStr = 'Passive Repeater ' + \ + str(prNum) + \ + ' Height to Center RAAT ' + fwd + ' (m)' + retHeightStr = 'Passive Repeater ' + \ + str(numPR + 1 - prNum) + \ + ' Height to Center RAAT ' + ret + ' (m)' + fwdAntMidbandGain = 'Passive Repeater ' + \ + str(prNum) + ' Ant Model Midband Gain (dB)' + retAntMidbandGain = 'Passive Repeater ' + \ + str(numPR + 1 - prNum) + \ + ' Ant Model Midband Gain (dB)' + + # R2-AIP-05 (e) + if (antType == 'Ant') and ( + r[fwdGainStrULS].strip() == ''): + if (keyv[2] == 5): + fwdGain = 38.8 + elif (keyv[2] == 6): + fwdGain = 39.5 + elif (keyv[2] == 7): + fwdGain = 39.5 + elif (keyv[2] == 8): + fwdGain = 39.5 + else: + sys.exit( + 'ERROR in fix_params.py: freq found not in UNII-5, UNII-6, UNII-7, UNII-8') + + # 6 ft converted to m + r[fwdAntDiameterStr] = 6 * 12 * 2.54 / 100 + + # R2-AIP-05 (a) + elif (antType == 'Ant') and (float(r[fwdAntDiameterStr]) != -1): + fwdGainULS = float(r[fwdGainStrULS]) + Drx = float(r[fwdAntDiameterStr]) + Eta = 0.55 + Gtypical = 10 * \ + math.log10( + Eta * (math.pi * Fc_unii * Drx / c)**2) + if (fwdGainULS >= Gtypical - + 0.7) and (fwdGainULS <= Gtypical + 0.7): + fwdGain = fwdGainULS + elif (r[fwdAntMidbandGain].strip() != ''): + fwdGain = float(r[fwdAntMidbandGain]) + else: + fwdGain = Gtypical + + # R2-AIP-05 (b): Does not apply to passive + # repeaters + elif (prNum == 0) and (float(r[retAntDiameterStr]) != -1) and (r[retGainStrULS].strip() != ''): + fwdGainULS = float(r[fwdGainStrULS]) + retGainULS = float(r[retGainStrULS]) + if (abs(fwdGainULS - retGainULS) <= 0.05): + D = float(r[retAntDiameterStr]) + Eta = 0.55 + Gtypical = 10 * \ + math.log10( + Eta * (math.pi * Fc_unii * D / c)**2) + if (fwdGainULS >= Gtypical - + 0.7) and (fwdGainULS <= Gtypical + 0.7): + fwdGain = fwdGainULS + elif (r[retAntMidbandGain].strip() != ''): + fwdGain = float(r[retAntMidbandGain]) + else: + fwdGain = Gtypical + r[fwdAntDiameterStr] = D + + if (antType == 'Ant') and (fwdGain == ''): + matchFlagFwdGain = True + else: + matchFlagFwdGain = False + + if float(r[fwdHeightStr]) == -1: + matchFlagFwdHeight = True + else: + matchFlagFwdHeight = False + + if float(r[retHeightStr]) == -1: + matchFlagRetHeight = True + else: + matchFlagRetHeight = False + + if (matchFlagFwdGain or matchFlagFwdHeight or matchFlagRetHeight) and r['return_FSID'].strip( + ) == '': + rxLat = float(r['Rx Lat Coords']) + rxLon = float(r['Rx Long Coords']) + txLat = float(r['Tx Lat Coords']) + txLon = float(r['Tx Long Coords']) + foundMatch = False + foundCandidate = False + for (mi, m) in enumerate(csmap[keyv]): + if (not foundMatch) and (mi != ri): + m_rxLat = float(m['Rx Lat Coords']) + m_rxLon = float(m['Rx Long Coords']) + m_txLat = float(m['Tx Lat Coords']) + m_txLon = float(m['Tx Long Coords']) + m_numPR = int( + m['Num Passive Repeater']) + if ((abs(rxLon - m_txLon) < 2.78e-4) and (abs(rxLat - m_txLat) < 2.78e-4) + and (abs(txLon - m_rxLon) < 2.78e-4) and (abs(txLat - m_rxLat) < 2.78e-4) and (numPR == m_numPR)): + prIdx = 1 + prMatch = True + while (prMatch and ( + prIdx <= numPR)): + prLat = float( + r['Passive Repeater ' + str(prIdx) + ' Lat Coords']) + prLon = float( + r['Passive Repeater ' + str(prIdx) + ' Long Coords']) + prType = r['Passive Repeater ' + + str(prIdx) + ' Ant Type'] + m_prLat = float( + m['Passive Repeater ' + str(numPR + 1 - prIdx) + ' Lat Coords']) + m_prLon = float( + m['Passive Repeater ' + str(numPR + 1 - prIdx) + ' Long Coords']) + m_prType = r['Passive Repeater ' + + str(numPR + 1 - prIdx) + ' Ant Type'] + if (abs(prLon - m_prLon) < 2.78e-4) and ( + abs(prLat - m_prLat) < 2.78e-4) and (prType == m_prType): + prIdx += 1 + else: + prMatch = False + if prMatch: + if m['return_FSID'].strip() == '': + foundMatch = True + matchi = mi + else: + foundCandidate = True + candidatei = mi + if foundMatch: + m = csmap[keyv][matchi] + csmap[keyv][ri]['return_FSID'] = m['FSID'] + csmap[keyv][matchi]['return_FSID'] = r['FSID'] + matchmap[ri] = matchi + matchmap[matchi] = ri + logFile.write('Matched FSID: ' + + str(r['FSID']) + + ' and ' + + str(m['FSID']) + + ' numPR = ' + + str(numPR) + + '\n') + elif foundCandidate: + m = csmap[keyv][candidatei] + csmap[keyv][ri]['return_FSID'] = '-' + \ + m['FSID'] + matchmap[ri] = candidatei + logFile.write('FSID: ' + + str(r['FSID']) + + ' using candidate match ' + + str(m['FSID']) + + ' numPR = ' + + str(numPR) + + '\n') + + matchi = matchmap[ri] + + # R2-AIP-14 (a) + if matchFlagFwdHeight: + if prNum == 0: + r[fwdHeightStr] = '42.5' + elif (antType == 'Ant'): + r[fwdHeightStr] = '8.5' + else: + r[fwdHeightStr] = '7.0' + if matchi != -1: + m = csmap[keyv][matchi] + + if m[retHeightStr].strip() != '': + m_retHeightAGL = float(m[retHeightStr]) + if m_retHeightAGL > 1.5: + r[fwdHeightStr] = str( + m_retHeightAGL) + + if matchFlagRetHeight: + if prNum == 0: + r[retHeightStr] = '42.5' + elif (antType == 'Ant'): + r[retHeightStr] = '8.5' + else: + r[retHeightStr] = '7.0' + + if matchi != -1: + m = csmap[keyv][matchi] + + if m[fwdHeightStr].strip() != '': + m_fwdHeightAGL = float(m[fwdHeightStr]) + if m_fwdHeightAGL > 1.5: + r[retHeightStr] = str( + m_fwdHeightAGL) + + # R2-AIP-05 (c) + if matchFlagFwdGain: + if matchi != -1: + m = csmap[keyv][matchi] + if (float(m[retAntDiameterStr]) != - + 1) and (m[retGainStrULS].strip() != ''): + + fwdGainULS = float(r[fwdGainStrULS]) + m_txGainULS = float(m[retGainStrULS]) + if (abs(fwdGainULS - m_txGainULS) <= 0.05): + D = float(m[retAntDiameterStr]) + Eta = 0.55 + Gtypical = 10 * \ + math.log10( + Eta * (math.pi * Fc_unii * D / c)**2) + if (fwdGainULS >= Gtypical - + 0.7) and (fwdGainULS <= Gtypical + 0.7): + fwdGain = fwdGainULS + elif (m[retAntMidbandGain].strip() != ''): + fwdGain = float( + m[retAntMidbandGain]) + else: + fwdGain = Gtypical + r[fwdAntDiameterStr] = str(D) + + # R2-AIP-05 (d) + if (antType == 'Ant') and (fwdGain == ''): + fwdGainULS = float(r[fwdGainStrULS]) + if fwdGainULS < 32.0: + fwdGain = 32.0 + elif fwdGainULS > 48.0: + fwdGain = 48.0 + else: + fwdGain = fwdGainULS + oneOverSqrtEta = 1.0 / math.sqrt(0.55) + D = (c / (math.pi * Fc_unii)) * \ + (10**((fwdGain) / 20)) * oneOverSqrtEta + csmap[keyv][ri][fwdAntDiameterStr] = str(D) + + if (antType == 'Ant'): + r[fwdGainStr] = str(fwdGain) + + ########################################################### + # R2-AIP-08 Diversity Antenna Gain and Diameter # + # R2-AIP-15 Diversity Antenna Height # + ########################################################### + rxDiameterStr = 'Rx Ant Diameter (m)' + rxGainStrULS = 'Rx Gain ULS (dBi)' + rxGainStr = 'Rx Gain (dBi)' + diversityDiameterStr = 'Rx Diversity Ant Diameter (m)' + diversityGainStrULS = 'Rx Diversity Gain ULS (dBi)' + diversityGainStr = 'Rx Diversity Gain (dBi)' + rxAntModelMatchedStr = 'Rx Ant Model Name Matched' + rxHeightStr = 'Rx Height to Center RAAT (m)' + diversityHeightStr = 'Rx Diversity Height to Center RAAT (m)' + + if (r[diversityGainStrULS] != '') and ( + float(r[diversityGainStrULS]) != 0.0): + if (r[rxGainStrULS] != '') and (r[rxAntModelMatchedStr] != '') and ( + abs(float(r[rxGainStrULS]) - float(r[diversityGainStrULS])) < 0.05): + r[diversityGainStr] = r[rxGainStr] + r[diversityDiameterStr] = r[rxDiameterStr] + else: + diversityGainULS = float(r[diversityGainStrULS]) + if (diversityGainULS >= 28.0) and ( + diversityGainULS <= 48.0): + r[diversityGainStr] = r[diversityGainStrULS] + else: + r[diversityGainStr] = r[rxGainStrULS] + + diversityGain = float(r[diversityGainStr]) + oneOverSqrtEta = 1.0 / math.sqrt(0.55) + D = (c / (math.pi * Fc_unii)) * \ + (10**((diversityGain) / 20)) * oneOverSqrtEta + r[diversityDiameterStr] = str(D) + + if (r[diversityHeightStr] == ''): + rxHeight = float(r[rxHeightStr]) + if (rxHeight < 14.0): + r[diversityHeightStr] = str(rxHeight + 11) + else: + r[diversityHeightStr] = str(rxHeight - 11) + + if (float(r[diversityHeightStr]) < 1.5): + r[diversityHeightStr] = str(1.5) + ########################################################### + + ########################################################### + # R2-AIP-17 Near Field Adjustment # + ########################################################### + nearFieldDiameterStr = 'Rx Near Field Ant Diameter (m)' + nearFieldDistLimitStr = 'Rx Near Field Dist Limit (m)' + nearFieldEfficiencyStr = 'Rx Near Field Ant Efficiency' + + if (r[rxAntModelMatchedStr] != ''): + rxNearFieldAntennaDiameter = float(r[rxDiameterStr]) + method = 1 + if (keyv[2] == 5): + if (abs(rxNearFieldAntennaDiameter - + 3.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 32.0 + gainRangeMax = 34.5 + foundDiameter = True + elif (abs(rxNearFieldAntennaDiameter - 4.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 34.5 + gainRangeMax = 37.55 + foundDiameter = True + elif (abs(rxNearFieldAntennaDiameter - 6.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 37.55 + gainRangeMax = 40.35 + foundDiameter = True + elif (abs(rxNearFieldAntennaDiameter - 8.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 40.35 + gainRangeMax = 42.55 + foundDiameter = True + elif (abs(rxNearFieldAntennaDiameter - 10.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 42.55 + gainRangeMax = 44.55 + foundDiameter = True + elif (abs(rxNearFieldAntennaDiameter - 12.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 44.55 + gainRangeMax = 46.15 + foundDiameter = True + elif (abs(rxNearFieldAntennaDiameter - 15.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 46.15 + gainRangeMax = 48.0 + foundDiameter = True + else: + foundDiameter = False + else: + if (abs(rxNearFieldAntennaDiameter - + 3.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 32.0 + gainRangeMax = 34.55 + foundDiameter = True + elif (abs(rxNearFieldAntennaDiameter - 4.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 34.55 + gainRangeMax = 37.65 + foundDiameter = True + elif (abs(rxNearFieldAntennaDiameter - 6.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 37.65 + gainRangeMax = 40.55 + foundDiameter = True + elif (abs(rxNearFieldAntennaDiameter - 8.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 40.55 + gainRangeMax = 42.75 + foundDiameter = True + elif (abs(rxNearFieldAntennaDiameter - 10.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 42.75 + gainRangeMax = 44.55 + foundDiameter = True + elif (abs(rxNearFieldAntennaDiameter - 12.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 44.55 + gainRangeMax = 46.25 + foundDiameter = True + elif (abs(rxNearFieldAntennaDiameter - 15.0 * 12 * 2.54 * 0.01) <= 0.01): + gainRangeMin = 46.25 + gainRangeMax = 48.0 + foundDiameter = True + else: + foundDiameter = False + + elif (r[rxGainStrULS] == ''): + diameterFt = 6.0 + rxNearFieldAntennaDiameter = diameterFt * 12.0 * 2.54 * 0.01 # convert ft to m + method = 3 + elif (keyv[2] == 5): + rxGainULS = float(r[rxGainStrULS]) + if ((rxGainULS >= 32.0) and (rxGainULS <= 48.0)): + # ****************************************************************************# + # * Table 3: U-NII-5 Antenna Size versus Gain *# + # ****************************************************************************# + if (rxGainULS <= 34.35): + diameterFt = 3.0 + elif (rxGainULS <= 37.55): + diameterFt = 4.0 + elif (rxGainULS <= 40.35): + diameterFt = 6.0 + elif (rxGainULS <= 42.55): + diameterFt = 8.0 + elif (rxGainULS <= 44.55): + diameterFt = 10.0 + elif (rxGainULS <= 46.15): + diameterFt = 12.0 + else: + diameterFt = 15.0 + # ****************************************************************************# + rxNearFieldAntennaDiameter = diameterFt * 12.0 * 2.54 * 0.01 # convert ft to m + method = 2 + else: + diameterFt = 6.0 + rxNearFieldAntennaDiameter = diameterFt * 12.0 * 2.54 * 0.01 # convert ft to m + method = 3 + else: + rxGainULS = float(r[rxGainStrULS]) + if ((rxGainULS >= 32.0) and (rxGainULS <= 48.0)): + # ****************************************************************************# + # * Table 4: U-NII-7 Antenna Size versus Gain *# + # ****************************************************************************# + if (rxGainULS <= 34.55): + diameterFt = 3.0 + elif (rxGainULS <= 37.65): + diameterFt = 4.0 + elif (rxGainULS <= 40.55): + diameterFt = 6.0 + elif (rxGainULS <= 42.75): + diameterFt = 8.0 + elif (rxGainULS <= 44.55): + diameterFt = 10.0 + elif (rxGainULS <= 46.25): + diameterFt = 12.0 + else: + diameterFt = 15.0 + # ****************************************************************************# + rxNearFieldAntennaDiameter = diameterFt * \ + 12.0 * 2.54 * 0.01 # convert ft to m + method = 2 + else: + diameterFt = 6.0 + rxNearFieldAntennaDiameter = diameterFt * 12.0 * 2.54 * 0.01 # convert ft to m + method = 3 + + if (r[rxGainStrULS] == ''): + effDB = -2.6 + else: + rxGainULS = float(r[rxGainStrULS]) + if (method == 1) and (not foundDiameter): + effDB = -2.6 + elif (method == 1) and ((rxGainULS < gainRangeMin - 0.3) or (rxGainULS > gainRangeMax + 0.3)): + effDB = -2.6 + elif method == 3: + effDB = -2.6 + else: + effDB = rxGainULS + 20.0 * \ + math.log10( + c / (math.pi * Fc_unii * rxNearFieldAntennaDiameter)) + + rxNearFieldDistLimit = 2 * Fc_unii * \ + rxNearFieldAntennaDiameter * rxNearFieldAntennaDiameter / c + + rxNearFieldAntEfficiency = 10**(effDB / 10.0) + if (rxNearFieldAntEfficiency < 0.4): + rxNearFieldAntEfficiency = 0.4 + elif (rxNearFieldAntEfficiency > 0.7): + rxNearFieldAntEfficiency = 0.7 + + r[nearFieldDiameterStr] = str(rxNearFieldAntennaDiameter) + r[nearFieldDistLimitStr] = str(rxNearFieldDistLimit) + r[nearFieldEfficiencyStr] = str(rxNearFieldAntEfficiency) + ########################################################### + + elif keyv[0] == 'CA': + freq = float(r['Center Frequency (MHz)']) + + for prIdx in range(2 * numPR + 1): + if prIdx == 0: + prNum = 0 + prDir = 0 + antType = 'Ant' + else: + prNum = ((prIdx - 1) % numPR) + 1 + prDir = (prIdx - 1) // numPR + antType = r['Passive Repeater ' + + str(prNum) + ' Ant Type'] + + if prNum == 0: + fwdGainStrULS = 'Rx Gain ULS (dBi)' + fwdGainStr = 'Rx Gain (dBi)' + fwdHeightStr = 'Rx Height to Center RAAT (m)' + else: + if prDir == 0: + prDirStr = 'Rx' + else: + prDirStr = 'Tx' + fwdGainStrULS = 'Passive Repeater ' + \ + str(prNum) + ' ULS Fixed Back-to-Back Gain ' + \ + prDirStr + ' (dBi)' + fwdGainStr = 'Passive Repeater ' + \ + str(prNum) + ' Back-to-Back Gain ' + \ + prDirStr + ' (dBi)' + fwdHeightStr = 'Passive Repeater ' + \ + str(prNum) + ' Height to Center RAAT ' + \ + prDirStr + ' (m)' + + # R2-AIP-05-CAN + # R2-AIP-39-CAN + if (antType == "Ant"): + if r[fwdGainStrULS].strip() == '' or float( + r[fwdGainStrULS]) < 0.0: + if (freq <= 6425.0): + r[fwdGainStr] = '41.7' + else: + r[fwdGainStr] = '42.1' + + # R2-AIP-14-CAN + # R2-AIP-XX-CAN + if r[fwdHeightStr].strip() == '': + r[fwdHeightStr] = '56' + elif float(r[fwdHeightStr]) < 1.5: + r[fwdHeightStr] = '1.5' + else: + freq = float(r['Center Frequency (MHz)']) + + for prIdx in range(2 * numPR + 1): + if prIdx == 0: + prNum = 0 + prDir = 0 + antType = 'Ant' + else: + prNum = ((prIdx - 1) % numPR) + 1 + prDir = (prIdx - 1) // numPR + antType = r['Passive Repeater ' + + str(prNum) + ' Ant Type'] + + if prNum == 0: + fwdGainStr = 'Rx Gain (dBi)' + fwdAntDiameterStr = 'Rx Ant Diameter (m)' + else: + if prDir == 0: + prDirStr = 'Rx' + else: + prDirStr = 'Tx' + fwdGainStr = 'Passive Repeater ' + \ + str(prNum) + ' Back-to-Back Gain ' + \ + prDirStr + ' (dBi)' + fwdAntDiameterStr = 'Passive Repeater ' + \ + str(prNum) + ' ' + fwd + ' Ant Diameter (m)' + + if (antType == "Ant"): + if (r[fwdAntDiameterStr] == '-1'): + gain = float(r[fwdGainStr]) + wavelength = 2.99792458e2 / freq + D = wavelength * 10**((gain - 7.7) / 20.0) + r[fwdAntDiameterStr] = str(D) + + for ri, r in enumerate(csmap[keyv]): + fsid = int(r['FSID']) + if fsid in fsidmap: + sys.exit('ERROR: FSID: ' + str(fsid) + " not unique\n") + fsidmap[fsid] = tuple([ri, keyv]) + + for fsid in sorted(fsidmap.keys()): + ri = fsidmap[fsid][0] + keyv = fsidmap[fsid][1] + r = csmap[keyv][ri] + + numPR = int(r['Num Passive Repeater']) + for prIdx in range(numPR): + prNum = prIdx + 1 + prType = r['Passive Repeater ' + str(prNum) + ' Ant Type'] + if (prType == 'Ant') and False: + rxGainStr = 'Passive Repeater ' + \ + str(prNum) + ' Back-to-Back Gain Rx (dBi)' + txGainStr = 'Passive Repeater ' + \ + str(prNum) + ' Back-to-Back Gain Tx (dBi)' + rxGain = r[rxGainStr] + txGain = r[txGainStr] + txFlag = False + if txGain != '': + if ((float(txGain) >= 32.0) and (float(txGain) <= 48.0)): + txFlag = True + if not txFlag: + r[txGainStr] = rxGain + csvwriter.writerow(r) diff --git a/src/ratapi/ratapi/db/generators.py b/src/ratapi/ratapi/db/generators.py new file mode 100644 index 0000000..f5ffbbb --- /dev/null +++ b/src/ratapi/ratapi/db/generators.py @@ -0,0 +1,341 @@ +''' Convert files to different formats ''' + +import logging +import sqlalchemy as sa +from sqlalchemy.orm import sessionmaker +import csv +from numpy import loadtxt, genfromtxt +from .models.base import Base +from .models.population import Population +from .models.uls import ULS + +#: LOGGER for this module +LOGGER = logging.getLogger(__name__) + + +def save_chunk(session, to_save): + ''' Chunked save operation. Falls back to saving individual enties if sqlalchemy is on old version ''' + + if hasattr(session, 'bulk_save_objects'): + session.bulk_save_objects(to_save) + else: + # old version of sqlalchemy don't support bulk save + LOGGER.warning( + "SQL alchemy is using old version. This will increase commit time") + for entity in to_save: + session.add(entity) + + +def load_csv_data(file_name, headers=None): + ''' Loads csv file into python objects + + :param filename: csv file to load + + :param haders: ((name, type), ...) + + :rtype: (iterable rows, file handle or None) + ''' + if headers is None: + file_handle = open(file_name, 'rb') + return (csv.DictReader(file_handle), file_handle) + + (names, formats) = tuple(zip(*headers)) + return (loadtxt(file_name, + skiprows=1, + dtype={ + 'names': names, + 'formats': formats, + }, + delimiter=','), None) + + +def _as_bool(s): + if s == 'Y': + return True + if s == 'N': + return False + return None + + +def _as_float(s): + if s == '' or s is None: + return None + return float(s) + + +def _as_int(s): + if s == '': + return None + return int(s) + + +def create_pop_db(db_name, data_file): + ''' Creates an sqlite database from a csv file + + :param db_name: name of sqlite database to be created + + :param data_file: name of csv file to read + ''' + + LOGGER.debug("creating db") + engine = sa.create_engine('sqlite:///' + db_name + '.sqlite3') + + # drop data from existing table if we are overritting the file + Base.metadata.drop_all(engine, tables=[Population.__table__]) + + # create population table + Base.metadata.create_all(engine, tables=[Population.__table__]) + + LOGGER.debug("creating session") + session = sessionmaker(bind=engine) + s = session() + + try: + LOGGER.debug("loading data file: %s", data_file) + headers = ( + ('Latitude (deg)', 'f8'), + ('Longitude (deg)', 'f8'), + ('Density (people/sq-km)', 'f8'), + ) + (data, file_handle) = load_csv_data(data_file, headers) + + # generate queries in chunks to reduce memory footprint + for chunk in range(0, len(data), 10000): + LOGGER.debug("chunk: %i", chunk) + print("chunk: %i", chunk) + save_chunk(s, + [ + Population( + latitude=row['Latitude (deg)'], + longitude=row['Longitude (deg)'], + density=row['Density (people/sq-km)'], + ) + for row in data[chunk:chunk + 10000] + ] + ) + + if not (file_handle is None): + file_handle.close() + LOGGER.debug("committing") + s.commit() + except Exception as e: + s.rollback() + LOGGER.error(str(e)) + finally: + s.close() + LOGGER.info('Population database created: %s.db', db_name) + + +def create_uls_db(db_name, data_file): + ''' create sqlite database from csv file + + :param db_name: file name of sqlite database to be created + + :param data_file: file name of csv + ''' + + LOGGER.debug("creating db") + engine = sa.create_engine('sqlite:///' + db_name + '.sqlite3') + + # drop data from existing table if we are overritting the file + Base.metadata.drop_all(engine, tables=[ULS.__table__]) + + # create uls table + Base.metadata.create_all(engine, tables=[ULS.__table__]) + + LOGGER.debug("creating session") + session = sessionmaker(bind=engine) + s = session() + + try: + LOGGER.debug("loading data file: %s", data_file) + (data, file_handle) = load_csv_data(data_file) + + # generate queries in chunks to reduce memory footprint + to_save = [] + invalid_rows = 0 + errors = [] + for count, row in enumerate(data): + try: + to_save.append(ULS( + #: FSID + fsid=int(row.get("FSID") or count + 2), + #: Callsign + callsign=row["Callsign"], + #: Status + status=row["Status"], + #: Radio Service + radio_service=row["Radio Service"], + #: Entity Name + name=row["Entity Name"], + #: Mobile + mobile=_as_bool(row["Mobile"]), + #: Rx Callsign + rx_callsign=row["Rx Callsign"], + #: Rx Antenna Number + rx_antenna_num=int(row["Rx Antenna Number"] or '0'), + #: Frequency Assigned (MHz) + freq_assigned_start_mhz=_as_float(row["Frequency Assigned (MHz)"].split( + "-")[0]) if ("-" in str(row["Frequency Assigned (MHz)"])) else float(row["Frequency Assigned (MHz)"]), + freq_assigned_end_mhz=_as_float(row["Frequency Assigned (MHz)"].split( + "-")[1]) if ("-" in str(row["Frequency Assigned (MHz)"])) else float(row["Frequency Assigned (MHz)"]), + #: Tx EIRP (dBm) + tx_eirp=_as_float(row["Tx EIRP (dBm)"]), + #: Emissions Designator + emissions_des=row["Emissions Designator"], + #: Tx Lat Coords + tx_lat_deg=_as_float(row["Tx Lat Coords"]), + #: Tx Long Coords + tx_long_deg=_as_float(row["Tx Long Coords"]), + #: Tx Ground Elevation (m) + tx_ground_elev_m=_as_float( + row["Tx Ground Elevation (m)"]), + #: Tx Polarization + tx_polarization=row["Tx Polarization"], + #: Tx Height to Center RAAT (m) + tx_height_to_center_raat_m=_as_float( + row["Tx Height to Center RAAT (m)"]), + #: Tx Gain (dBi) + tx_gain=_as_float(row["Tx Gain (dBi)"]), + #: Rx Lat Coords + rx_lat_deg=_as_float(row["Rx Lat Coords"]), + #: Rx Long Coords + rx_long_deg=_as_float(row["Rx Long Coords"]), + #: Rx Ground Elevation (m) + rx_ground_elev_m=_as_float( + row["Rx Ground Elevation (m)"]), + #: Rx Ant Model + rx_ant_model=row["Rx Ant Model"], + #: Rx Height to Center RAAT (m) + rx_height_to_center_raat_m=_as_float( + row["Rx Height to Center RAAT (m)"]), + #: Rx Gain (dBi) + rx_gain=_as_float(row["Rx Gain (dBi)"]), + #: Passive Receiver Indicator + p_rx_indicator=( + row.get("Passive Receiver Indicator") or "N"), + #: Passive Repeater Latitude Coords, + p_rp_lat_degs=_as_float( + row.get("Passive Repeater Lat Coords")), + #: Passive Repeater Longitude Coords, + p_rp_lon_degs=_as_float( + row.get("Passive Repeater Lat Coords")), + #: Passive Repeater Height to Center RAAT (m) + p_rp_height_to_center_raat_m=_as_float( + row.get("Passive Repeater Height to Center RAAT (m)")) + )) + except Exception as e: + LOGGER.error(str(e)) + invalid_rows = invalid_rows + 1 + if invalid_rows > 50: + errors.append("{}\nData: {}".format(str(e), str(row))) + + if count % 10000 == 0: + print("chunk: %i", count) + save_chunk(s, to_save) + to_save = [] + + if len(to_save) > 0: + LOGGER.info("remaining chunk: %i", len(to_save)) + save_chunk(s, to_save) + to_save = [] + + if not (file_handle is None): + file_handle.close() + + if len(to_save) > 0: + logging.debug("remaining chunk: %i", len(to_save)) + save_chunk(s, to_save) + to_save = [] + + LOGGER.warn("Invalid rows: %i", invalid_rows) + LOGGER.debug("committing...") + s.commit() + LOGGER.info('ULS database created: %s.sqlite3', db_name) + + return invalid_rows, errors + + except Exception as e: + s.rollback() + LOGGER.error(str(e)) + raise e + finally: + s.close() + + +def shp_to_spatialite(dst, src): + ''' Convert shape file to sql spatialite database + + NOTE: Does NOT sanitize inputs and runs in shell. Be careful of injection attacks + + :param dst: spatialite file name to write to + + :param src: shp file to read from + ''' + from subprocess import check_call, CalledProcessError + import os + + if os.path.exists(dst): + os.remove(dst) + + try: + cmd = ( + 'ogr2ogr ' + + '-f SQLite ' + + '-t_srs "WGS84" ' + + '-dsco SPATIALITE=YES ' + + dst + ' ' + + src + ) + LOGGER.debug(cmd) + check_call(cmd, shell=True) + LOGGER.debug("converted to sqlite.") + + except CalledProcessError as e: + LOGGER.error(e.cmd) + LOGGER.error(e.output) + LOGGER.error( + "Error raised by shp_to_spatialite with code: %i", e.returncode) + raise RuntimeError("Shape file could not be converted to database") + + +def spatialite_to_raster(dst, src, table, elev_field): + ''' Convert spatialite database into raster file + + NOTE: Does NOT sanitize inputs and runs in shell. Be careful of injection attacks + + :param dst: raster file to write to + + :param src: spatialite file to read from + + :param table: name of table in spatialite to read from + + :param elev_field: name of field in table to get raster value from + ''' + from subprocess import check_call, CalledProcessError + import os + + try: + check_call( + 'gdal_rasterize ' + + # use sort to ensure that overlapping polygons give highest hight + # in raster lookup + '-sql "SELECT * FROM {} ORDER BY {}" '.format(table, elev_field) + + # pull from ELEVATION attribute (view in QGIS) + '-a {} '.format(elev_field) + + '-of GTiff ' + # output format + # tile output (optimization) + '-co "TILED=YES" ' + + '-ot Float32 ' + # raster data type + # no data special value + '-a_nodata 0x7FC00000 ' + + # resolution (in units of projection) + '-tr 0.00001 0.00001 ' + + '-a_srs WGS84 ' + + src + ' ' + + dst, shell=True) + + except CalledProcessError as e: + LOGGER.error( + "Error raised by shp_to_spatialite with code: %i", e.returncode) + raise RuntimeError("Spacialite file could not be converted to raster") diff --git a/src/ratapi/ratapi/db/models/__init__.py b/src/ratapi/ratapi/db/models/__init__.py new file mode 100644 index 0000000..26b1f6a --- /dev/null +++ b/src/ratapi/ratapi/db/models/__init__.py @@ -0,0 +1,2 @@ +''' SQL databse table models ''' +from . import base, population, uls diff --git a/src/ratapi/ratapi/db/models/antenna_pattern.py b/src/ratapi/ratapi/db/models/antenna_pattern.py new file mode 100644 index 0000000..48ba493 --- /dev/null +++ b/src/ratapi/ratapi/db/models/antenna_pattern.py @@ -0,0 +1,24 @@ +''' Table for antenna patterns +''' + +import sqlalchemy as sa +from sqlalchemy import types +from base import Base + + +class Population(Base): + ''' Antenna Pattern table + ''' + + __tablename__ = 'antenna_pattern' + __table_args__ = ( + ) + + #: id + id = sa.Column(types.Integer, nullable=False, primary_key=True) + + #: Model + model = sa.Column(types.String(64), nullable=False, index=True) + + #: Pattern (stored as raw array of 1800 32-bit floats) + pattern = sa.Column(types.LargeBinary, nullable=False) diff --git a/src/ratapi/ratapi/db/models/base.py b/src/ratapi/ratapi/db/models/base.py new file mode 100644 index 0000000..0f9c43f --- /dev/null +++ b/src/ratapi/ratapi/db/models/base.py @@ -0,0 +1,17 @@ +''' Base classes underlying model classes. +''' + +import sqlalchemy as sa +from sqlalchemy import types +import sqlalchemy.ext.declarative as declarative + +#: Base class for declarative models +Base = declarative.declarative_base() +#: Naming conventions codified +# Base.metadata.naming_convention = { +# "ix": 'ix_%(column_0_label)s', +# "uq": "uq_%(table_name)s_%(column_0_name)s", +# # "ck": "ck_%(table_name)s_%(constraint_name)s", +# "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", +# "pk": "pk_%(table_name)s" +# } diff --git a/src/ratapi/ratapi/db/models/population.py b/src/ratapi/ratapi/db/models/population.py new file mode 100644 index 0000000..bd431f7 --- /dev/null +++ b/src/ratapi/ratapi/db/models/population.py @@ -0,0 +1,35 @@ +''' Table for population density +''' + +import sqlalchemy as sa +from sqlalchemy import types +from .base import Base + + +class Population(Base): + ''' Population density table + ''' + + __tablename__ = 'population' + __table_args__ = ( + ) + + #: Latitude (deg) + latitude = sa.Column( + types.Float, + sa.CheckConstraint('latitude BETWEEN -90 AND 90'), + nullable=False, + primary_key=True, + index=True) + + #: Longitude (deg) + longitude = sa.Column( + types.Float, + sa.CheckConstraint('longitude BETWEEN -180 AND 180'), + nullable=False, + primary_key=True, + index=True) + + #: Density (people/sq-km) + density = sa.Column(types.Float, sa.CheckConstraint( + 'density >= 0'), nullable=False) diff --git a/src/ratapi/ratapi/db/models/uls.py b/src/ratapi/ratapi/db/models/uls.py new file mode 100644 index 0000000..9d68300 --- /dev/null +++ b/src/ratapi/ratapi/db/models/uls.py @@ -0,0 +1,107 @@ +''' Table for ULS database +''' + +import sqlalchemy as sa +from sqlalchemy import Column, orm +from sqlalchemy.types import String, Boolean, Integer, Float, Date +from .base import Base + + +class ULS(Base): + ''' ULS Database table + ''' + + __tablename__ = 'uls' + __table_args__ = ( + sa.UniqueConstraint('fsid'), + ) + + #: id (internal) + id = Column(Integer, nullable=False, primary_key=True) + + #: FSID + fsid = Column(Integer, nullable=False, index=True) + + #: Callsign + callsign = Column(String(16), nullable=False) + + #: Status + status = Column(String(1), nullable=False) + + #: Radio Service + radio_service = Column(String(4), nullable=False) + + #: Entity Name + name = Column(String(256), nullable=False) + + #: Common Carrier + common_carrier = Column(Boolean) + + #: Mobile + mobile = Column(Boolean) + + #: Rx Callsign + rx_callsign = Column(String(16), nullable=False) + + #: Rx Location Number + # rx_location_num = Column(Integer, nullable=False) + + #: Rx Antenna Number + rx_antenna_num = Column(Integer, nullable=False) + + #: Frequency Assigned (MHz) + freq_assigned_start_mhz = Column(Float, nullable=False) + freq_assigned_end_mhz = Column(Float, nullable=False) + + #: TX EIRP (dBm) + tx_eirp = Column(Float) + + #: Emissions Designator + emissions_des = Column(String(64), nullable=False) + + #: Tx Lat Coords + tx_lat_deg = Column(Float, nullable=False, index=True) + + #: Tx Long Coords + tx_long_deg = Column(Float, nullable=False, index=True) + + #: Tx Ground Elevation (m) + tx_ground_elev_m = Column(Float) + + #: Tx Polarization + tx_polarization = Column(String(1), nullable=False) + + #: Tx Height to Center RAAT (m) + tx_height_to_center_raat_m = Column(Float) + + #: Tx Beamwidth + # tx_beamwidth = Column(Float, nullable=False) + + #: Tx Gain (dBi) + tx_gain = Column(Float) + + #: Rx Lat Coords + rx_lat_deg = Column(Float, nullable=False, index=True) + + #: Rx Long Coords + rx_long_deg = Column(Float, nullable=False, index=True) + + #: Rx Ground Elevation (m) + rx_ground_elev_m = Column(Float) + + #: Rx Ant Model + rx_ant_model = Column(String(64)) + + #: Rx Height to Center RAAT (m) + rx_height_to_center_raat_m = Column(Float) + + #: Rx Gain (dBi) + rx_gain = Column(Float) + + p_rx_indicator = Column(String(1)) + + p_rp_lat_degs = Column(Float, index=True) + + p_rp_lon_degs = Column(Float, index=True) + + p_rp_height_to_center_raat_m = Column(Float) diff --git a/src/ratapi/ratapi/db/processAntennaCSVs.py b/src/ratapi/ratapi/db/processAntennaCSVs.py new file mode 100644 index 0000000..03726ae --- /dev/null +++ b/src/ratapi/ratapi/db/processAntennaCSVs.py @@ -0,0 +1,424 @@ +import csv + + +def fixModelName(modelPrefix, modelName): + ########################################################################## + # Convert modelName to uppercase + ########################################################################## + modelName = modelName.upper() + ########################################################################## + + ########################################################################## + # Remove non-alhpanumeric characters + ########################################################################## + modelName = ''.join(filter(str.isalnum, modelName)) + ########################################################################## + + ########################################################################## + # Prepend prefix + ########################################################################## + modelName = modelPrefix + modelName + ########################################################################## + + return modelName + + +def processAntFiles( + inputDir, + processCA, + combineAntennaRegionFlag, + antennaListFile, + antennaPrefixFile, + antennaPatternFile, + logFile): + logFile.write('Processing antenna files' + '\n') + + antennaModelDiamGainCsvFilePath = inputDir + "/antenna_model_diameter_gain.csv" + billboardReflectorCsvFilePath = inputDir + "/billboard_reflector.csv" + categoryB1AntennasCsvFilePath = inputDir + "/category_b1_antennas.csv" + highPerformanceAntennasCsvFilePath = inputDir + "/high_performance_antennas.csv" + + if processCA: + antennaPatternCsvFilePath = inputDir + "/CA/AP.csv" + + if combineAntennaRegionFlag: + prefixUS = "" + prefixCA = "" + else: + prefixUS = "US:" + prefixCA = "CA:" + + antennaListFilePath = antennaListFile + + # Read in all input CSVs as dictionaries and fetch relevant columns + + ########################################################################## + # Billboard Reflector CSV + ########################################################################## + billboardReflectorReader = csv.DictReader( + open(billboardReflectorCsvFilePath, 'r')) + + reflectorAntennaModelList = [] + reflectorHeightList = [] + reflectorWidthList = [] + for row in billboardReflectorReader: + reflectorAntennaModelList.append( + fixModelName(prefixUS, row['antennaModel'])) + reflectorHeightList.append(row['height_m']) + reflectorWidthList.append(row['width_m']) + ########################################################################## + + ########################################################################## + # Antenna Model/Diameter/Gain CSV + ########################################################################## + antennaModelDiamGainReader = csv.DictReader( + open(antennaModelDiamGainCsvFilePath, 'r')) + + standardModelList = [] + antennaDiameterList = [] + antennaGainList = [] + for row in antennaModelDiamGainReader: + standardModelList.append(fixModelName(prefixUS, row['standardModel'])) + antennaDiameterList.append(row['diameter_m']) + antennaGainList.append(row['gain_dBi']) + ########################################################################## + + ########################################################################## + # Category B1 Antenna CSV + ########################################################################## + categoryB1AntennasReader = csv.DictReader( + open(categoryB1AntennasCsvFilePath)) + antennaModelPrefixB1List = [] + for row in categoryB1AntennasReader: + antennaModelPrefixB1List.append( + fixModelName(prefixUS, row['antennaModelPrefix'])) + ########################################################################## + + ########################################################################## + # HP Antenna CSV + ########################################################################## + highPerformanceAntennasReader = csv.DictReader( + open(highPerformanceAntennasCsvFilePath)) + antennaModelPrefixHpList = [] + for row in highPerformanceAntennasReader: + antennaModelPrefixHpList.append( + fixModelName(prefixUS, row['antennaModelPrefix'])) + ########################################################################## + + ########################################################################## + # Antenna Pattern File from ISED (Canada) + ########################################################################## + antennaPatternModelList = [] + antennaPatternGainList = [] + antennaPatternDiameterList = [] + antennaPatternTypeList = [] + antennaPatternAzimuthList = [] + antennaPatternAttenuationList = [] + if processCA: + antennaPatternReader = csv.DictReader(open(antennaPatternCsvFilePath)) + for row in antennaPatternReader: + antennaPatternModelList.append(fixModelName( + prefixCA, row['Antenna Model Number'])) + antennaPatternGainList.append(row['Antenna Gain [dBi]']) + antennaPatternDiameterList.append(row['Antenna Diameter']) + antennaPatternTypeList.append(row['Pattern Type']) + antennaPatternAzimuthList.append(row['Pattern Azimuth [deg]']) + antennaPatternAttenuationList.append( + row['Pattern Attenuation [dB]']) + ########################################################################## + + antmap = {} + antpatternRaw = {} + antpatternInterp = {} + + # First process antenna models from the antenna_model_diameter_gain CSV + for ind, antennaModel in enumerate(standardModelList): + # All antennas from the antenna_model_diameter_gain CSV have type "Ant" + antType = "Ant" + + # Check if the current antenna model starts with a B1 prefix + category = "OTHER" + if antennaModel.startswith(tuple(antennaModelPrefixB1List)): + category = "B1" + # Now check if the current antenna model starts with a HP prefix + elif antennaModel.startswith(tuple(antennaModelPrefixHpList)): + category = "HP" + + # These lists are both in sync with the antennaModel variable + diameter = antennaDiameterList[ind] + midbandGain = antennaGainList[ind] + + if not diameter: + # print("Current antenna (model name = {}) is empty".format(antennaModel, diameter)) + diameter = '' + if not midbandGain: + # print("Current antenna (model name = {}) is empty".format(antennaModel, midbandGain)) + midbandGain = '' + + if antennaModel in antmap: + r = antmap[antennaModel] + if (r['Diameter (m)'] != diameter) or ( + r['Midband Gain (dBi)'] != midbandGain): + logFile.write( + 'WARNING: Antenna model ' + + antennaModel + + ' defined multiple times in file ' + + antennaModelDiamGainCsvFilePath + + ' with different parameters\n') + else: + row = {} + row['Ant Model'] = antennaModel + row['Type'] = antType + row['Reflector Height (m)'] = '' + row['Reflector Width (m)'] = '' + row['Category'] = category + row['Diameter (m)'] = diameter + row['Midband Gain (dBi)'] = midbandGain + row['Has Pattern'] = '0' + + antmap[antennaModel] = row + + # Now process antenna models from the billboard_reflector CSV + for ind, antennaModel in enumerate(reflectorAntennaModelList): + # All antennas from the billboard_reflector CSV have type "Ref" + antType = "Ref" + + # Check if the current antenna model starts with a B1 prefix + category = "OTHER" + if antennaModel.startswith(tuple(antennaModelPrefixB1List)): + category = "B1" + # Now check if the current antenna model starts with a HP prefix + elif antennaModel.startswith(tuple(antennaModelPrefixHpList)): + category = "HP" + + reflectorHeight = reflectorHeightList[ind] + reflectorWidth = reflectorWidthList[ind] + + if antennaModel in antmap: + r = antmap[antennaModel] + if (r['Reflector Height (m)'] != reflectorHeight) or ( + r['Reflector Width (m)'] != reflectorWidth): + logFile.write( + 'WARNING: Antenna model ' + + antennaModel + + ' defined multiple times in file ' + + billboardReflectorCsvFilePath + + ' with different parameters\n') + else: + row = {} + row['Ant Model'] = antennaModel + row['Type'] = antType + row['Reflector Height (m)'] = reflectorHeight + row['Reflector Width (m)'] = reflectorWidth + row['Category'] = category + row['Diameter (m)'] = '' + row['Midband Gain (dBi)'] = '' + row['Has Pattern'] = '0' + + antmap[antennaModel] = row + + # Now process antenna models from the antenna_pattern CSV + for ind, antennaModel in enumerate(antennaPatternModelList): + # All antennas from the antenna_pattern CSV have type "Ant" + antType = "Ant" + + antennaGain = antennaPatternGainList[ind] + diameter = antennaPatternDiameterList[ind] + type = antennaPatternTypeList[ind] + azimuth = float(antennaPatternAzimuthList[ind]) + attenuation = float(antennaPatternAttenuationList[ind]) + + if type == "HH": + + if antennaModel in antmap: + row = antmap[antennaModel] + if (antmap[antennaModel]['Diameter (m)'] != diameter): + logFile.write( + 'WARNING: Antenna model ' + + antennaModel + + ' multiple diameters specified ' + + diameter + + ' and ' + + antmap[antennaModel]['Diameter (m)'] + + '\n') + antmap[antennaModel]['Diameter (m)'] = diameter + if (antmap[antennaModel]['Midband Gain (dBi)'] != antennaGain): + logFile.write( + 'WARNING: Antenna model ' + + antennaModel + + ' multiple gain values specified ' + + antennaGain + + ' and ' + + antmap[antennaModel]['Midband Gain (dBi)'] + + '\n') + antmap[antennaModel]['Midband Gain (dBi)'] = antennaGain + else: + row = {} + row['Ant Model'] = antennaModel + row['Type'] = antType + row['Reflector Height (m)'] = '' + row['Reflector Width (m)'] = '' + row['Category'] = 'OTHER' + row['Diameter (m)'] = diameter + row['Midband Gain (dBi)'] = antennaGain + antmap[antennaModel] = row + row['Has Pattern'] = '1' + + ################################################################### + # Compute Angle Off Boresight (aob) in [0, 180] # + ################################################################### + aob = azimuth + while aob >= 180.0: + aob = aob - 360.0 + while aob < -180.0: + aob = aob + 360.0 + aob = abs(aob) + ################################################################### + + if antennaModel in antpatternRaw: + antpatternRaw[antennaModel].append(tuple([aob, attenuation])) + else: + antpatternRaw[antennaModel] = [tuple([aob, attenuation])] + + ########################################################################## + # Write antennaListFile (antenna_model_list.csv) # + ########################################################################## + with open(antennaListFilePath, 'w') as fout: + headerList = [ + 'Ant Model', + 'Type', + 'Reflector Height (m)', + 'Reflector Width (m)', + 'Category', + 'Diameter (m)', + 'Midband Gain (dBi)', + 'Has Pattern'] + + csvwriter = csv.writer(fout, delimiter=',') + csvwriter = csv.DictWriter(fout, headerList) + csvwriter.writeheader() + + for antennaModel in sorted(antmap.keys()): + row = antmap[antennaModel] + csvwriter.writerow(row) + ########################################################################## + + ########################################################################## + # Write antennaPrefixFile (antenna_prefix_list.csv) # + ########################################################################## + with open(antennaPrefixFile, 'w') as fout: + headerList = ['Prefix', 'Type', 'Category'] + + csvwriter = csv.writer(fout, delimiter=',') + csvwriter = csv.DictWriter(fout, headerList) + csvwriter.writeheader() + + row = {} + for prefix in antennaModelPrefixB1List: + row['Prefix'] = prefix + row['Type'] = 'Ant' + row['Category'] = 'B1' + csvwriter.writerow(row) + + for prefix in antennaModelPrefixHpList: + row['Prefix'] = prefix + row['Type'] = 'Ant' + row['Category'] = 'HP' + csvwriter.writerow(row) + + ########################################################################## + + aobStep = 0.1 + aobStart = 0.0 + aobStop = 180.0 + numAOB = int(round((aobStop - aobStart) / aobStep)) + 1 + + for antennaModel in sorted(antpatternRaw.keys()): + antpatternRaw[antennaModel].sort(key=lambda y: y[0]) + + ####################################################################### + # For Angle Off Boresight values specified more than once, # + # use min attenuation (largest gain) # + ####################################################################### + i = 0 + while i <= len(antpatternRaw[antennaModel]) - 2: + if antpatternRaw[antennaModel][i][0] == antpatternRaw[antennaModel][i + 1][0]: + if antpatternRaw[antennaModel][i][1] != antpatternRaw[antennaModel][i + 1][1]: + logFile.write( + 'WARNING: Antenna model ' + + antennaModel + + ' angle off boresight ' + + antpatternRaw[antennaModel][i][0] + + ' has multiple attenuations specified\n') + antpatternRaw[antennaModel][i][1] == min( + antpatternRaw[antennaModel][i][1], antpatternRaw[antennaModel][i + 1][1]) + antpatternRaw[antennaModel].pop(i + 1) + else: + i = i + 1 + ####################################################################### + + N = len(antpatternRaw[antennaModel]) + minAOB = antpatternRaw[antennaModel][0][0] + maxAOB = antpatternRaw[antennaModel][N - 1][0] + useFlag = True + + ####################################################################### + # Confirm that there is data over the entire range [0,180] # + ####################################################################### + if (minAOB > 0.0) or (maxAOB < 180.0): + useFlag = False + logFile.write( + 'WARNING: Antenna model ' + + antennaModel + + ' gain values do not cover the angle off boresight range [0,180], range is [' + + minAOB + + ',' + + maxAOB + + ']\n') + ####################################################################### + + ####################################################################### + # Interpolate pattern data to cover [0,180] in 0.1 deg steps # + ####################################################################### + if useFlag: + k = 0 + for i in range(numAOB): + aob = (aobStart * (numAOB - 1 - i) + + aobStop * i) / (numAOB - 1) + while antpatternRaw[antennaModel][k][0] < aob: + k += 1 + if k == 0: + attn = antpatternRaw[antennaModel][0][1] + else: + k1 = k + k0 = k - 1 + aob0 = antpatternRaw[antennaModel][k0][0] + aob1 = antpatternRaw[antennaModel][k1][0] + attn0 = antpatternRaw[antennaModel][k0][1] + attn1 = antpatternRaw[antennaModel][k1][1] + attn = (attn0 * (aob1 - aob) + attn1 * + (aob - aob0)) / (aob1 - aob0) + if antennaModel in antpatternInterp: + antpatternInterp[antennaModel].append(attn) + else: + antpatternInterp[antennaModel] = [attn] + ####################################################################### + + antModelList = sorted(antpatternInterp.keys()) + with open(antennaPatternFile, 'w') as fout: + # Open the output CSV + headerList = ['Off-axis angle (deg)'] + antModelList + + csvwriter = csv.writer(fout, delimiter=',') + csvwriter = csv.DictWriter(fout, headerList) + csvwriter.writeheader() + + for i in range(numAOB): + aob = (aobStart * (numAOB - 1 - i) + aobStop * i) / (numAOB - 1) + row.clear() + row['Off-axis angle (deg)'] = str(aob) + + for antennaModel in antModelList: + row[antennaModel] = str(-1.0 * + antpatternInterp[antennaModel][i]) + csvwriter.writerow(row) diff --git a/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/antenna_model_diameter_gain.csv b/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/antenna_model_diameter_gain.csv new file mode 100644 index 0000000..1405067 --- /dev/null +++ b/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/antenna_model_diameter_gain.csv @@ -0,0 +1,2809 @@ +manufacturer,antennaModel,standardModel,diameter_ft,diameter_m,gain_dBi,notes +MOTOROLA,85009294001,85009294001,6,1.83,38.2,https://www.winncom.com/pdf/Motorola_PTP800_Antennas_6/Motorola_PTP800_Antennas_6.pdf +MOTOROLA,85009294002,85009294002,8,2.44,40.8,https://www.winncom.com/pdf/Motorola_PTP800_Antennas_6/Motorola_PTP800_Antennas_6.pdf +MOTOROLA,85010089021,85010089021,6,1.83,39,https://www.winncom.com/pdf/Motorola_PTP800_Antennas_6/Motorola_PTP800_Antennas_6.pdf +MOTOROLA,85010091006,85010091006,4,1.22,35,https://www.winncom.com/pdf/Motorola_PTP800_Antennas_6/Motorola_PTP800_Antennas_6.pdf +MOTOROLA,85010091007,85010091007,6,1.83,39,https://www.winncom.com/pdf/Motorola_PTP800_Antennas_6/Motorola_PTP800_Antennas_6.pdf +MOTOROLA,85010092021,85010092021,6,1.83,39,https://www.winncom.com/pdf/Motorola_PTP800_Antennas_6/Motorola_PTP800_Antennas_6.pdf +DRAGONWAVE,A-ANG-6G-6,AANG6G6,6,1.83,38.9,https://www.talleycom.com/static/pdf/A-ANT-18G-72-C.pdf +DRAGONWAVE,A-ANT-06G-36-C-A,AANT06G36CA,3,0.91,33,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT-06G-48-C-A,AANT06G48CA,4,1.22,35,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT-06G-72-C-A,AANT06G72CA,6,1.83,39,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT-06G-96-W-A,AANT06G96WA,8,2.44,41.9,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT-6G-6,AANT6G6,6,1.83,38.9,https://www.talleycom.com/static/pdf/A-ANT-18G-72-C.pdf +DRAGONWAVE,A-ANT-6G-6-C,AANT6G6C,6,1.83,38.9,https://www.talleycom.com/static/pdf/A-ANT-18G-72-C.pdf +DRAGONWAVE,A-ANT-6G-72,AANT6G72,6,1.83,39,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE INC,A-ANT-6G-8,AANT6G8,8,2.44,,Indeterminate. Band designator 8 is not known. +DRAGONWAVE,A-ANT-6G-96,AANT6G96,8,2.44,41.9,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT-L6G-48-C-R,AANTL6G48CR,4,1.22,35,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DragonWave,A-ANT-L6G-6-C,AANTL6G6C,6,1.83,38.5,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT-L6G-72-C,AANTL6G72C,6,1.83,38.5,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT-L6G-72-C-A,AANTL6G72CA,6,1.83,38.5,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT-L6G-72-C-R,AANTL6G72CR,6,1.83,38.5,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT_L6G-96-W-A,AANTL6G96WA,8,2.44,41.5,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT-L6G-96-W-A,AANTL6G96WA,8,2.44,41.5,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT-L6G-96-W-R,AANTL6G96WR,8,2.44,41.5,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT-U6G-72,AANTU6G72,6,1.83,39.8,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT-U6G-72-C,AANTU6G72C,6,1.83,39.8,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANT-U6G-72-C-A,AANTU6G72CA,6,1.83,39.8,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +DRAGONWAVE,A-ANY-06G-48-C-A,AANY06G48CA,4,1.22,35,https://www.scribd.com/document/348184480/Andrew-Antenna-Specs +TRANGO,AD6G-3,AD6G3,3.35,1.02,32.5,https://www.gotrango.com/wp-content/uploads/support/documents/datasheets/DS-1001_Rev_D_Trango-Antenna.pdf +TRANGO,AD6G-3T2,AD6G3T2,3.35,1.02,32.5,https://www.gotrango.com/wp-content/uploads/support/documents/datasheets/DS-1001_Rev_D_Trango-Antenna.pdf +TRANGO,AD6G-3-T2,AD6G3T2,3.35,1.02,32.5,https://www.gotrango.com/wp-content/uploads/support/documents/datasheets/DS-1001_Rev_D_Trango-Antenna.pdf +TRANGO,AD6G-4-T2,AD6G4T2,4,1.22,35.6,https://www.gotrango.com/wp-content/uploads/support/documents/datasheets/DS-1001_Rev_D_Trango-Antenna.pdf +TRANGO,AD6G-6-T2,AD6G6T2,6,1.83,39.4,https://www.gotrango.com/wp-content/uploads/support/documents/datasheets/DS-1001_Rev_D_Trango-Antenna.pdf +TRANGO,AD6G-6-T2CATA,AD6G6T2CATA,6,1.83,39.4,https://www.gotrango.com/wp-content/uploads/support/documents/datasheets/DS-1001_Rev_D_Trango-Antenna.pdf +TRANGO,AD6GU-8-T2,AD6GU8T2,8,2.44,42,Comsearch's Frequency Coordination Database +ALCOMA,AL3-6,AL36,3,0.91,33.1,http://www.al-wireless.com/media/document/brochure-en-al3-06mpr-130301.pdf +ALCOMA,AL3-6/MPR,AL36MPR,3,0.91,33.1,http://www.al-wireless.com/media/document/brochure-en-al3-06mpr-130301.pdf +ALCOMA,AL3-6_MPR,AL36MPR,3,0.91,33.1,http://www.al-wireless.com/media/document/brochure-en-al3-06mpr-130301.pdf +ALCOMA,AL4-6/MPR,AL46MPR,4,1.22,35.6,http://www.al-wireless.com/media/document/brochure-en-al4-06mpr-130301.pdf +HUAWEI,AL6D24HS,AL6D24HS,8,2.44,41,https://fccid.io/ANATEL/03790-14-03257/AL6D24HS/C830E611-6064-4969-9403-B4F0A121FE9B/PDF +CERAGON,AM-3-6-A,AM36A,3,0.91,33.5,https://www.sicetelecom.it/wp-content/MU/datasheet/Ceragon-Ceragon-Antennas-Parabola-90cm-11GHz-perDISH90-11-C2-SC-SICE-Distributore-Italiano.pdf +CERAGON,AM-4-6-A,AM46A,4,1.22,35.8,https://www.sociustechnology.com.au/WebRoot/ecshared01/Shops/sociustec/57E4/CD3A/D46A/003C/50E6/AC10/003F/EB59/Ceragon_am-4-freq-circ-cr1_doc-00049561_reva_00.pdf +CERAGON,AM-4-6-R,AM46R,4,1.22,35.8,https://www.sociustechnology.com.au/WebRoot/ecshared01/Shops/sociustec/57E4/CD3A/D46A/003C/50E6/AC10/003F/EB59/Ceragon_am-4-freq-circ-cr1_doc-00049561_reva_00.pdf +CERAGON,AM-4-6-RW,AM46RW,4,1.22,36.5,https://www.sociustechnology.com.au/WebRoot/ecshared01/Shops/sociustec/57E4/CD3A/D46A/003C/50E6/AC10/003F/EB59/Ceragon_am-4-freq-circ-cr1_doc-00049561_reva_00.pdf +ERICSSON,ANT0 1.2 6L HPX,ANT0126LHPX,4,1.22,35.8,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,ANT01.26UHPX,ANT0126UHPX,4,1.22,35.8,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,ANT0 1.8 6L HPX,ANT0186LHPX,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,ANT0 2.4 6L HPX,ANT0246LHPX,8,2.44,42.1,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,ANT0 2.4 6U HPX,ANT0246UHPX,8,2.44,42.1,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,ANT0B3.06LHPX,ANT0B306LHPX,10,3.05,43.5,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,ANT2 1.2 6L HPX,ANT2126LHPX,4,1.22,35.8,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,ANT2 1.8 6L HP,ANT2186LHP,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,ANT2 1.8 6L HPX,ANT2186LHPX,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,ANT2 1.8 6U HPX,ANT2186UHPX,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +Ericsson,ANT2 A 1.8 6L HPX,ANT2A186LHPX,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,ANT3 A 1.2 6L HP,ANT3A126LHP,4,1.22,35.8,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,ANT3_A_1.2_6L_HP,ANT3A126LHP,4,1.22,35.8,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,ANT3 A 1.8 6L HP,ANT3A186LHP,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,ANT3_A_1.8_6L_HP,ANT3A186LHP,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ARC WIRELESS,ARC-UHP-MW-6-4,ARCUHPMW64,4,1.22,36.2,Comsearch's Frequency Coordination Database +HUAWEI,AU6D24HS,AU6D24HS,8,2.44,41.8,https://fccid.io/ANATEL/03789-14-03257/AU6D24HS/87065A42-DA7D-402F-AD09-25D5F1505FBD/PDF +Ericsson,BFZ 622 55/3D01H,BFZ622553D01H,6,1.83,39.3,http://storage.mtender.gov.md/get/f7291f5a-ceb2-4ed0-a95d-d47694abcbe0-1570511924257 +Radio Frequency Systems,DA 10-59,DA1059,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/DA10-59AD +CABLEWAVE SYSTEMS,DA10 59,DA1059,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/DA10-59AD +CABLEWAVE SYSTEMS,DA10.59,DA1059,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/DA10-59AD +RFS,DA10?59,DA1059,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/DA10-59AC +CABLEWAVE SYSTEMS,DA10-59,DA1059,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/DA10-59AD +RFS,DA10-59,DA1059,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/DA10-59AC +RFS,DA10-59A,DA1059A,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/DA10-59AC +CABLEWAVE SYSTEMS,DA10-59AC,DA1059AC,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/DA10-59AC +CABLEWAVE SYSTEMS,DA10-59WAC,DA1059WAC,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/DA10-59AD +RFS,DA10-59WAC,DA1059WAC,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/DA10-59AC +RFS,DA10-59WW,DA1059WW,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/DA10-59AC +CABLEWAVE SYSTEMS,DA10 65,DA1065,10,3.05,44.1,https://www.rfsworld.com/pim/product/html/DA10-65AC +CABLEWAVE SYSTEMS,DA10-65,DA1065,10,3.05,44.1,https://www.rfsworld.com/pim/product/html/DA10-65AC +RFS,DA10-65,DA1065,10,3.05,44.1,https://www.rfsworld.com/pim/product/html/DA10-65AC +RFS,DA10?65A,DA1065A,10,3.05,44.1,https://www.rfsworld.com/pim/product/html/DA10-65AC +RFS,DA10-65A,DA1065A,10,3.05,44.1,https://www.rfsworld.com/pim/product/html/DA10-65AC +CABLEWAVE SYSTEMS,DA10-65AC,DA1065AC,10,3.05,44.1,https://www.rfsworld.com/pim/product/html/DA10-65AC +RFS,DA10-W57A,DA10W57A,10,3.05,43.5,https://www.rfsworld.com/pim/product/html/DA10-W57AC +RFS,DA12-58W,DA1258W,12,3.66,45.2,Comsearch's Frequency Coordination Database +CABLEWAVE,DA12-59,DA1259,12,3.66,45.1,https://www.rfsworld.com/pim/product/html/DA12-59AC +RFS,DA12-59A,DA1259A,12,3.66,45.1,https://www.rfsworld.com/pim/product/html/DA12-59AC +CABLEWAVE SYSTEMS,DA12 65,DA1265,12,3.66,45.8,https://www.rfsworld.com/pim/product/html/DA12-65AC +CABLEWAVE SYSTEMS,DA12-65,DA1265,12,3.66,45.8,https://www.rfsworld.com/pim/product/html/DA12-65AC +RFS,DA12-65A,DA1265A,12,3.66,45.8,https://www.rfsworld.com/pim/product/html/DA12-65AC +CABLEWAVE SYSTEMS,DA6-59,DA659,6,1.83,39,Comsearch's Frequency Coordination Database +RFS,DA6-59,DA659,6,1.83,39,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,DA6-59A,DA659A,6,1.83,39,Comsearch's Frequency Coordination Database +RFS,DA6-59A,DA659A,6,1.83,39,Comsearch's Frequency Coordination Database +RFS,DA6-59AC,DA659AC,6,1.83,39,Comsearch's Frequency Coordination Database +RFS,DA6-59B,DA659B,6,1.83,39,https://alliancecorporation.ca/images/documents/wireless-infrastructure-documents/RFS/Microwave_antennas/Alliance_distributor_RFS_Microwave_Antenna_DA6-59BC.pdf +CABLEWAVE SYSTEMS,DA6-59W.A,DA659WA,6,1.83,39.3,Comsearch's Frequency Coordination Database +RFS,DA6-59WW,DA659WW,6,1.83,39.3,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,DA6-65,DA665,6,1.83,39.9,Comsearch's Frequency Coordination Database +RFS,DA6-65,DA665,6,1.83,39.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,DA6 65A,DA665A,6,1.83,39.8,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,DA6-65A,DA665A,6,1.83,39.8,Comsearch's Frequency Coordination Database +RFS,DA6-65A,DA665A,6,1.83,39.8,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,DA6-65AC,DA665AC,6,1.83,39.8,Comsearch's Frequency Coordination Database +RFS,DA6-65B,DA665B,6,1.83,39.8,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,DA6-65D,DA665D,6,1.83,39.8,Comsearch's Frequency Coordination Database +RFS/Cablewave,DA6W57,DA6W57,6,1.83,39,https://www.rfsworld.com/pim/product/html/DA6-W57BC +RFS,DA6-W578,DA6W578,6,1.83,39,https://www.rfsworld.com/pim/product/html/DA6-W57BC +RFS,DA6-W57A,DA6W57A,6,1.83,39,https://www.rfsworld.com/pim/product/html/DA6-W57BC +RFS,DA6-W57B,DA6W57B,6,1.83,39,https://www.rfsworld.com/pim/product/html/DA6-W57BC +CABLEWAVE SYSTEMS,DA8-59,DA859,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/DA8-59AC +RFS,DA8-59,DA859,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/DA8-59AC +CABLEWAVE SYSTEMS,DA8-59A,DA859A,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/DA8-59AC +RFS,DA8-59A,DA859A,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/DA8-59AC +CABLEWAVE SYSTEMS,DA8-59AC,DA859AC,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/DA8-59AC +CABLEWAVE,DA8-59E,DA859E,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/DA8-59AC +CABLEWAVE SYSTEMS,DA8-59WAC,DA859WAC,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/DA8-59AC +RFS,DA8-59WAC,DA859WAC,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/DA8-59AC +RFS,DA8-59WW,DA859WW,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/DA8-59AC +CABLEWAVE SYSTEMS,DA8 65,DA865,8,2.44,42.3,https://www.rfsworld.com/pim/product/html/DA8-65AC +CABLEWAVE SYSTEMS,DA8-65,DA865,8,2.44,42.3,https://www.rfsworld.com/pim/product/html/DA8-65AC +RFS,DA8-65,DA865,8,2.44,42.3,https://www.rfsworld.com/pim/product/html/DA8-65AC +RFS,DA8?65A,DA865A,8,2.44,42.3,https://www.rfsworld.com/pim/product/html/DA8-65AC +RFS,DA8-65A,DA865A,8,2.44,42.3,https://www.rfsworld.com/pim/product/html/DA8-65AC +CABLEWAVE SYSTEMS,DA8-65AC,DA865AC,8,2.44,42.3,https://www.rfsworld.com/pim/product/html/DA8-65AC +RFS,DA8-W57A,DA8W57A,8,2.44,41.5,https://www.rfsworld.com/pim/product/html/DA8-W57AC +CABLEWAVE SYSTEMS,DAX10-59,DAX1059,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/DAX10-59AC4 +RFS,DAX10-59,DAX1059,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/DAX10-59AC +CABLEWAVE SYSTEMS,DAX10-59A,DAX1059A,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/DAX10-59AC4 +RFS,DAX10-59A,DAX1059A,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/DAX10-59AC +CABLEWAVE SYSTEMS,DAX10-59AC,DAX1059AC,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/DAX10-59AC +CABLEWAVE SYSTEMS,DAX10-59C,DAX1059C,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/DAX10-59AC +RFS/Cablewave,DAX1065,DAX1065,10,3.05,43.9,https://www.rfsworld.com/pim/product/html/DAX10-65AC +RFS,DAX10-65A,DAX1065A,10,3.05,43.9,https://www.rfsworld.com/pim/product/html/DAX10-65AC +RFS,DAX10-W59 (P),DAX10W59P,10,3.05,43.5,https://www.rfsworld.com/pim/product/html/DAX10-W59AC +RFS,DAX10-W59(P),DAX10W59P,10,3.05,43.5,https://www.rfsworld.com/pim/product/html/DAX10-W59AC +RFS,DAX10-W59 RF,DAX10W59RF,10,3.05,43.5,https://www.rfsworld.com/pim/product/html/DAX10-W59AC +CABLEWAVE SYSTEMS,DAX12-59,DAX1259,12,3.66,44.8,https://www.rfsworld.com/pim/product/html/DAX12-59AC +RFS,DAX12-59,DAX1259,12,3.66,44.8,https://www.rfsworld.com/pim/product/html/DAX12-59AC +RFS,DAX12-59A,DAX1259A,12,3.66,44.8,https://www.rfsworld.com/pim/product/html/DAX12-59AC +RFS,DAX12-W59,DAX12W59,12,3.66,44.8,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +CABLEWAVE SYSTEMS,DAX6-59A,DAX659A,6,1.83,38.7,Comsearch's Frequency Coordination Database +RFS,DAX6-59A,DAX659A,6,1.83,38.7,Comsearch's Frequency Coordination Database +RFS,DAX6-59B,DAX659B,6,1.83,38.7,Comsearch's Frequency Coordination Database +RFS,DAX6-59B(P),DAX659BP,6,1.83,38.7,Comsearch's Frequency Coordination Database +,DAX6-65B,DAX665B,6,1.83,39.7,"https://www.rfsworld.com/images/sma/rpe/dax6-65b,%20011103.pdf" +CABLEWAVE SYSTEMS,DAX8-59,DAX859,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/DAX8-59AC +RFS,DAX8-59,DAX859,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/DAX8-59AC +CABLEWAVE SYSTEMS,DAX8-59A,DAX859A,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/DAX8-59AC +RFS,DAX8-59A,DAX859A,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/DAX8-59AC +RFS,DAX8-59W,DAX859W,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/DAX8-59AC +RFS/Cablewave,DAX865,DAX865,8,2.44,42.2,https://www.rfsworld.com/pim/product/html/DAX8-65AC +CABLEWAVE SYSTEMS,DAX8-65,DAX865,8,2.44,42.2,https://www.rfsworld.com/pim/product/html/DAX8-65AD +RFS,DAX8-65A,DAX865A,8,2.44,42.2,https://www.rfsworld.com/pim/product/html/DAX8-65AC +RFS,DAX8-W59A,DAX8W59A,8,2.44,41.3,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +RFS,DBUX12-W60W103ACC,DBUX12W60W103ACC,12,3.66,45.1,https://www.rfsworld.com/pim/product/pdf/DBUX12-W60W103ACC +RFS,DBUX6-W60W103A,DBUX6W60W103A,6,1.83,39.3,https://www.rfsworld.com/pim/product/pdf/DBUX6-W60W103ADB +RFS,DBUX6-W60W103A_L,DBUX6W60W103AL,6,1.83,39.3,https://www.rfsworld.com/pim/product/pdf/DBUX6-W60W103ADB +RFS,DBUX8-W60W103ACC,DBUX8W60W103ACC,8,2.44,41.7,https://www.rfsworld.com/pim/product/pdf/DBUX8-W60W103ACC +RFS,DBUX8-W60W103A_L,DBUX8W60W103AL,8,2.44,41.7,"https://www.rfsworld.com/images/sma/rpe/dbux8/dbux8-w60w103a_l%20(5.925-7.125),%2020201104.pdf" +RFS,DBX8-W60W103,DBX8W60W103,8,2.44,41.7,https://www.rfsworld.com/pim/product/pdf/DBUX8-W60W103ACC +RFS,DBX8-W60W103ACC_L,DBX8W60W103ACCL,8,2.44,41.7,https://www.rfsworld.com/pim/product/pdf/DBUX8-W60W103ACC +GABRIEL,DD10-64BSE,DD1064BSE,10,3.05,44,Comsearch's Frequency Coordination Database +GABRIEL,DD10P-1J23107,DD10P1J23107,10,3.05,42,Comsearch's Frequency Coordination Database +GABRIEL,DD12-64B,DD1264B,12,3.66,45.4,Comsearch's Frequency Coordination Database +GABRIEL,DD12P-59CSE,DD12P59CSE,12,3.66,44.7,Comsearch's Frequency Coordination Database +GABRIEL,DD6-64DSE,DD664DSE,6,1.83,40,Comsearch's Frequency Coordination Database +GABRIEL,DD6W-5971,DD6W5971,6,1.83,39.5,Comsearch's Frequency Coordination Database +GABRIEL,DD8-64BSE,DD864BSE,8,2.44,42.2,Comsearch's Frequency Coordination Database +GABRIEL,DD8-64DSE,DD864DSE,8,2.44,42,Comsearch's Frequency Coordination Database +GABRIEL,DD8W-5971,DD8W5971,8,2.44,40.4,Comsearch's Frequency Coordination Database +GABRIEL,DDP 10P-59BSE,DDP10P59BSE,10,3.05,43.1,Comsearch's Frequency Coordination Database +Gabriel Electronics,DDP6P-59,DDP6P59,6,1.83,38.5,Comsearch's Frequency Coordination Database +GABRIEL,DDP6P-59CSE,DDP6P59CSE,6,1.83,39,Comsearch's Frequency Coordination Database +GABRIEL,DDP8P-59,DDP8P59,8,2.44,40.7,Comsearch's Frequency Coordination Database +GABRIEL,DDP8P-59BSE,DDP8P59BSE,8,2.44,41.2,Comsearch's Frequency Coordination Database +GABRIEL,DFB8-64ASE,DFB864ASE,8,2.44,42.3,Typo should be DRFB864ASE. Using that gain. +GABRIEL,DFRB6-64,DFRB664,6,1.83,39.5,Typo should be DRFB664 using that gain +RADIO WAVES,DP6-59,DP659,6,1.83,38.3,Comsearch's Frequency Coordination Database +RADIO WAVES,DP6-64,DP664,6,1.83,38.8,Comsearch's Frequency Coordination Database +GABRIEL,DP6P-1J23A,DP6P1J23A,6,1.83,38.79,Comsearch's Frequency Coordination Database +RADIO WAVES,DPD6-59,DPD659,6,1.83,38.1,Comsearch's Frequency Coordination Database +GABRIEL,DRF8-59BSE,DRF859BSE,8,2.44,41.3,Typo should be DRFB859BSE. Using that gain. +GABRIEL,DRFB10-59BSE,DRFB1059BSE,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,DRFB10-59CSE,DRFB1059CSE,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,DRFB-10-59CSE,DRFB1059CSE,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,DRFB10-64,DRFB1064,10,3.05,44,Comsearch's Frequency Coordination Database +GABRIEL,DRFB10-64A,DRFB1064A,10,3.05,43.9,Comsearch's Frequency Coordination Database +GABRIEL,DRFB10-64ASE,DRFB1064ASE,10,3.05,43.9,Comsearch's Frequency Coordination Database +GABRIEL,DRFB10 64CSE,DRFB1064CSE,10,3.05,44,Comsearch's Frequency Coordination Database +GABRIEL,DRFB10-64CSE,DRFB1064CSE,10,3.05,44,Comsearch's Frequency Coordination Database +GABRIEL,DRFB10P-59,DRFB10P59,10,3.05,42.9,Comsearch's Frequency Coordination Database +GABRIEL,DRFB10W-5971,DRFB10W5971,10,3.05,43.6,Comsearch's Frequency Coordination Database +Gabriel Electronics,DRFB12-59CSE,DRFB1259CSE,12,3.66,44.2,http://hpwren.ucsd.edu/SCI-5.8GHz-link/rss-57.pdf +GABRIEL,DRFB12-64,DRFB1264,12,3.66,45.5,Comsearch's Frequency Coordination Database +GABRIEL,DRFB12-64A,DRFB1264A,12,3.66,45.5,Comsearch's Frequency Coordination Database +GABRIEL,DRFB12-64BSE,DRFB1264BSE,12,3.66,45.5,Comsearch's Frequency Coordination Database +,DRFB4-59ASE,DRFB459ASE,4,1.22,34.7,http://hpwren.ucsd.edu/SCI-5.8GHz-link/rss-57.pdf +GABRIEL,DRFB6-5968SE,DRFB65968SE,6,1.83,38.9,Comsearch's Frequency Coordination Database +RFS,DRFB6-5968SE,DRFB65968SE,6,1.83,38.9,Comsearch's Frequency Coordination Database +GABRIEL,DRFB6-5971SE,DRFB65971SE,6,1.83,39.6,Comsearch's Frequency Coordination Database +GABRIEL,DRFB6-59BSE,DRFB659BSE,6,1.83,39.1,Comsearch's Frequency Coordination Database +GABRIEL,DRFB6-59 CSE,DRFB659CSE,6,1.83,39.1,https://manualzz.com/doc/28600464/5.690---5.925-ghz-parabolic-antennas +GABRIEL,DRFB6-59CSE,DRFB659CSE,6,1.83,39.1,https://manualzz.com/doc/28600464/5.690---5.925-ghz-parabolic-antennas +GABRIEL,DRFB6-64,DRFB664,6,1.83,39.5,Comsearch's Frequency Coordination Database +GABRIEL,DRFB6-64A,DRFB664A,6,1.83,39.8,Comsearch's Frequency Coordination Database +GABRIEL,DRFB6-64ASE,DRFB664ASE,6,1.83,39.8,Comsearch's Frequency Coordination Database +,,DRFB664BSE,6,1.83,40.1,https://red.uao.edu.co/bitstream/handle/10614/6230/T04236.pdf?sequence=1&isAllowed=y +GABRIEL,DRFB6 64BSE,DRFB664BSE,6,1.83,40.1,Comsearch's Frequency Coordination Database +GABRIEL,DRFB6-64BSE,DRFB664BSE,6,1.83,40.1,Comsearch's Frequency Coordination Database +GABRIEL,DRfB6-64BSE,DRFB664BSE,6,1.83,40.1,Comsearch's Frequency Coordination Database +RADIO WAVES INC,DRFB6-64-BSE,DRFB664BSE,6,1.83,40.1,https://red.uao.edu.co/bitstream/handle/10614/6230/T04236.pdf?sequence=1&isAllowed=y +GABRIEL,DRFB6-64CSE,DRFB664CSE,6,1.83,40.1,Comsearch's Frequency Coordination Database +GABRIEL,DRFB6-6A,DRFB66A,6,1.83,39.8,Typo should be DRFB664A. Using that gain. +GABRIEL,DRFB6-6BSE,DRFB66BSE,6,1.83,40.1,Typo should be DRFB664BSE. Using that gain. +GABRIEL,DRFB6P-59,DRFB6P59,6,1.83,38.5,Comsearch's Frequency Coordination Database +GABRIEL,DRFB6W-5971SE,DRFB6W5971SE,6,1.83,39.6,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8-5968SE,DRFB85968SE,8,2.44,41.4,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8-59ASE,DRFB859ASE,8,2.44,41,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8-59BSE,DRFB859BSE,8,2.44,41.3,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8-59CSE,DRFB859CSE,8,2.44,41.3,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8-64,DRFB864,8,2.44,42,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8-64A,DRFB864A,8,2.44,42.3,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8-64 ASE,DRFB864ASE,8,2.44,42.3,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8-64ASE,DRFB864ASE,8,2.44,42.3,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8-64BSE,DRFB864BSE,8,2.44,42.3,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8 64CSE,DRFB864CSE,8,2.44,42.1,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8-64CSE,DRFB864CSE,8,2.44,42.1,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8P-59,DRFB8P59,8,2.44,41,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8-W-5971-P-RK,DRFB8W5971PRK,8,2.44,40.1,Comsearch's Frequency Coordination Database +GABRIEL,DRFB8W-5971SE,DRFB8W5971SE,8,2.44,40.1,Comsearch's Frequency Coordination Database +Commscope/Andrew,HDH1065,HDH1065,10,3.05,43.9,https://www.datasheets360.com/pdf/-476679127502020136 +ANDREW,HDH10-65,HDH1065,10,3.05,43.9,https://www.datasheets360.com/pdf/-476679127502020136 +COMMSCOPE,HDH10-65 MAIN,HDH1065MAIN,10,3.05,43.9,https://www.datasheets360.com/pdf/-476679127502020136 +Commscope/Andrew,HDH665,HDH665,6,1.83,39.7,https://www.datasheets360.com/pdf/-476679127502020136 +Commscope/Andrew,HDH865,HDH865,8,2.44,42.4,https://www.datasheets360.com/pdf/-476679127502020136 +ANDREW,HDH8-65,HDH865,8,2.44,42.4,https://www.datasheets360.com/pdf/-476679127502020136 +ANDREW,HDH8-65 MAIN,HDH865MAIN,8,2.44,42.4,https://www.datasheets360.com/pdf/-476679127502020136 +COMMSCOPE,HDH8-65 MAIN,HDH865MAIN,8,2.44,42.4,https://www.datasheets360.com/pdf/-476679127502020136 +COMMSCOPE,HDH8-65 Main,HDH865MAIN,8,2.44,42.4,https://www.datasheets360.com/pdf/-476679127502020136 +RADIO WAVES,HDP6-59,HDP659,6,1.83,38.7,Typo should be HPD659. Using that gain. +COMMSCOPE,HDV665,HDV665,6,1.83,39.7,https://www.datasheets360.com/pdf/-476679127502020136 +Commscope/Andrew,HDV665,HDV665,6,1.83,39.7,https://www.datasheets360.com/pdf/-476679127502020136 +COMMSCOPE,HDV6-65,HDV665,6,1.83,39.7,https://www.datasheets360.com/pdf/-476679127502020136 +ANDREW,HDV6-65 MAIN,HDV665MAIN,6,1.83,39.7,https://www.datasheets360.com/pdf/-476679127502020136 +COMMSCOPE,HDV865,HDV865,8,2.44,42.4,https://www.datasheets360.com/pdf/-476679127502020136 +Commscope/Andrew,HDV865,HDV865,8,2.44,42.4,https://www.datasheets360.com/pdf/-476679127502020136 +ANDREW,HDV8-65,HDV865,8,2.44,42.4,https://www.datasheets360.com/pdf/-476679127502020136 +ANDREW,HDV8-65 DIV,HDV865DIV,8,2.44,42.4,https://www.datasheets360.com/pdf/-476679127502020136 +ANDREW,HDV8-65 MAIN,HDV865MAIN,8,2.44,42.4,https://www.datasheets360.com/pdf/-476679127502020136 +COMMSCOPE,HDV8-65 MAIN,HDV865MAIN,8,2.44,42.4,https://www.datasheets360.com/pdf/-476679127502020136 +ANDREW,HDX10-59A L MAIN,HDX1059ALMAIN,10,3.05,43.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HDX10-59A L MAIN,HDX1059ALMAIN,10,3.05,43.9,https://www.datasheets360.com/pdf/-7751896179374140443 +COMMSCOPE,HDX10-59A MAIN LF,HDX1059AMAINLF,10,3.05,43.2,https://www.datasheets360.com/pdf/-7751896179374140443 +COMMSCOPE,HDX10-59A R MAIN,HDX1059ARMAIN,10,3.05,43.2,https://www.datasheets360.com/pdf/-7751896179374140443 +COMMSCOPE,HDX10-59A R MAIN,HDX1059ARMAIN,10,3.05,43.2,https://www.datasheets360.com/pdf/-7751896179374140443 +COMMSCOPE,HDX10-59 MAIN RF,HDX1059MAINRF,10,3.05,43.2,https://www.datasheets360.com/pdf/-7751896179374140443 +ANDREW,HDX10-59 R DIV,HDX1059RDIV,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HDX10-59 RF MAINRF,HDX1059RFMAINRF,10,3.05,43.2,https://www.datasheets360.com/pdf/-7751896179374140443 +ANDREW,HDX12-59 RF MAIN,HDX1259RFMAIN,12,3.66,45,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HDX8-59,HDX859,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HDX8-59 L,HDX859L,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HDX8-59 L MAIN,HDX859LMAIN,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HDX8-59 L MAIN,HDX859LMAIN,8,2.44,41.5,https://www.datasheets360.com/pdf/-7751896179374140443 +ANDREW,HDX8-59L MAIN,HDX859LMAIN,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HDX8-59 MAIN LF,HDX859MAINLF,8,2.44,41.5,https://www.datasheets360.com/pdf/-7751896179374140443 +COMMSCOPE,HDX8-59 MAIN RF,HDX859MAINRF,8,2.44,41.5,https://www.datasheets360.com/pdf/-7751896179374140443 +COMMSCOPE,HDX8-59MAIN RF,HDX859MAINRF,8,2.44,41.5,https://www.datasheets360.com/pdf/-7751896179374140443 +COMMSCOPE,HDX8-59 RF,HDX859RF,8,2.44,41.5,https://www.datasheets360.com/pdf/-7751896179374140443 +COMMSCOPE,HDX8-59 RF MAIN,HDX859RFMAIN,8,2.44,41.5,https://www.datasheets360.com/pdf/-7751896179374140443 +ANDREW,HDX8-59 R MAIN,HDX859RMAIN,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HDX8-59 R MAIN,HDX859RMAIN,8,2.44,41.5,https://www.datasheets360.com/pdf/-7751896179374140443 +Radio Frequency Systems,HDX8-59R MAIN,HDX859RMAIN,8,2.44,41.5,https://www.datasheets360.com/pdf/-7751896179374140443 +COMMSCOPE,HDX8-59 R MAIN RF,HDX859RMAINRF,8,2.44,41.5,https://www.datasheets360.com/pdf/-7751896179374140443 +RADIO WAVES,HO6-59,HO659,6,1.83,39,Typo should be HP659. Using that gain. +ANDREW,HP10 59,HP1059,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP10-59,HP1059,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP10-59,HP1059,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP10-59D,HP1059D,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP10-59D,HP1059D,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP10-59E,HP1059E,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP10-59E,HP1059E,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP10-59F,HP1059F,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP10-59F,HP1059F,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP10-59G,HP1059G,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP10-59G,HP1059G,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP-10-59G,HP1059G,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP10-59-P1A,HP1059P1A,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP10-59W,HP1059W,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP10-59W,HP1059W,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP1059WB,HP1059WB,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP1059WB,HP1059WB,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP10-59WB,HP1059WB,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP10-59WB,HP1059WB,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP10-611E,HP10611E,10,3.05,43,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2F44d80f59%2Fandrew.com%2FHP8-186.pdf +COMMSCOPE,HP10-611E,HP10611E,10,3.05,43,https://www.launch3telecom.com/commscopeandrew/hp10611p1a.html +ANDREW,Hp10-611E,HP10611E,10,3.05,43,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2F44d80f59%2Fandrew.com%2FHP8-186.pdf +ANDREW,HP10-65,HP1065,10,3.05,43.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP10-65,HP1065,10,3.05,43.9,https://www.launch3telecom.com/commscopeandrew/hp1065.html +ANDREW,HP10-65D,HP1065D,10,3.05,43.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +,,HP1065E,10,3.05,43.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP10 65E,HP1065E,10,3.05,43.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP10-65 E,HP1065E,10,3.05,43.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP10-65E,HP1065E,10,3.05,43.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP10-65E,HP1065E,10,3.05,43.9,https://www.launch3telecom.com/commscopeandrew/hp1065.html +COMMSCOPE,hp10-65e,HP1065E,10,3.05,43.9,https://www.launch3telecom.com/commscopeandrew/hp1065.html +ANDREW,HP10 65E P1A,HP1065EP1A,10,3.05,43.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP10-65E-P1A,HP1065EP1A,10,3.05,43.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP 10-65G,HP1065G,10,3.05,43.9,Comsearch's Frequency Coordination Database +ANDREW,HP10-65G,HP1065G,10,3.05,43.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP10-65G,HP1065G,10,3.05,43.9,https://www.launch3telecom.com/commscopeandrew/hp1065.html +COMMSCOPE,HP10A-59,HP10A59,10,3.05,43.2,Comsearch's Frequency Coordination Database +ANDREW,HP12-59,HP1259,12,3.66,45,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP12-59,HP1259,12,3.66,45,https://objects.eanixter.com/PD353926.PDF +COMMSCOPE,HP12-59D,HP1259D,12,3.66,45,https://objects.eanixter.com/PD353926.PDF +ANDREW,HP12-59E,HP1259E,12,3.66,45,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP12-59F,HP1259F,12,3.66,45,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP12-59F,HP1259F,12,3.66,45,https://objects.eanixter.com/PD353926.PDF +ANDREW,hp12-59f,HP1259F,12,3.66,45,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP-12-59F,HP1259F,12,3.66,45,https://www.datasheets360.com/pdf/-7751896179374140443 +COMMSCOPE,HP12-59W,HP1259W,12,3.66,45,https://objects.eanixter.com/PD353926.PDF +COMMSCOPE,HP12-59WB,HP1259WB,12,3.66,45,https://objects.eanixter.com/PD353926.PDF +ANDREW,HP12-65,HP1265,12,3.66,45.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +Commscope,HP12-65D,HP1265D,12,3.66,45.6,https://objects.eanixter.com/PD353931.PDF +ANDREW,HP12-65E,HP1265E,12,3.66,45.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP12-65E,HP1265E,12,3.66,45.6,https://objects.eanixter.com/PD353931.PDF +COMMSCOPE,HP1265G,HP1265G,12,3.66,45.6,https://objects.eanixter.com/PD353931.PDF +ANDREW,HP12-65G,HP1265G,12,3.66,45.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP12-65G,HP1265G,12,3.66,45.6,https://objects.eanixter.com/PD353931.PDF +ANDREW,HP15-59D,HP1559D,15,4.57,46.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FHP15-59.pdf +COMMSCOPE,HP15-59D,HP1559D,15,4.57,46.4,https://objects.eanixter.com/PD400300.PDF +RADIO WAVES,HP3-6,HP36,3,0.91,33.3,https://www.radiowaves.com/getmedia/70a14957-da0c-43d1-9642-f4def2654c20/HP3-6.aspx +RADIO WAVES,HP3-6.4,HP364,3,0.91,33.4,https://www.radiowaves.com/en/product/hp3-6-4 +ANDREW,HP4-2201,HP42201,4,1.22,,Model does not belong in this band. +ANDREW,HP4-220A,HP4220A,4,1.22,,Model does not belong in this band. +RADIO WAVES,HP4-5.9,HP459,4,1.22,35,https://www.radiowaves.com/getmedia/b38e1116-15bd-44fb-abb0-d78f501eab49/HP4-5.9.aspx +RADIO WAVES,HP4-59,HP459,4,1.22,35,https://www.radiowaves.com/getmedia/b38e1116-15bd-44fb-abb0-d78f501eab49/HP4-5.9.aspx +RADIO WAVES,hp4-59,HP459,4,1.22,35,https://www.radiowaves.com/getmedia/b38e1116-15bd-44fb-abb0-d78f501eab49/HP4-5.9.aspx +"RADIO WAVES, INC",HP4-59 (V-POL ONLY),HP459VPOLONLY,4,1.22,35,https://www.radiowaves.com/getmedia/b38e1116-15bd-44fb-abb0-d78f501eab49/HP4-5.9.aspx +COMMSCOPE,HP4-6,HP46,4,1.22,36.5,https://www.radiowaves.com/getmedia/422b3335-8b4c-4b58-bed7-a7f94772045e/HP4-6.aspx +RADIO WAVES,HP4-6,HP46,4,1.22,36.5,https://www.radiowaves.com/getmedia/422b3335-8b4c-4b58-bed7-a7f94772045e/HP4-6.aspx +RADIO WAVES,HP4-6.4,HP464,4,1.22,35.9,https://www.radiowaves.com/en/product/hp4-6-4 +RADIO WAVES,HP4-64,HP464,4,1.22,35.9,https://www.radiowaves.com/getmedia/adaab50d-559f-4684-8e3d-5a75816185d8/HP4-6.4.aspx +COMMSCOPE,HP4-65B,HP465B,4,1.22,36,https://objects.eanixter.com/PD354008.PDF +COMMSCOPE,HP4-65C,HP465C,4,1.22,36,https://objects.eanixter.com/PD354008.PDF +Mark Antenna,HP-60144W,HP60144W,12,3.66,45.1,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna Products,HP-60A120D LF,HP60A120DLF,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antennas,HP-60A120L,HP60A120L,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,HP-60A120L LF,HP60A120LLF,10,3.05,43.2,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,HP-60A144D LF,HP60A144DLF,12,3.66,45,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,HP-60A72,HP60A72,6,1.83,38.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,HP-60A72 LF,HP60A72LF,6,1.83,38.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,HP-60A72-LF,HP60A72LF,6,1.83,38.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PRODUCTS,HP-60A96,HP60A96,8,2.44,41.6,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNAS,HP-60A96D,HP60A96D,8,2.44,41.6,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,HP-60A96D RF,HP60A96DRF,8,2.44,41.6,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNAS,HP-60A96D* RF,HP60A96DRF,8,2.44,41.6,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PROD,HP-60A96 (LF),HP60A96LF,8,2.44,41.6,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK,HP-60A96 LF,HP60A96LF,8,2.44,41.6,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,HP-60A96 RF,HP60A96RF,8,2.44,41.6,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +ANDREW,HP6-107G,HP6107G,,,,Model does not belong in this band. +Commscope/Andrew,HP657W,HP657W,6,1.83,38.5,https://objects.eanixter.com/PD354043.PDF +ANDREW,HP6-57W,HP657W,6,1.83,38.5,Comsearch's Frequency Coordination Database +COMMSCOPE,HP6-57W,HP657W,6,1.83,38.5,https://objects.eanixter.com/PD354043.PDF +COMMSCOPE,HP6-57WA,HP657WA,6,1.83,38.5,https://objects.eanixter.com/PD354043.PDF +RADIO WAVES,HP6-5.9,HP659,6,1.83,39,https://www.radiowaves.com/getmedia/fbd5837d-c14d-41ce-8225-bb25edab4af3/HP6-5.9.aspx +"RADIO WAVES, INC",HP659,HP659,6,1.83,39,https://www.radiowaves.com/getmedia/fbd5837d-c14d-41ce-8225-bb25edab4af3/HP6-5.9.aspx +ANDREW,HP6-59,HP659,6,1.83,39,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php Using Radio Waves gain because it's higher for this standardModel +COMMSCOPE,HP6-59,HP659,6,1.83,39,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php Using Radio Waves gain because it's higher for this standardModel +RADIO WAVES,HP6-59,HP659,6,1.83,39,https://www.radiowaves.com/getmedia/fbd5837d-c14d-41ce-8225-bb25edab4af3/HP6-5.9.aspx +RFS,HP6-59,HP659,6,1.83,39,https://www.radiowaves.com/getmedia/fbd5837d-c14d-41ce-8225-bb25edab4af3/HP6-5.9.aspx +RADIO WAVES,Hp6-59,HP659,6,1.83,39,https://www.radiowaves.com/getmedia/fbd5837d-c14d-41ce-8225-bb25edab4af3/HP6-5.9.aspx +COMMSCOPE,HP6-59B,HP659B,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6-59D,HP659D,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP6-59E,HP659E,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6 - 59F,HP659F,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP6-59F,HP659F,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP6-59F,HP659F,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6-59G,HP659G,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP6-59G,HP659G,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6 59H,HP659H,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP6-59H,HP659H,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP6-59H,HP659H,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW CORP,HP6-59H RF,HP659HRF,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6 59J,HP659J,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP6-59J,HP659J,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP6-59J,HP659J,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,hp6-59j,HP659J,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6-59K,HP659K,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP6-59K,HP659K,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP6-59k,HP659K,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6--59K,HP659K,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6-59 P1A,HP659P1A,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6-59-P1A,HP659P1A,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6-59-P3A/K,HP659P3AK,6,1.83,38.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6-59W,HP659W,6,1.83,39,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP6-59W,HP659W,6,1.83,39,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6-59WA,HP659WA,6,1.83,39,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP6-59WA,HP659WA,6,1.83,39,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6-59 WB,HP659WB,6,1.83,39,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6-59WB,HP659WB,6,1.83,39,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP6-59WB,HP659WB,6,1.83,39,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP6-59WC,HP659WC,6,1.83,39,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP6-59WC,HP659WC,6,1.83,39,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Mark Antenna,HP-65A96 RF,HP65A96RF,8,2.44,42.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +RADIO WAVES,HP6-6,HP66,6,1.83,39.3,https://www.radiowaves.com/getmedia/ede15878-f905-48f9-b209-87ad2a770b9e/HP6-6.aspx +RADIO WAVES,hp6-6,HP66,6,1.83,39.3,https://www.radiowaves.com/getmedia/ede15878-f905-48f9-b209-87ad2a770b9e/HP6-6.aspx +RADIO WAVES,HP6-64,HP664,6,1.83,39.1,https://www.radiowaves.com/getmedia/54bd53db-9fc3-47dc-a6f9-cd44b76c670c/HP6-6.4.aspx +ANDREW,HP6 65,HP665,6,1.83,39.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP6-65,HP665,6,1.83,39.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP6-65,HP665,6,1.83,39.8,https://objects.eanixter.com/PD354057.PDF +RADIO WAVES,HP6-65,HP665,6,1.83,39.8,Comsearch's Frequency Coordination Database +COMMSCOPE,HP6-65D,HP665D,6,1.83,39.8,https://objects.eanixter.com/PD354057.PDF +ANDREW,HP6 65E,HP665E,6,1.83,39.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP6-65E,HP665E,6,1.83,39.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP6-65E,HP665E,6,1.83,39.8,https://objects.eanixter.com/PD354057.PDF +ANDREW,HP6-65F,HP665F,6,1.83,39.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP6-65F,HP665F,6,1.83,39.8,https://objects.eanixter.com/PD354057.PDF +ANDREW,HP6-65H,HP665H,6,1.83,39.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP6-65H,HP665H,6,1.83,39.8,https://objects.eanixter.com/PD354057.PDF +COMMSCOPE,HP6 65J,HP665J,6,1.83,39.8,https://objects.eanixter.com/PD354057.PDF +COMMSCOPE,HP6?65J,HP665J,6,1.83,39.8,https://objects.eanixter.com/PD354057.PDF +ANDREW,HP6-65J,HP665J,6,1.83,39.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP6-65J,HP665J,6,1.83,39.8,https://objects.eanixter.com/PD354057.PDF +ANDREW,hp6-65j,HP665J,6,1.83,39.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,Hp6-65J,HP665J,6,1.83,39.8,https://objects.eanixter.com/PD354057.PDF +ANDREW,HP6-65K,HP665K,6,1.83,39.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP6-65K,HP665K,6,1.83,39.8,https://objects.eanixter.com/PD354057.PDF +COMMSCOPE,HP6-65L,HP665L,6,1.83,39.8,https://objects.eanixter.com/PD354057.PDF +RADIO WAVES,HP6-69,HP669,6,1.83,39.8,Typo should be HP665. Using that gain. +RADIO WAVES,HP6-6EX,HP66EX,6,1.83,39.8,Typo should be HP665E. Using that gain. +ANDREW,HP8-186,HP8186,8,2.44,30.7,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2F44d80f59%2Fandrew.com%2FHP8-186.pdf +ANDREW,HP8-50F,HP850F,8,2.44,,Indeterminate. Band designator 50 could be several different options. +COMMSCOPE,HP8-56F,HP856F,8,2.44,,Indeterminate. Band designator 56 could be several different options. +Commscope/Andrew,HP857W,HP857W,8,2.44,41.2,https://www.launch3telecom.com/commscopeandrew/hp857w.html +ANDREW,HP8-57W,HP857W,8,2.44,41.2,https://objects.eanixter.com/PD354132.PDF +COMMSCOPE,HP8-57W,HP857W,8,2.44,41.2,https://www.launch3telecom.com/commscopeandrew/hp857w.html +ANDREW,HP8-57-W,HP857W,8,2.44,41.2,https://objects.eanixter.com/PD354132.PDF +,,HP859,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP8-59,HP859,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP8-59,HP859,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +RADIO WAVES,HP8-59,HP859,8,2.44,41.5,https://objects.eanixter.com/PD354136.PDF +ANDREW,HP8-59D,HP859D,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP8-59D,HP859D,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP8-59E,HP859E,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP8-59E,HP859E,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP8-59F,HP859F,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP8-59F,HP859F,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,Hp8-59F,HP859F,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP8-59f,HP859F,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP8-59J,HP859J,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP8-59J,HP859J,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP8-59-P3A,HP859P3A,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP8-59-P3M,HP859P3M,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP8-59W,HP859W,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP8-59W,HP859W,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HP8-59WA,HP859WA,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HP8-59WA,HP859WA,8,2.44,41.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +RADIO WAVES,HP8-6,HP86,8,2.44,42.1,Comsearch's Frequency Coordination Database +ANDREW,HP8-611D,HP8611D,8,2.44,41.3,https://www.datasheets360.com/pdf/3255520392787409515 +COMMSCOPE,HP8-611D,HP8611D,8,2.44,41.3,https://www.datasheets360.com/pdf/3255520392787409515 +ANDREW,HP8-611D 6GHZ,HP8611D6GHZ,8,2.44,41.3,https://www.datasheets360.com/pdf/3255520392787409515 +RADIO WAVES,HP8-64,HP864,8,2.44,42.3,Comsearch's Frequency Coordination Database +ANDREW,HP8-65,HP865,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP8-65,HP865,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP8-6511,HP86511,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP8-6511,HP86511,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP8-65D,HP865D,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP8-65D,HP865D,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP8 65E,HP865E,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP8-65E,HP865E,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP8-65E,HP865E,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,hp8-65e,HP865E,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP8 65E MAIN & HP6 65F DI,HP865EMAINHP665FDI,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP865F,HP865F,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP8-65F,HP865F,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP8-65F,HP865F,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HP8-65-P1A,HP865P1A,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HP8-65-P3M,HP865P3M,8,2.44,42.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +RADIO WAVES,HPD3-6,HPD36,3,0.91,33.1,https://www.radiowaves.com/en/product/hpd3-6 +RADIO WAVES,HPD4-59,HPD459,4,1.22,35.7,https://www.radiowaves.com/en/product/hpd4-5-9 +RADIO WAVES,HPD4-6,HPD46,4,1.22,36.3,https://www.radiowaves.com/getmedia/1e60f342-4fd0-434d-bbd6-26c332d98cd0/HPD4-6.aspx +RADIO WAVES,HPD4-64,HPD464,4,1.22,35.7,https://www.radiowaves.com/getmedia/35b3f4df-dab5-46d2-9e8d-8c4c1fbdb1a3/HPD4-6.4.aspx +RADIO WAVES,HPD6-59,HPD659,6,1.83,38.7,https://www.radiowaves.com/en/product/hpd6-5-9 +RADIO WAVES,HPD6-6,HPD66,6,1.83,39.1,https://www.radiowaves.com/getmedia/3491e115-f6ca-4a33-bdb9-6077b7f797dd/HPD6-6.aspx +RADIO WAVES,HPD6-64,HPD664,6,1.83,38.9,https://www.radiowaves.com/getmedia/4047a681-60b2-4efb-97bc-9f27691d263f/HPD6-6.4.aspx +RADIO WAVES,HPD8-59,HPD859,8,2.44,41.4,Comsearch's Frequency Coordination Database +RADIO WAVES,HPD8-59F,HPD859F,8,2.44,41.4,Comsearch's Frequency Coordination Database +RADIO WAVES,HPD8-6,HPD86,8,2.44,41.9,Comsearch's Frequency Coordination Database +ANDREW,HPS12-59D,HPS1259D,12,3.66,44.8,Typo should be HPX1259D. Using that gain. +ANDREW,HPX10-59,HPX1059,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX10-59,HPX1059,10,3.05,43.1,https://objects.eanixter.com/PD354271.PDF +ANDREW,HPX10-59D,HPX1059D,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX10-59D,HPX1059D,10,3.05,43.1,https://objects.eanixter.com/PD354271.PDF +ANDREW,HPX10-59E,HPX1059E,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX10-59E,HPX1059E,10,3.05,43.1,https://objects.eanixter.com/PD354271.PDF +COMMSCOPE,HPX10-65C,HPX1065C,10,3.05,44,https://objects.eanixter.com/PD354274.PDF +ANDREW,HPX1065D,HPX1065D,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HPX10-65D,HPX1065D,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HPX10-65D,HPX1065D,10,3.05,44,https://objects.eanixter.com/PD354274.PDF +ANDREW,HPX10A-59,HPX10A59,10,3.05,43.1,https://objects.eanixter.com/PD354271.PDF +COMMSCOPE,HPX10A-59,HPX10A59,10,3.05,43.1,Comsearch's Frequency Coordination Database +ANDREW,HPX12-59,HPX1259,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HPX12-59D,HPX1259D,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX12-59E,HPX1259E,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HPX12-59F,HPX1259F,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX12-59F,HPX1259F,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX12-65D,HPX1265D,12,3.66,45.4,https://objects.eanixter.com/PD354358.PDF +ANDREW,HPX15-59E,HPX1559E,15,4.57,46.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX15-59E,HPX1559E,15,4.57,46.4,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HPX6-59,HPX659,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX6-59E,HPX659E,6,1.83,38.8,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HPX6-59F,HPX659F,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX6-59F,HPX659F,6,1.83,38.8,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HPX6-59G,HPX659G,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX6-59G,HPX659G,6,1.83,38.8,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HPX6-59K,HPX659K,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX6-59K,HPX659K,6,1.83,38.8,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HPX6-59-P3A/K,HPX659P3AK,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX6-59-P3A/K,HPX659P3AK,6,1.83,38.8,https://www.datasheets360.com/pdf/8090544319402768422 +COMMSCOPE,HPX6-65D,HPX665D,6,1.83,39.5,https://www.datasheets360.com/pdf/5940162334482066781 +ANDREW,HPX6-65E,HPX665E,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HPX6-65E,HPX665E,6,1.83,39.5,https://www.datasheets360.com/pdf/5940162334482066781 +COMMSCOPE,HPX6 65F,HPX665F,6,1.83,39.5,https://www.datasheets360.com/pdf/5940162334482066781 +ANDREW,HPX6-65F,HPX665F,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HPX6-65F,HPX665F,6,1.83,39.5,https://www.datasheets360.com/pdf/5940162334482066781 +ANDREW,HPX8-59,HPX859,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX8-59,HPX859,8,2.44,41.3,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HPX8-59-8,HPX8598,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HPX8-59D,HPX859D,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX8-59D,HPX859D,8,2.44,41.3,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HPX8 59E,HPX859E,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HPX8?59E,HPX859E,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX8?59E,HPX859E,8,2.44,41.3,https://www.datasheets360.com/pdf/8090544319402768422 +COMMSCOPE,HPX859E,HPX859E,8,2.44,41.3,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HPX8-59E,HPX859E,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HPX8-59E,HPX859E,8,2.44,41.3,https://www.datasheets360.com/pdf/8090544319402768422 +COMMSCOPE,HPX8-65,HPX865,8,2.44,42,https://www.datasheets360.com/pdf/5940162334482066781 +ANDREW,HPX8-65D,HPX865D,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HPX8-65D,HPX865D,8,2.44,42,https://www.datasheets360.com/pdf/5940162334482066781 +ANDREW,HPX8A-59,HPX8A59,8,2.44,41,Comsearch's Frequency Coordination Database +COMMSCOPE,HPX8A-59,HPX8A59,8,2.44,41,Comsearch's Frequency Coordination Database +COMMSCOPE,HS6-6W,HS66W,6,1.83,39.1,Typo should be HX66W. Using that gain +ANDREW,HSX10-59A LF,HSX1059ALF,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HSX10-59A LF,HSX1059ALF,10,3.05,42.9,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HSX10-59A (RF),HSX1059ARF,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HSX10-59A RF,HSX1059ARF,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HSX10-59A RF,HSX1059ARF,10,3.05,42.9,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HSX10-59A-RF,HSX1059ARF,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HSX10-59-P3A LF,HSX1059P3ALF,10,3.05,42.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HSX10-64A RF,HSX1064ARF,10,3.05,43.6,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FHSX8-64A.pdf +ANDREW,HSX12-59A,HSX1259A,12,3.66,44.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HSX12-59A RF,HSX1259ARF,12,3.66,44.7,https://www.datasheets360.com/pdf/8090544319402768422 +COMMSCOPE,HSX4-59A LF,HSX459ALF,4,1.22,34.4,https://objects.eanixter.com/PD354693.PDF +COMMSCOPE,HSX4-59A RF,HSX459ARF,4,1.22,34.4,https://objects.eanixter.com/PD354693.PDF +COMMSCOPE,HSX4-59-P3A/A (LF),HSX459P3AALF,4,1.22,34.4,https://objects.eanixter.com/PD354693.PDF +COMMSCOPE,HSX4-59 RF,HSX459RF,4,1.22,34.4,https://objects.eanixter.com/PD354693.PDF +COMMSCOPE,HSX4-64,HSX464,4,1.22,35,https://objects.eanixter.com/PD354694.PDF +ANDREW,HSX6-59A L,HSX659AL,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HSX6-59A LF,HSX659ALF,6,1.83,38.8,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HSX6-59A (RF),HSX659ARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HSX6-59A RF,HSX659ARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HSX6-59A RF,HSX659ARF,6,1.83,38.8,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HSX6-59B LF,HSX659BLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HSX6-59B LF,HSX659BLF,6,1.83,38.8,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HSX6-59B RF,HSX659BRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HSX6-59B RF,HSX659BRF,6,1.83,38.8,https://www.datasheets360.com/pdf/8090544319402768422 +COMMSCOPE,HSX6-59K,HSX659K,6,1.83,38.8,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HSX6-59-P3A LF,HSX659P3ALF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HSX6-64A LF,HSX664ALF,6,1.83,39.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HSX6-64A LF,HSX664ALF,6,1.83,39.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HSX6-64A R,HSX664AR,6,1.83,39.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HSX664A RF,HSX664ARF,6,1.83,39.6,https://www.datasheets360.com/pdf/5940162334482066781 +ANDREW,HSX6-64A RF,HSX664ARF,6,1.83,39.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HSX6-64A RF,HSX664ARF,6,1.83,39.6,https://www.datasheets360.com/pdf/5940162334482066781 +COMMSCOPE,HSX6-64B LF,HSX664BLF,6,1.83,39.6,https://www.datasheets360.com/pdf/5940162334482066781 +ANDREW,HSX6-64B RF,HSX664BRF,6,1.83,39.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HSX6-64B RF,HSX664BRF,6,1.83,39.6,https://www.datasheets360.com/pdf/5940162334482066781 +ANDREW,HSX6-64-P3A/B RF,HSX664P3ABRF,6,1.83,39.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HSX8-59A,HSX859A,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HSX8-59A LF,HSX859ALF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HSX8-59A LF,HSX859ALF,8,2.44,41.3,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HSX8-59A (RF),HSX859ARF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HSX8-59A RF,HSX859ARF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,HSX8-59A RF,HSX859ARF,8,2.44,41.3,https://www.datasheets360.com/pdf/8090544319402768422 +ANDREW,HSX8-59-P3A LF,HSX859P3ALF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HSX8-59-P3A (RF),HSX859P3ARF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HSX8-59-RF,HSX859RF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,HSX8-64A RF,HSX864ARF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HSX8-64B RF,HSX864BRF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,HSX8-64-RF,HSX864RF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,HX10-6W,HX106W,10,3.05,43.2,https://www.commscope.com/globalassets/digizuite/262649-p360-hx10-6w-6gf-external.pdf +ANDREW,HX6-6W,HX66W,6,1.83,39.1,https://www.commscope.com/globalassets/digizuite/260874-p360-hx6-6w-external.pdf +COMMSCOPE,HX6-6W,HX66W,6,1.83,39.1,https://www.commscope.com/globalassets/digizuite/260874-p360-hx6-6w-external.pdf +COMMSCOPE,HX6-6W-6GR,HX66W6GR,6,1.83,39.1,https://www.commscope.com/globalassets/digizuite/262695-p360-hx6-6w-6gr-external.pdf +ANDREW,HX8-6W,HX86W,8,2.44,41.6,https://www.commscope.com/globalassets/digizuite/260884-p360-hx8-6w-external.pdf +COMMSCOPE,HX8-6W,HX86W,8,2.44,41.6,https://www.commscope.com/globalassets/digizuite/260884-p360-hx8-6w-external.pdf +WESTERN ELECTRIC,KS15676,KS15676,10,3.05,43.2,https://www.telecomarchive.com/docs/bsp-archive/402/402-421-100_I2.pdf +WESTERN ELECTRIC,KS15676 BC,KS15676BC,10,3.05,43.2,https://www.telecomarchive.com/docs/bsp-archive/402/402-421-100_I2.pdf +WESTERN ELECTRIC,KS15676 BD,KS15676BD,10,3.05,43.2,https://www.telecomarchive.com/docs/bsp-archive/402/402-421-100_I2.pdf +WESTERN ELECTRIC,"KS15676, BD",KS15676BD,10,3.05,43.2,https://www.telecomarchive.com/docs/bsp-archive/402/402-421-100_I2.pdf +WESTERN ELECTRIC,"KS15676,BD",KS15676BD,10,3.05,43.2,https://www.telecomarchive.com/docs/bsp-archive/402/402-421-100_I2.pdf +WESTERN ELECTRIC,KS15676BD,KS15676BD,10,3.05,43.2,https://www.telecomarchive.com/docs/bsp-archive/402/402-421-100_I2.pdf +WESTERN ELECTRIC,KS-21972,KS21972,10,3.05,43.5,http://etler.com/docs/bsp-archive/402/402-422-100_I1.pdf +COMMSCOPE,LX6-6W (CAT A),LX66WCATA,6,1.83,38.1,https://www.commscope.com/globalassets/digizuite/277061-p360-lx6-6w-6gr-external.pdf +MARK ANTENA PRODUCTS,MHP-60120W,MHP60120W,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,MHP-60120WD LF,MHP60120WDLF,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,MHP-60A 120D,MHP60A120D,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark,MHP-60A120D,MHP60A120D,10,3.05,43.4,Comsearch's Frequency Coordination Database +Mark,MHP-60A120D (LF),MHP60A120DLF,10,3.05,43.4,Comsearch's Frequency Coordination Database +MARK ANTENNA,MHP60A120D LF,MHP60A120DLF,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,MHP-60A120D LF,MHP60A120DLF,10,3.05,43.4,https://its.ntia.gov/umbraco/surface/download/publication?reportNumber=90-267_ocr.pdf +MARK ANTENNA,MHP-60A120D RF,MHP60A120DRF,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,MHP60A120L-2,MHP60A120L2,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,MHP-60A120L-2,MHP60A120L2,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,MHP-60A120 LF,MHP60A120LF,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=0480ca47c46d7b83326b2d8bd5ddfa66595fab&type=O&term=MHP-60A72DL +Mark Antenna,MHP-60A120 RF,MHP60A120RF,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,MHP-60A120 RF),MHP60A120RF,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENA PRODUCTS,MHP-60A120W(*),MHP60A120W,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,MHP-60A120W RF,MHP60A120WRF,10,3.05,43.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,MHP-60A144D,MHP60A144D,12,3.66,44.8,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,MHP-60A144D RF,MHP60A144DRF,12,3.66,44.8,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,MHP-60A72,MHP60A72,6,1.83,38.8,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK,MHP-60A72D (LF),MHP60A72DLF,6,1.83,38.8,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,MHP-60A72D LF,MHP60A72DLF,6,1.83,38.8,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna Prod,MHP-60A72D (RF),MHP60A72DRF,6,1.83,38.8,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antennas,MHP-60A72D RF,MHP60A72DRF,6,1.83,38.8,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,MHP-60A72 LF,MHP60A72LF,6,1.83,38.8,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PROD,MHP-60A72 RF,MHP60A72RF,6,1.83,38.8,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PROD,MHP-60A96D,MHP60A96D,8,2.44,41.3,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,MHP-60A96D (LF),MHP60A96DLF,8,2.44,41.3,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,MHP-60A96D LF,MHP60A96DLF,8,2.44,41.3,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PROD,MHP-60A96D (RF),MHP60A96DRF,8,2.44,41.3,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,MHP-60A96D RF,MHP60A96DRF,8,2.44,41.3,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PRODUCTS,MHP60A96 LF,MHP60A96LF,8,2.44,41.3,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +,MHP-60A96 LF,MHP60A96LF,8,2.44,41.3,https://www.datasheetarchive.com/pdf/download.php?id=0480ca47c46d7b83326b2d8bd5ddfa66595fab&type=O&term=MHP-60A72DL +Mark Antenna,MHP-60A96 LF,MHP60A96LF,8,2.44,41.3,https://www.datasheetarchive.com/pdf/download.php?id=0480ca47c46d7b83326b2d8bd5ddfa66595fab&type=O&term=MHP-60A72DL +MARK ANTENNA PROD,MHP-60A96 RF,MHP60A96RF,8,2.44,41.3,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna Products,MHP-60WA96,MHP60WA96,8,2.44,41.3,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark RSI,MHP-65A96DL,MHP65A96DL,8,2.44,42.4,Comsearch's Frequency Coordination Database +"Cambium Networks, LTD",N060080L006A,N060080L006A,6,1.83,39.5,https://www.dlmanuals.com/manual/cambium-ptp-800-series/user-manual/173 +"Cambium Networks, LTD",N060082D128A,N060082D128A,3,0.91,33,https://www.ispsupplies.com/core/media/media.nl?id=7521917&c=393682&h=WmzK6TO7Dv_Trk6A4cSGbqKajbuyl2_8aOz1GrrL5w5AxAeo +"Cambium Networks, LTD",N060082D129A,N060082D129A,4,1.22,35,https://www.ispsupplies.com/core/media/media.nl?id=7521920&c=393682&h=2zo9IEM_D7pfnqckKLVyf1xzvAvKL6U1NNzeXoop6HCiEHyy +CAMBIUM NETWORKS,N060082D132A,N060082D132A,6,1.83,39,https://www.ispsupplies.ca/core/media/media.nl?id=5506200&c=393682&h=BctKf_IGanF6Pzw7AhFva8pUq8JkqNdydudyQ6O-85F-yHHV +CAMBIUM NETWORKS,N060082D151A,N060082D151A,3,0.91,33.3,https://www.winncom.com/pdf/Cambium_N060082D151A/Cambium_N060082D151A.PDF +CAMBIUM NETWORKS,N060082D152A,N060082D152A,4,1.22,36.5,https://www.winncom.com/en/products/N060082D152A +"Cambium Networks, LTD",N060082D153A,N060082D153A,4,1.22,39.3,https://www.winncom.com/en/products/N060082D152A +Mark Antennas,P060A72D,P060A72D,6,1.83,38.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +ANDREW,P10-186,P10186,10,3.05,44,Comsearch's Frequency Coordination Database +COMMSCOPE,P10-186,P10186,10,3.05,44,Comsearch's Frequency Coordination Database +ANDREW,P10-186A,P10186A,10,3.05,44,Comsearch's Frequency Coordination Database +ANDREW,P10-59,P1059,10,3.05,43,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +COMMSCOPE,P10-59C,P1059C,10,3.05,43.3,Comsearch's Frequency Coordination Database +ANDREW,P10-59D,P1059D,10,3.05,43.3,Comsearch's Frequency Coordination Database +COMMSCOPE,P10-59D,P1059D,10,3.05,43.3,Comsearch's Frequency Coordination Database +ANDREW,P10-65,P1065,10,3.05,43.9,Comsearch's Frequency Coordination Database +COMMSCOPE,P10-65,P1065,10,3.05,43.9,Comsearch's Frequency Coordination Database +COMMSCOPE,p10-65,P1065,10,3.05,43.9,Comsearch's Frequency Coordination Database +RFS,P10-65A,P1065A,10,3.05,43.9,Comsearch's Frequency Coordination Database +ANDREW,P10-65C,P1065C,10,3.05,43.9,Comsearch's Frequency Coordination Database +COMMSCOPE,P10-65C,P1065C,10,3.05,43.9,Comsearch's Frequency Coordination Database +COMMSCOPE,p10-65c,P1065C,10,3.05,43.9,Comsearch's Frequency Coordination Database +ANDREW,P10-65D,P1065D,10,3.05,43.9,Comsearch's Frequency Coordination Database +COMMSCOPE,P10-65D,P1065D,10,3.05,43.9,Comsearch's Frequency Coordination Database +RFS,P10-65D,P1065D,10,3.05,43.9,Comsearch's Frequency Coordination Database +ANDREW,P12-59E,P1259E,12,3.66,44.6,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +COMMSCOPE,P12-65,P1265,12,3.66,45.8,Comsearch's Frequency Coordination Database +ANDREW,P12-65D,P1265D,12,3.66,45.6,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,P12-65E,P1265E,12,3.66,45.6,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +COMMSCOPE,P12-65E,P1265E,12,3.66,45.6,Comsearch's Frequency Coordination Database +COMMSCOPE,P4-65,P465,4,1.22,36.2,Comsearch's Frequency Coordination Database +COMMSCOPE,P4-65C,P465C,4,1.22,36.3,Comsearch's Frequency Coordination Database +COMMSCOPE,P4-65D,P465D,4,1.22,36.3,Comsearch's Frequency Coordination Database +MARK ANTENNA,P-6072,P6072,6,1.83,38.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,P-60A120D LF,P60A120DLF,10,3.05,43.3,https://www.datasheetarchive.com/pdf/download.php?id=0480ca47c46d7b83326b2d8bd5ddfa66595fab&type=O&term=MHP-60A72DL +MARK ANTENNA PRODS,P-60A120D* LF,P60A120DLF,10,3.05,43.3,https://www.datasheetarchive.com/pdf/download.php?id=0480ca47c46d7b83326b2d8bd5ddfa66595fab&type=O&term=MHP-60A72DL +MARK ANTENNA,P-60A120D RF,P60A120DRF,10,3.05,43.3,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PRODS,P-60A120L,P60A120L,10,3.05,43.3,https://www.datasheetarchive.com/pdf/download.php?id=0480ca47c46d7b83326b2d8bd5ddfa66595fab&type=O&term=MHP-60A72DL +Mark Antenna,P-60A120 LF,P60A120LF,10,3.05,43.3,https://www.datasheetarchive.com/pdf/download.php?id=0480ca47c46d7b83326b2d8bd5ddfa66595fab&type=O&term=MHP-60A72DL +Mark,P-60A144 RF,P60A144RF,12,3.66,45,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antennas,P-60A72D,P60A72D,6,1.83,38.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-60A72D LF,P60A72DLF,6,1.83,38.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNAS,P-60A72D RF,P60A72DRF,6,1.83,38.7,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PROD,P-60A72 (LF),P60A72LF,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PRODUCTS,P-60A72 LF,P60A72LF,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna Prod,P-60A72(LF),P60A72LF,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna Prod,p-60A72(LF),P60A72LF,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PRODUCTS,P60A72 RF,P60A72RF,6,1.83,38.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PRODUCTS,P-60A72 RF,P60A72RF,6,1.83,38.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antennas,P-60A72RF,P60A72RF,6,1.83,38.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-60A96,P60A96,8,2.44,41.5,https://www.datasheetarchive.com/pdf/download.php?id=0480ca47c46d7b83326b2d8bd5ddfa66595fab&type=O&term=MHP-60A72DL +Mark Antenna,P-60A96D,P60A96D,8,2.44,41.5,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,P-60A96 (LF),P60A96LF,8,2.44,41.5,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,P-60A96 LF,P60A96LF,8,2.44,41.5,https://www.datasheetarchive.com/pdf/download.php?id=0480ca47c46d7b83326b2d8bd5ddfa66595fab&type=O&term=MHP-60A72DL +MARK ANTENNA,P-60A96LF,P60A96LF,8,2.44,41.5,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PRODUCTS,P 60A96 RF,P60A96RF,8,2.44,41.5,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna Products,P-60A96 RF,P60A96RF,8,2.44,41.5,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-60A96RF,P60A96RF,8,2.44,41.5,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +ANDREW,P6-25D,P625D,6,1.83,,Model does not belong in this band. +Mark,P-65120W,P65120W,10,3.05,44,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PROD,P 6572,P6572,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,P-6572L,P6572L,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PROD,P 6572W,P6572W,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-6572W,P6572W,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +ANDREW,P6-57W,P657W,6,1.83,38.5,Comsearch's Frequency Coordination Database +COMMSCOPE,P6-57W,P657W,6,1.83,38.5,Comsearch's Frequency Coordination Database +ANDREW,P6-59,P659,6,1.83,38.7,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +Mark Antennas,P-6596W,P6596W,8,2.44,42.4,https://www.datasheetarchive.com/pdf/download.php?id=0480ca47c46d7b83326b2d8bd5ddfa66595fab&type=O&term=MHP-60A72DL +MARK ANTENNA PRODUCT,P-6596WD,P6596WD,8,2.44,42.5,https://its.ntia.gov/umbraco/surface/download/publication?reportNumber=90-267_ocr.pdf +MARK ANTENNA PRODUCT,P-6596-WD,P6596WD,8,2.44,42.5,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +COMMSCOPE,P6-59C,P659C,6,1.83,38.9,Comsearch's Frequency Coordination Database +ANDREW,P6 59D,P659D,6,1.83,38.9,Comsearch's Frequency Coordination Database +ANDREW,P6-59D,P659D,6,1.83,38.9,Comsearch's Frequency Coordination Database +COMMSCOPE,P6-59D,P659D,6,1.83,38.9,Comsearch's Frequency Coordination Database +Mark,P65A120 LF,P65A120LF,10,3.05,44,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-65A120 LF,P65A120LF,10,3.05,44,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,P-65A120LF,P65A120LF,10,3.05,44,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-65A144,P65A144,12,3.66,45.8,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK,P-65A48D,P65A48D,4,1.22,36.3,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNAS,P 65A72,P65A72,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-65A72,P65A72,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-65A72D LF,P65A72DLF,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-65A72D RF,P65A72DRF,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PRODUCT,P-65A72L-2,P65A72L2,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=b5514b47c46d7b83326b2d8bd5ddfa66595fab&type=O&term=P-65A72 +Mark,P- 65A72 LF,P65A72LF,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-65A72 LF,P65A72LF,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PROD,P-65A72LF,P65A72LF,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna Products,P-65A72 (RF),P65A72RF,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PRODUCT,P-65A72 RF,P65A72RF,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=b5514b47c46d7b83326b2d8bd5ddfa66595fab&type=O&term=P-65A72 +Mark Antenna Products,P-65A72(RF),P65A72RF,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,P-65A72RF,P65A72RF,6,1.83,39.9,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PRODUCTS,P 65A96,P65A96,8,2.44,42.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P65A96,P65A96,8,2.44,42.4,Comsearch's Frequency Coordination Database +MARK ANTENNAS,P-65A96D,P65A96D,8,2.44,42.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-65A96D RF,P65A96DRF,8,2.44,42.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-65A96DRF,P65A96DRF,8,2.44,42.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA PROD,P-65A96 L,P65A96L,6,1.83,42.5,https://its.ntia.gov/umbraco/surface/download/publication?reportNumber=90-267_ocr.pdf +MARK ANTENNA PRODUCTS,P65A96L-2,P65A96L2,8,2.44,42.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,P-65A96L-2,P65A96L2,8,2.44,42.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-65A96 LF,P65A96LF,8,2.44,42.4,https://www.datasheetarchive.com/pdf/download.php?id=0480ca47c46d7b83326b2d8bd5ddfa66595fab&type=O&term=MHP-60A72DL +MARK ANTENNA,P-65A96R,P65A96R,8,2.44,42.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antennas,P65A96 RF,P65A96RF,8,2.44,42.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +MARK ANTENNA,P-65A96 RF,P65A96RF,8,2.44,42.4,Comsearch's Frequency Coordination Database +MARK ANTENNA,P65A96RF,P65A96RF,8,2.44,42.4,Comsearch's Frequency Coordination Database +Mark Antenna,P-65A96RF,P65A96RF,8,2.44,42.4,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +Mark Antenna,P-65AD144V MN RF,P65AD144VMNRF,12,3.66,45.8,https://www.datasheetarchive.com/pdf/download.php?id=30859147c46d7b83326b2d8bd5ddfa66595fab&type=O&term=PT-65A96 +ANDREW,P6-65,P665,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +COMMSCOPE,P6-65,P665,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,P6-65A,P665A,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,P6-65C,P665C,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +COMMSCOPE,P6-65C,P665C,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,P6 65D,P665D,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,P6 65d,P665D,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,P6.65D,P665D,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,P6-65D,P665D,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +COMMSCOPE,P6-65D,P665D,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,p6-65d,P665D,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,P6-65E,P665E,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +COMMSCOPE,P6-65E,P665E,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +RFS,P6-65E,P665E,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +COMMSCOPE,p6-65e,P665E,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +COMMSCOPE,P6-65e,P665E,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,P6-65J,P665J,6,1.83,39.5,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,P8-186,P8186,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/P8-186-pdf.php +COMMSCOPE,P8-186,P8186,8,2.44,42,Comsearch's Frequency Coordination Database +ANDREW,P8-186 2GHZ-V,P81862GHZV,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/P8-186-pdf.php +ANDREW,P8-186 2GHZ-V,P81862GHZV,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/P8-186-pdf.php +COMMSCOPE,P8-57W,P857W,8,2.44,41.2,Comsearch's Frequency Coordination Database +COMMSCOPE,P8-59A,P859A,8,2.44,41.5,Comsearch's Frequency Coordination Database +COMMSCOPE,P8-59C,P859C,8,2.44,41.5,Comsearch's Frequency Coordination Database +ANDREW,P8-59D,P859D,8,2.44,41.5,Comsearch's Frequency Coordination Database +COMMSCOPE,P8-59D,P859D,8,2.44,41.5,Comsearch's Frequency Coordination Database +COMMSCOPE,P8?65,P865,8,2.44,42,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,P8-65,P865,8,2.44,42,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +COMMSCOPE,P8-65,P865,8,2.44,42,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,P8-65A,P865A,8,2.44,42,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,P8-65C,P865C,8,2.44,42.3,Comsearch's Frequency Coordination Database +COMMSCOPE,P8-65C,P865C,8,2.44,42.3,Comsearch's Frequency Coordination Database +ANDREW,P8-65D,P865D,8,2.44,42.3,Comsearch's Frequency Coordination Database +COMMSCOPE,P8-65D,P865D,8,2.44,42.3,Comsearch's Frequency Coordination Database +ANDREW,P8-65d,P865D,8,2.44,42.3,Comsearch's Frequency Coordination Database +ANDREW,P8-65-D,P865D,8,2.44,42.3,Comsearch's Frequency Coordination Database +COMMSCOPE,P8-65-D,P865D,8,2.44,42.3,Comsearch's Frequency Coordination Database +ANDREW,P8-65D A64130,P865DA64130,8,2.44,42.3,Comsearch's Frequency Coordination Database +ANDREW,P8-65 or similar,P865ORSIMILAR,8,2.44,42.3,Comsearch's Frequency Coordination Database +RFS/Cablewave,PA1059,PA1059,10,3.05,43.4,Comsearch's Frequency Coordination Database +RFS,PA10-59,PA1059,10,3.05,43.4,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA10-59A,PA1059A,10,3.05,43.4,Comsearch's Frequency Coordination Database +RFS,PA10-59A,PA1059A,10,3.05,43.4,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA10-59AC,PA1059AC,10,3.05,43.4,Comsearch's Frequency Coordination Database +ANDREW,PA10-65,PA1065,10,3.05,44.1,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA10-65,PA1065,10,3.05,44.1,Comsearch's Frequency Coordination Database +RFS,PA10-65,PA1065,10,3.05,44.1,Comsearch's Frequency Coordination Database +RFS,PA10-65A,PA1065A,10,3.05,44.1,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA10-65AC,PA1065AC,10,3.05,44.1,Comsearch's Frequency Coordination Database +ANDREW,PA10-65D,PA1065D,10,3.05,44.1,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA10-65 (FCC S65000),PA1065FCCS65000,10,3.05,44.1,Comsearch's Frequency Coordination Database +RFS,PA10-W57A,PA10W57A,10,3.05,43.5,Comsearch's Frequency Coordination Database +RFS,PA10-W57A (P),PA10W57AP,10,3.05,43.5,Comsearch's Frequency Coordination Database +RFS/Cablewave,PA10W59,PA10W59,10,3.05,43.6,https://www.rfsworld.com/userfiles/pdf/287a.pdf +RFS,PA10-W59A,PA10W59A,10,3.05,43.6,Comsearch's Frequency Coordination Database +RFS/Cablewave,PA1259,PA1259,12,3.66,45.1,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA12-59,PA1259,12,3.66,45.1,Comsearch's Frequency Coordination Database +RFS,PA12-65,PA1265,12,3.66,45.8,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA12-65A,PA1265A,12,3.66,45.8,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA12-65AC,PA1265AC,12,3.66,45.8,Comsearch's Frequency Coordination Database +RFS/Cablewave,PA12W59,PA12W59,12,3.66,45.2,https://www.rfsworld.com/userfiles/pdf/287a.pdf +RFS,PA12-W59A,PA12W59A,12,3.66,45.2,Comsearch's Frequency Coordination Database +RFS,PA4-W57A,PA4W57A,4,1.22,35.5,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA6-54B,PA654B,6,1.83,,Indeterminate. Band designator 54 has multiple options. +RFS/Cablewave,PA659,PA659,6,1.83,39.2,https://www.rfsworld.com/userfiles/pdf/287a.pdf +CABLEWAVE SYSTEMS,PA6-59,PA659,6,1.83,39.2,https://www.rfsworld.com/userfiles/pdf/287a.pdf +RFS,PA6-59A,PA659A,6,1.83,39,Comsearch's Frequency Coordination Database +RFS,PA6-59B,PA659B,6,1.83,39,Comsearch's Frequency Coordination Database +ANDREW,PA6-59W,PA659W,6,1.83,39.2,https://www.rfsworld.com/userfiles/pdf/287a.pdf +CABLEWAVE SYSTEMS,PA6-59W,PA659W,6,1.83,39.2,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA6 65,PA665,6,1.83,39.9,Comsearch's Frequency Coordination Database +ANDREW,PA6-65,PA665,6,1.83,39.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA6-65,PA665,6,1.83,39.9,Comsearch's Frequency Coordination Database +RFS,PA6-65,PA665,6,1.83,39.9,Comsearch's Frequency Coordination Database +ANDREW,PA6-65A,PA665A,6,1.83,39.9,Comsearch's Frequency Coordination Database +RFS,PA6-65A,PA665A,6,1.83,39.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA6-65AC,PA665AC,6,1.83,39.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA6-65AC S63000,PA665ACS63000,6,1.83,39.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA6.65B,PA665B,6,1.83,39.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA6-65B,PA665B,6,1.83,39.9,Comsearch's Frequency Coordination Database +RFS,PA6-65B,PA665B,6,1.83,39.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA6-65B S91650,PA665BS91650,6,1.83,39.9,Comsearch's Frequency Coordination Database +RFS,PA6-W57A,PA6W57A,6,1.83,39,Comsearch's Frequency Coordination Database +RFS,PA6-W57B,PA6W57B,6,1.83,39,Comsearch's Frequency Coordination Database +RFS/Cablewave,PA6W59,PA6W59,6,1.83,39.2,https://www.rfsworld.com/userfiles/pdf/287a.pdf +RFS,PA6-W59A,PA6W59A,6,1.83,39.2,Comsearch's Frequency Coordination Database +RFS,PA6-W59B,PA6W59B,6,1.83,39.2,Comsearch's Frequency Coordination Database +RFS/Cablewave,PA859,PA859,8,2.44,41.7,https://www.rfsworld.com/userfiles/pdf/287a.pdf +CABLEWAVE SYSTEMS,PA8-59,PA859,8,2.44,41.7,https://www.rfsworld.com/userfiles/pdf/287a.pdf +RFS,PA8-59A,PA859A,8,2.44,41.6,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA8-59B,PA859B,8,2.44,41.6,Comsearch's Frequency Coordination Database +RFS,PA8-59B,PA859B,8,2.44,41.6,Comsearch's Frequency Coordination Database +ANDREW,PA8-65,PA865,8,2.44,41.7,https://www.rfsworld.com/userfiles/pdf/287a.pdf +CABLEWAVE SYSTEMS,PA8-65,PA865,8,2.44,41.7,https://www.rfsworld.com/userfiles/pdf/287a.pdf +RFS,PA8-65,PA865,8,2.44,41.7,https://www.rfsworld.com/userfiles/pdf/287a.pdf +RFS,PA8?65A,PA865A,8,2.44,42.4,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA8-65A,PA865A,8,2.44,42.4,Comsearch's Frequency Coordination Database +RFS,PA8-65A,PA865A,8,2.44,42.4,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA8-65AC,PA865AC,8,2.44,42.4,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA8-65B,PA865B,8,2.44,42.4,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PA8-W57A,PA8W57A,8,2.44,41.5,Comsearch's Frequency Coordination Database +RFS,PA8-W57A,PA8W57A,8,2.44,41.5,Comsearch's Frequency Coordination Database +RFS,PA8-W57AC,PA8W57AC,8,2.44,41.5,Comsearch's Frequency Coordination Database +RFS/Cablewave,PA8W59,PA8W59,8,2.44,41.7,https://www.rfsworld.com/userfiles/pdf/287a.pdf +RFS,PA8-W59A,PA8W59A,8,2.44,41.7,Comsearch's Frequency Coordination Database +RFS,PAA8-65A,PAA865A,8,2.44,,Indeterminate. Third character has multiple options. +ANDREW,PAAR8-59W RF,PAAR859WRF,8,2.44,41,https://objects.eanixter.com/PD355687.PDF +COMMSCOPE,PAARX8-59WA,PAARX859WA,8,2.44,40.7,https://objects.eanixter.com/PD376733.PDF +RFS,PAD10-50A,PAD1050A,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/PAD10-59AC +CABLEWAVE SYSTEMS,PAD10-59,PAD1059,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/PAD10-59AC +RFS,PAD10-59,PAD1059,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/PAD10-59AC +RFS,PAD10-59A,PAD1059A,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/PAD10-59AC +CABLEWAVE SYSTEMS,PAD10-59AC,PAD1059AC,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/PAD10-59AC +RFS,PAD10-64A,PAD1064A,10,3.05,43.9,https://www.rfsworld.com/pim/product/html/PAD10-65AC +CABLEWAVE SYSTEMS,PAD10-65,PAD1065,10,3.05,43.9,https://www.rfsworld.com/pim/product/html/PAD10-65AC +RFS,PAD 10-65A,PAD1065A,10,3.05,43.9,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +RFS,PAD10-65A,PAD1065A,10,3.05,43.9,https://www.rfsworld.com/pim/product/html/PAD10-65AC +CABLEWAVE SYSTEMS,PAD10-65AC,PAD1065AC,10,3.05,43.9,https://www.rfsworld.com/pim/product/html/PAD10-65AC +RFS,PAD10-W57A,PAD10W57A,10,3.05,43.5,https://www.rfsworld.com/pim/product/html/PAD10-W57AC +RFS,PAD10-W59A,PAD10W59A,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/PAD10-W59AC +RFS,PAD1-65A,PAD165A,10,3.05,43.9,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +RFS,PAD6-107A,PAD6107A,,,,Model does not belong in this band. +RFS,PAD6-259B,PAD6259B,6,1.83,,Indeterminate. Band designator 259 is not known. +CABLEWAVE SYSTEMS,PAD6-50AC,PAD650AC,6,1.83,,Indeterminate. Band designator 50 has multiple options. +RFS,PAD6-57B,PAD657B,6,1.83,,Indeterminate. Band designator 57 is unknown. +CABLEWAVE SYSTEMS,PAD6-57W,PAD657W,6,1.83,,Indeterminate. Band designator 57 is unknown. +RFS,PAD6-57WA,PAD657WA,6,1.83,,Indeterminate. Band designator 57 is unknown. +CABLEWAVE SYSTEMS,PAD6-59,PAD659,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PAD6-59BC +RFS,PAD6-59,PAD659,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PAD6-59BC +RFS,PAD6?59A,PAD659A,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PAD6-59BC +CABLEWAVE SYSTEMS,PAD6-59A,PAD659A,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PAD6-59BC +RFS,PAD6-59A,PAD659A,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PAD6-59BC +CABLEWAVE SYSTEMS,PAD6-59AC,PAD659AC,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PAD6-59BC +RFS,PAD6-59B,PAD659B,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PAD6-59BC +RFS,PAD-6-59B,PAD659B,6,1.83,38.7,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +RFS,PAD6-59N,PAD659N,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PAD6-59BC +CABLEWAVE SYSTEMS,PAD6-59W,PAD659W,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PAD6-59BC +RFS,PAD6-59WW,PAD659WW,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PAD6-59BC +CABLEWAVE SYSTEMS,PAD6-65,PAD665,6,1.83,39.6,https://www.rfsworld.com/pim/product/html/PAD6-65BC +RFS,PAD6-65,PAD665,6,1.83,39.6,https://www.rfsworld.com/pim/product/html/PAD6-65BC +RFS,PAD665A,PAD665A,6,1.83,39.6,https://www.rfsworld.com/pim/product/html/PAD6-65BC +ANDREW,PAD6-65A,PAD665A,6,1.83,39.6,https://docplayer.net/44808769-Microwave-antenna-systems.html +CABLEWAVE SYSTEMS,PAD6-65A,PAD665A,6,1.83,39.6,Comsearch's Frequency Coordination Database +RFS,PAD6-65A,PAD665A,6,1.83,39.6,https://www.rfsworld.com/pim/product/html/PAD6-65BC +RFS,PAd6-65A,PAD665A,6,1.83,39.6,https://www.rfsworld.com/pim/product/html/PAD6-65BC +CABLEWAVE SYSTEMS,PAD6-65AC,PAD665AC,6,1.83,39.6,https://www.rfsworld.com/pim/product/html/PAD6-65BC +RFS,PAD6-65AC,PAD665AC,6,1.83,39.6,https://www.rfsworld.com/pim/product/html/PAD6-65BC +RFS,PAD6-65AC-1S,PAD665AC1S,6,1.83,39.6,https://www.rfsworld.com/pim/product/html/PAD6-65BC +RFS,PAD6-65B,PAD665B,6,1.83,39.6,https://www.rfsworld.com/pim/product/html/PAD6-65BC +RFS,PAD6-65BC,PAD665BC,6,1.83,39.6,https://www.rfsworld.com/pim/product/html/PAD6-65BC +RFS,PAD6-65D,PAD665D,6,1.83,39.6,https://www.rfsworld.com/pim/product/html/PAD6-65BC +RFS,PAD6-69A,PAD669A,6,1.83,39.6,Typo should be PAD665A. Using that gain. +RFS,PAD6-W57A,PAD6W57A,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PAD6-W57BC +RFS,PAD6W57B,PAD6W57B,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PAD6-W57BC +RFS,PAD6w57B,PAD6W57B,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PAD6-W57BC +RFS,PAD6-W57B,PAD6W57B,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PAD6-W57BC +RFS,PAD6-w57B,PAD6W57B,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PAD6-W57BC +RFS,PAD6-W57G,PAD6W57G,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PAD6-W57BC +RFS,PAD6-W59A,PAD6W59A,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/PAD6-W59BC +ANDREW,PAD6-W59B,PAD6W59B,6,1.83,39.1,https://www.rfsworld.com/userfiles/pdf/6ghz_aws_relocation_kit.pdf +COMMSCOPE,PAD6-W59B,PAD6W59B,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/PAD6-W59BC +RFS,PAD6-W59B,PAD6W59B,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/PAD6-W59BC +RFS,PAD8-57A,PAD857A,8,2.44,,Indeterminate. Band designator 57 is unknown. +RFS,PAD8 57W,PAD857W,8,2.44,41.4,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAD8-57W,PAD857W,8,2.44,41.4,Comsearch's Frequency Coordination Database +RFS,PAD8-58A,PAD858A,8,2.44,,Indeterminate. Band designator 58 is unknown. +CABLEWAVE SYSTEMS,PAD8-59,PAD859,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/PAD8-59AC +RFS,PAD8-59,PAD859,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/PAD8-59AC +RFS,PAD8-59A,PAD859A,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/PAD8-59AC +CABLEWAVE SYSTEMS,PAD8-59AC,PAD859AC,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/PAD8-59AC +RFS,PAD8-59AC,PAD859AC,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/PAD8-59AC +CABLEWAVE SYSTEMS,PAD8-59W,PAD859W,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/PAD8-59AC +CABLEWAVE SYSTEMS,PAD8-59WAC,PAD859WAC,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/PAD8-59AC +RFS,PAD8-59WW,PAD859WW,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/PAD8-59AC +RFS,PAD8-5(A,PAD85A,6,1.83,41.3,https://www.rfsworld.com/pim/product/html/PAD8-59AC +CABLEWAVE SYSTEMS,PAD8-65,PAD865,8,2.44,42.1,https://www.rfsworld.com/pim/product/html/PAD8-65AC +RFS,PAD8.65A,PAD865A,8,2.44,42.1,https://www.rfsworld.com/pim/product/html/PAD8-65AC +CABLEWAVE SYSTEMS,PAD8-65A,PAD865A,8,2.44,42.1,https://www.rfsworld.com/pim/product/html/PAD8-65AC +RFS,PAD8-65A,PAD865A,8,2.44,42.1,https://www.rfsworld.com/pim/product/html/PAD8-65AC +RFS,PAD8-65a,PAD865A,8,2.44,42.1,https://www.rfsworld.com/pim/product/html/PAD8-65AC +RFS,PAD8 65AC,PAD865AC,8,2.44,42.1,https://www.rfsworld.com/pim/product/html/PAD8-65AC +CABLEWAVE SYSTEMS,PAD8-65AC,PAD865AC,8,2.44,42.1,https://www.rfsworld.com/pim/product/html/PAD8-65AC +RFS,PAD8-65AC,PAD865AC,8,2.44,42.1,https://www.rfsworld.com/pim/product/html/PAD8-65AC +RFS,PAD8-U57A,PAD8U57A,8,2.44,41.4,Typo should be PAD8W57A. Using that gain. +RFS,PAD8-W159A,PAD8W159A,8,2.44,41.6,Typo should be PAD8W59A. Using that gain. +RFS,PAD8-W57,PAD8W57,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PAD8-W57AC +RFS,PAD8W57A,PAD8W57A,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PAD8-W57AC +RFS,PAD8-W57A,PAD8W57A,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PAD8-W57AC +RFS,PAD8-W57B,PAD8W57B,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PAD8-W57AC +RFS,PAD8-W59A,PAD8W59A,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/PAD8-W59AC +RFS,PAD8-W59W,PAD8W59W,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/PAD8-W59AC +RFS,PAD^-W59B,PADW59B,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/PAD6-W59BC +RFS,PADX10-59W,PADX1059W,10,3.05,43.4,Comsearch's Frequency Coordination Database +RFS,PADX10-65A,PADX1065A,10,3.05,43.9,Comsearch's Frequency Coordination Database +RFS,PADX10-U57A,PADX10U57A,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/PADX10-U57AC +RFS,PADX10-W57A,PADX10W57A,10,3.05,43.4,Comsearch's Frequency Coordination Database +COMMSCOPE,PADX10--W57A,PADX10W57A,10,3.05,43.4,Comsearch's Frequency Coordination Database +RFS,PADX10-W59A,PADX10W59A,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/PADX10-W59AC +RFS,PADX10-W65A,PADX10W65A,10,3.05,43.9,https://alliancecorporation.ca/images/documents/wireless-infrastructure-documents/RFS/Microwave_antennas/Alliance_distributor_RFS_Microwave_Antenna_PAD10-65AC.pdf +RFS,PADX6-59,PADX659,6,1.83,38.1,https://www.rfsworld.com/pim/product/html/PADX6-59BC1S1R +RFS,PADX6-59A,PADX659A,6,1.83,38.5,https://www.rfsworld.com/pim/product/html/PADX6-59BC1S1R +RFS,PADX6-59B,PADX659B,6,1.83,38.1,https://www.rfsworld.com/pim/product/html/PADX6-59BC1S1R +CABLEWAVE SYSTEMS,PADX6-59WAC,PADX659WAC,6,1.83,38.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PADX6-59WAC 43015C,PADX659WAC43015C,6,1.83,38.9,Comsearch's Frequency Coordination Database +RFS,PADX6-65A,PADX665A,6,1.83,39.4,Comsearch's Frequency Coordination Database +RFS,PADX6-U57A,PADX6U57A,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PADX6-U57AC +RFS,PADX6-W57 A,PADX6W57A,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PADX6-W57AC +RFS,PADX6W57A,PADX6W57A,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PADX6-W57AC +RFS,PADX6-W57A,PADX6W57A,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PADX6-W57AC +RFS,PADX6-W57AC,PADX6W57AC,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/PADX6-W57AC +ANDREW,PADX6-W59,PADX6W59,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PADX6-W59BC +RFS,PADX6-W59A,PADX6W59A,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PADX6-W59BC +RFS,PADX6 W59 B,PADX6W59B,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PADX6-W59BC +RFS,PADX6 W59B,PADX6W59B,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PADX6-W59BC +RFS,PADX6?W59B,PADX6W59B,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PADX6-W59BC +RFS,PADX6W59B,PADX6W59B,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PADX6-W59BC +COMMSCOPE,PADX6-W59B,PADX6W59B,6,1.83,38.9,Comsearch's Frequency Coordination Database +RFS,PADX6-W59B,PADX6W59B,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PADX6-W59BC +RFS,PADX6-W59BC,PADX6W59BC,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/PADX6-W59BC +RFS,PADX8-59A,PADX859A,8,2.44,41.1,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PADX8-59W,PADX859W,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-W59AC +CABLEWAVE SYSTEMS,PADX8-59W 44017C,PADX859W44017C,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-W59AC +CABLEWAVE SYSTEMS,PADX8-59WAC,PADX859WAC,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-W59AC +RFS,PADX8-59WAC,PADX859WAC,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-W59AC +CABLEWAVE SYSTEMS,PADX8-59WAC 44017C,PADX859WAC44017C,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-W59AC +CABLEWAVE SYSTEMS,PADX8-59WAC 44017C,PADX859WAC44017C,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-W59AC +Andrew Corporation,PADX8-59WAC 44017C,PADX859WAC44017C,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-W59AC +ANDREW CORPORATION,PADX8-65,PADX865,8,2.44,39.6,https://www.launch3telecom.com/shared_media/pdf/manufacturers/rfs_6.pdf +RFS,PADX8-65A,PADX865A,8,2.44,41.9,Comsearch's Frequency Coordination Database +RFS,PADX8-U57,PADX8U57,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-U57AC +RFS,PADX8?U57A,PADX8U57A,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-U57AC +COMMSCOPE,PADX8-U57A,PADX8U57A,8,2.44,41.4,Comsearch's Frequency Coordination Database +RFS,PADX8-U57A,PADX8U57A,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-U57AC +RFS,padx8-u57a,PADX8U57A,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-U57AC +RFS,PADX8-W571,PADX8W571,8,2.44,41.2,https://www.rfsworld.com/pim/product/html/PADX8-W57AC +RFS,PADX8?W57A,PADX8W57A,8,2.44,41.2,https://www.rfsworld.com/pim/product/html/PADX8-W57AC +RFS,PADX8-W57 A,PADX8W57A,8,2.44,41.2,https://www.rfsworld.com/pim/product/html/PADX8-W57AC +RFS,PADX8-W57A,PADX8W57A,8,2.44,41.2,https://www.rfsworld.com/pim/product/html/PADX8-W57AC +RFS,PADx8-W57A,PADX8W57A,8,2.44,41.2,https://www.rfsworld.com/pim/product/html/PADX8-W57AC +RFS,PADX8-W59A,PADX8W59A,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-W59AC +,PADx8-W59A,PADX8W59A,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-W59AC +RFS,PADX8-W69A,PADX8W69A,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/PADX8-W59AC +RFS,PADX8-WA57A,PADX8WA57A,8,2.44,41.2,https://www.rfsworld.com/pim/product/html/PADX8-W57AC +RFS,PADZ8-U57A,PADZ8U57A,8,2.44,41.4,Typo should be PADX8U57A. Using that gain +COMMSCOPE,PAE10-59A,PAE1059A,10,3.05,43.4,Typo should be PAL1059A. Using that gain. +CABLEWAVE SYSTEMS,PAL10-59,PAL1059,10,3.05,43.4,Comsearch's Frequency Coordination Database +ANDREW,PAL10-59A,PAL1059A,10,3.05,43.4,https://docplayer.net/44808769-Microwave-antenna-systems.html +CABLEWAVE SYSTEMS,PAL10-59A,PAL1059A,10,3.05,43.4,Comsearch's Frequency Coordination Database +RFS,PAL10-59A,PAL1059A,10,3.05,43.4,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL10-65,PAL1065,10,3.05,44.1,Comsearch's Frequency Coordination Database +RFS,PAL10-65A,PAL1065A,10,3.05,44.1,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL10-65AC,PAL1065AC,10,3.05,44.1,Comsearch's Frequency Coordination Database +RFS,PAL12-65A,PAL1265A,12,3.66,45.8,Comsearch's Frequency Coordination Database +RFS,PAL12-65AC,PAL1265AC,12,3.66,45.8,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL6-59,PAL659,6,1.83,39,Comsearch's Frequency Coordination Database +RFS,PAL6-59,PAL659,6,1.83,39,Comsearch's Frequency Coordination Database +RFS,PAL6-59A,PAL659A,6,1.83,39,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL6-59AC,PAL659AC,6,1.83,39,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL6 65,PAL665,6,1.83,39.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL6-65,PAL665,6,1.83,39.9,Comsearch's Frequency Coordination Database +RFS,PAL6-65,PAL665,6,1.83,39.9,Comsearch's Frequency Coordination Database +RFS,PAL6-65A,PAL665A,6,1.83,39.9,Comsearch's Frequency Coordination Database +RFS,PAL6-65AC,PAL665AC,6,1.83,39.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL6-65B,PAL665B,6,1.83,39.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL6-65B (FCC 91550),PAL665BFCC91550,6,1.83,39.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL6-65B (FCC S91550),PAL665BFCCS91550,6,1.83,39.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL8-17,PAL817,8,2.44,39.9,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL8-59A,PAL859A,8,2.44,41.6,Comsearch's Frequency Coordination Database +RFS,PAL8-59A,PAL859A,8,2.44,41.6,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL8-59AC,PAL859AC,8,2.44,41.6,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL8-59B,PAL859B,8,2.44,41.6,Comsearch's Frequency Coordination Database +RFS,PAL8-59B,PAL859B,8,2.44,41.6,Comsearch's Frequency Coordination Database +RFS,PAL 8-65,PAL865,8,2.44,42.4,https://docplayer.net/44808769-Microwave-antenna-systems.html +CABLEWAVE SYSTEMS,PAL8-65,PAL865,8,2.44,42.4,Comsearch's Frequency Coordination Database +RFS,PAL8-65,PAL865,8,2.44,42.4,Comsearch's Frequency Coordination Database +RFS,PAL8 - 65 A,PAL865A,8,2.44,42.4,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL8 65A,PAL865A,8,2.44,42.4,Comsearch's Frequency Coordination Database +RFS,PAL8-65 A,PAL865A,8,2.44,42.4,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL8-65.A,PAL865A,8,2.44,42.4,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAL8-65A,PAL865A,8,2.44,42.4,Comsearch's Frequency Coordination Database +RFS,PAL8-65A,PAL865A,8,2.44,42.4,Comsearch's Frequency Coordination Database +RFS,PAL8-65a,PAL865A,8,2.44,42.4,https://docplayer.net/44808769-Microwave-antenna-systems.html +CABLEWAVE SYSTEMS,PAL8-65AC,PAL865AC,8,2.44,42.4,https://docplayer.net/44808769-Microwave-antenna-systems.html +RFS,PAL8-65AD,PAL865AD,8,2.44,42.4,https://docplayer.net/44808769-Microwave-antenna-systems.html +ANDREW,PAR 10-59,PAR1059,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR10-59,PAR1059,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR10-59,PAR1059,10,3.05,43.2,Comsearch's Frequency Coordination Database +COMMSCOPE,PAR 10-59A,PAR1059A,10,3.05,43.2,Comsearch's Frequency Coordination Database +ANDREW,PAR10-59A,PAR1059A,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR10-59A,PAR1059A,10,3.05,43.2,Comsearch's Frequency Coordination Database +COMMSCOPE,PAr10-59A,PAR1059A,10,3.05,43.2,Comsearch's Frequency Coordination Database +ANDREW,PAR10-59A RF,PAR1059ARF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR10-59W,PAR1059W,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR10-59W,PAR1059W,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR10-59W (H),PAR1059WH,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR10-59W (H-POL ONLY),PAR1059WHPOLONLY,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR10-59W LF,PAR1059WLF,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR10-59W LF,PAR1059WLF,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR10-59W LW,PAR1059WLW,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR10-59W R,PAR1059WR,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR10-59W RF,PAR1059WRF,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR10-59W RF,PAR1059WRF,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR10-59WRF,PAR1059WRF,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR10-59W RF (V-POL ONLY),PAR1059WRFVPOLONLY,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR10-59W (V)LF,PAR1059WVLF,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR10-59W (V) RF,PAR1059WVRF,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR10-59W (V)RF,PAR1059WVRF,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR10-59W (V)RF,PAR1059WVRF,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope,PAR10-59W (V)RF,PAR1059WVRF,10,3.05,43.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR1065,PAR1065,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR10-65,PAR1065,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +RFS,PAR10-65A,PAR1065A,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR10-65A-RF,PAR1065ARF,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR10-65B RF,PAR1065BRF,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR10-65B RF,PAR1065BRF,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR10-65 LF,PAR1065LF,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR 10-65 RF,PAR1065RF,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR 10-65RF,PAR1065RF,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR10-65 RF,PAR1065RF,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR10-65 RF,PAR1065RF,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR10-65 RF,PAR1065RF,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR10-65RF,PAR1065RF,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR10-65-RF,PAR1065RF,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR10-65-RF,PAR1065RF,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW CORPORATION,PAR12-59,PAR1259,12,3.66,44.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR12-59A,PAR1259A,12,3.66,44.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR12-59A,PAR1259A,12,3.66,44.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR12-59W,PAR1259W,12,3.66,44.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR12-59W,PAR1259W,12,3.66,44.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR12-59-W,PAR1259W,12,3.66,44.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR12-59W RF,PAR1259WRF,12,3.66,44.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR12-59W RF,PAR1259WRF,12,3.66,44.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR12-65,PAR1265,12,3.66,45.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR12-65A,PAR1265A,12,3.66,45.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR12-65A (FCC 52430A),PAR1265AFCC52430A,12,3.66,45.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR12-65A RF,PAR1265ARF,12,3.66,45.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR12-65A RF,PAR1265ARF,12,3.66,45.3,Comsearch's Frequency Coordination Database +ANDREW,PAR6059,PAR6059,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6065A,PAR6065A,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6065B RF,PAR6065BRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-54B RF,PAR654BRF,6,1.83,,Indeterminate. Band designator 54 has multiple options. +ANDREW,PAR6-56A RF,PAR656ARF,6,1.83,,Indeterminate. Band designator 56 unknown. +COMMSCOPE,PAR6-58B,PAR658B,6,1.83,,Indeterminate. Band designator 58 unknown. +ANDREW,PAR6-58W RF,PAR658WRF,6,1.83,,Indeterminate. Band designator 58 unknown. +ANDREW,PAR 6 59,PAR659,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR 6-59,PAR659,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6 59,PAR659,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59,PAR659,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +CABLEWAVE SYSTEMS,PAR6-59,PAR659,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59,PAR659,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59A,PAR659A,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59A,PAR659A,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59a,PAR659A,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59A`,PAR659A,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6 59A RF,PAR659ARF,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59A RF,PAR659ARF,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59A rf,PAR659ARF,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59AWA (V)RF,PAR659AWAVRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6?59B,PAR659B,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR659B,PAR659B,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59B,PAR659B,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59B,PAR659B,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAr6-59B,PAR659B,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59D,PAR659D,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59E,PAR659E,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59 PXA/B,PAR659PXAB,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59QA (V)RF,PAR659QAVRF,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59Q RF,PAR659QRF,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59 RF,PAR659RF,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6 59 W,PAR659W,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6 59W,PAR659W,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59W,PAR659W,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59W,PAR659W,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,par6-59w,PAR659W,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6 59WA,PAR659WA,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR659WA,PAR659WA,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59WA,PAR659WA,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA,PAR659WA,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA (H),PAR659WAH,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA (H),PAR659WAH,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59WA LF,PAR659WALF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59 WA RF,PAR659WARF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59WA RF,PAR659WARF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA RF,PAR659WARF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA RF,PAR659WARF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59Wa RF,PAR659WARF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59WARF,PAR659WARF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WARF,PAR659WARF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA RF16.7,PAR659WARF167,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WARF (V-POL ONLY),PAR659WARFVPOLONLY,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope,PAR6-59WARF (V-POL ONLY),PAR659WARFVPOLONLY,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA RT,PAR659WART,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA (V) LF,PAR659WAVLF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA (V)LF,PAR659WAVLF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA (V) RF,PAR659WAVRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR659WA (V)RF,PAR659WAVRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA (V)RF,PAR659WAVRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA (V)RF,PAR659WAVRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope,PAR6-59WA (V)RF,PAR659WAVRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA(V) RF,PAR659WAVRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59WA(V)RF,PAR659WAVRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6- 59W LF,PAR659WLF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59W LF,PAR659WLF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59W LF,PAR659WLF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59W LF,PAR659WLF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59W R,PAR659WR,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR 6-59W RF,PAR659WRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59W RF,PAR659WRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59W RF,PAR659WRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59W RF,PAR659WRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59W RF,PAR659WRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,Par6-59W RF,PAR659WRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59-W RF,PAR659WRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59WRF,PAR659WRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR6-59W-RF,PAR659WRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR6-59W-RF,PAR659WRF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR^-65A,PAR65A,,,,No diameter available in the model so cannot discern proper gain +ANDREW,PAR-65A,PAR65A,,,,No diameter available in the model so cannot discern proper gain +ANDREW,PAR*-65A RF,PAR65ARF,,,,No diameter available in the model so cannot discern proper gain +ANDREW,PAR-65A RF,PAR65ARF,,,,No diameter available in the model so cannot discern proper gain +ANDREW,PAR-65B RF,PAR65BRF,,,,No diameter available in the model so cannot discern proper gain +ANDREW,PAR-65D,PAR65D,,,,No diameter available in the model so cannot discern proper gain +ANDREW,PAR-65 RF,PAR65RF,,,,No diameter available in the model so cannot discern proper gain +,,PAR665,6,1.83,38.8,https://www.talleycom.com/images/pdf/ANDPAR6-65-PXA.pdf +ANDREW,PAR 6-65,PAR665,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6/65,PAR665,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65,PAR665,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65`,PAR665,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6&6-59W,PAR6659W,6,1.83,38.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR 6-65A,PAR665A,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6 65A,PAR665A,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6.65A,PAR665A,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65A,PAR665A,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65A,PAR665A,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,par6-65a,PAR665A,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65A L,PAR665AL,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6 65A LF,PAR665ALF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65 A LF,PAR665ALF,6,1.83,38.8,Comsearch's Frequency Coordination Database +ANDREW,PAR6-65A LF,PAR665ALF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65A LF,PAR665ALF,6,1.83,38.8,Comsearch's Frequency Coordination Database +ANDREW,PAR6-65A R,PAR665AR,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6 65A RF,PAR665ARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6- 65A RF,PAR665ARF,6,1.83,38.8,Comsearch's Frequency Coordination Database +ANDREW,PAR6-65A (RF),PAR665ARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65A RF,PAR665ARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65A RF,PAR665ARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65A RF,PAR665ARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65A RF,PAR665ARF,6,1.83,38.8,Comsearch's Frequency Coordination Database +COMMSCOPE,PAR6-65A RF,PAR665ARF,6,1.83,38.8,Comsearch's Frequency Coordination Database +ANDREW,PAr6-65a rf,PAR665ARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65A rf,PAR665ARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65A Rf,PAR665ARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65A(RF),PAR665ARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65A(RF),PAR665ARF,6,1.83,38.8,Comsearch's Frequency Coordination Database +ANDREW,PAR6-65A-RF,PAR665ARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65A RF6,PAR665ARF6,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65A RF6.1,PAR665ARF61,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65A RFwqik561,PAR665ARFWQIK561,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR665B,PAR665B,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65B,PAR665B,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65B,PAR665B,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65B LF,PAR665BLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65B LF,PAR665BLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6 65B RF,PAR665BRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6 65B RF,PAR665BRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65B RF,PAR665BRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65B RF,PAR665BRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAr6-65B RF,PAR665BRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,Par6-65B RF,PAR665BRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65B-RF,PAR665BRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65B-RF,PAR665BRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65B RG,PAR665BRG,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65F RF,PAR665FRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65 LF,PAR665LF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65 LF,PAR665LF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65-PXA,PAR665PXA,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65-PXA/B,PAR665PXAB,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65-PXA/B,PAR665PXAB,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65 (R),PAR665R,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65 R,PAR665R,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6 65 RF,PAR665RF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65 RF,PAR665RF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65 RF,PAR665RF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR6-65 RF,PAR665RF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +Commscope,PAR-6-65 RF,PAR665RF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65RF,PAR665RF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-65W RF,PAR665WRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR6-69,PAR669,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +RFS,PAR6-W57B,PAR6W57B,6,1.83,38.9,Typo should be PAD6W57B. Using that gain. +ANDREW,PAR8065B LF,PAR8065BLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-50A,PAR850A,8,2.44,,Indeterminate. Band designator 50 has multiple options. +COMMSCOPE,PAR8-50W (H),PAR850WH,8,2.44,,Indeterminate. Band designator 50 has multiple options. +ANDREW,PAR8-58A,PAR858A,8,2.44,,Indeterminate. Band designator 58 has multiple options. +ANDREW,PAR8-58W,PAR858W,8,2.44,,Indeterminate. Band designator 58 has multiple options. +ANDREW,PAR8-58W RF,PAR858WRF,8,2.44,,Indeterminate. Band designator 58 has multiple options. +ANDREW,PAR8 59,PAR859,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-59,PAR859,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59,PAR859,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8=59A,PAR859A,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-59A,PAR859A,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59A,PAR859A,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +RFS,PAR8-59A,PAR859A,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,Par8-59A,PAR859A,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8--59A,PAR859A,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8 59A RF,PAR859ARF,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-59A RF,PAR859ARF,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59A RF,PAR859ARF,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,par8-59a rf,PAR859ARF,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-59B,PAR859B,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59B,PAR859B,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59B RF,PAR859BRF,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59-P7A,PAR859P7A,8,2.44,40.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-59W,PAR859W,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W,PAR859W,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-59WA,PAR859WA,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59WA LF,PAR859WALF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59WA RF,PAR859WARF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W (H),PAR859WH,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W H,PAR859WH,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W (H POL ONLY),PAR859WHPOLONLY,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W (H Pol Only),PAR859WHPOLONLY,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W (H-POL ONLY),PAR859WHPOLONLY,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-59 W LF,PAR859WLF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-59W LF,PAR859WLF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-59W LF,PAR859WLF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W LF,PAR859WLF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-59W RF,PAR859WRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-59W RF,PAR859WRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W RF,PAR859WRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W RF,PAR859WRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW CORPORATION,PAR8-59W RF,PAR859WRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,par8-59w rf,PAR859WRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59-W RF,PAR859WRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-59W-RF,PAR859WRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W-RF,PAR859WRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W RF (V-POL ONLY),PAR859WRFVPOLONLY,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-59W RP,PAR859WRP,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W (V) LF,PAR859WVLF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W (V)LF,PAR859WVLF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W (V) RF,PAR859WVRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W (V) RF,PAR859WVRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR859W (V)RF,PAR859WVRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W (V)RF,PAR859WVRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W (V)RF,PAR859WVRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PAR8-59W V(RF),PAR859WVRF,8,2.44,41,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PAR8-65,PAR865,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65,PAR865,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8 65A,PAR865A,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65A,PAR865A,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +CABLEWAVE SYSTEMS,PAR8-65A,PAR865A,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65A,PAR865A,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +RFS,PAR8-65A,PAR865A,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65A&B,PAR865AB,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65A LF,PAR865ALF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65A LF,PAR865ALF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8 65A RF,PAR865ARF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65A RF,PAR865ARF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65A RF,PAR865ARF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65A RF,PAR865ARF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65A-RF,PAR865ARF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65 B,PAR865B,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65B,PAR865B,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65B,PAR865B,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65 B LF,PAR865BLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65B LF,PAR865BLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65B LF,PAR865BLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65B LF,PAR865BLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65B LF,PAR865BLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +Andrew Corporation,PAR8-65B LF,PAR865BLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65B R,PAR865BR,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65B-R,PAR865BR,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8 65B RF,PAR865BRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8 -65B RF,PAR865BRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65 B RF,PAR865BRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65B RF,PAR865BRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65B RF,PAR865BRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65B RF,PAR865BRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65B RF,PAR865BRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +Andrew Corporation,PAR8-65B RF,PAR865BRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65B rf,PAR865BRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65B- RF,PAR865BRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR-8-65B RF,PAR865BRF,6,1.83,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65B RF`,PAR865BRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65B-RF,PAR865BRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65B-RF,PAR865BRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65D,PAR865D,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65LF,PAR865LF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65-PXA,PAR865PXA,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-65 RF,PAR865RF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PAR8-65 RF,PAR865RF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PAR8-69W RF,PAR869WRF,8,2.44,,Indeterminate. Band designator 69 has multiple options +ANDREW,PAR8X-59W,PAR8X59W,8,2.44,40.7,Typo should be PARX8-59W +ANDREW,PARA6-65A,PARA665A,6,1.83,,Indeterminate. Prefix could be PAR6 or PARX6. +ANDREW,PARX10-59,PARX1059,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX10-59,PARX1059,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX10-59A,PARX1059A,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX10-59-P7A,PARX1059P7A,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX10-59W,PARX1059W,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX10-59W,PARX1059W,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX10-59WA,PARX1059WA,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX10-59WAQ,PARX1059WAQ,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX10-59W / PARX8-59W,PARX1059WPARX859W,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX10-59W RF,PARX1059WRF,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX10-65,PARX1065,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PARX10-65,PARX1065,10,3.05,43.6,Comsearch's Frequency Coordination Database +ANDREW,PARX10-65 RF,PARX1065RF,10,3.05,43.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PARX12-59,PARX1259,12,3.66,44.7,Comsearch's Frequency Coordination Database +ANDREW,PARX12-59-P7M,PARX1259P7M,12,3.66,44.7,https://objects.eanixter.com/PD355695.PDF +COMMSCOPE,PARX12-65,PARX1265,12,3.66,45.5,Comsearch's Frequency Coordination Database +COMMSCOPE,PARX6-51A,PARX651A,6,1.83,,Indeterminate. Band designator 51 unknown. +ANDREW,PARX6-59,PARX659,6,1.83,37.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX6-59,PARX659,6,1.83,37.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARx6-59,PARX659,6,1.83,37.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX6?59A,PARX659A,6,1.83,37.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX6?59A,PARX659A,6,1.83,37.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX659A,PARX659A,6,1.83,37.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX6-59A,PARX659A,6,1.83,37.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX6-59A,PARX659A,6,1.83,37.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX6-59a,PARX659A,6,1.83,37.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX6-59-A,PARX659A,6,1.83,37.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +RFS,PARX6-59-PXA,PARX659PXA,6,1.83,37.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX6-59-PXA/A,PARX659PXAA,6,1.83,37.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX6-59-PXA/A,PARX659PXAA,6,1.83,37.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX6-59W,PARX659W,6,1.83,38.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX6-59W,PARX659W,6,1.83,38.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX6-59w,PARX659W,6,1.83,38.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARx6-59W,PARX659W,6,1.83,38.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX6-59W A,PARX659WA,6,1.83,38.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX659WA,PARX659WA,6,1.83,38.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX6-59WA,PARX659WA,6,1.83,38.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX6-59WA,PARX659WA,6,1.83,38.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX659WB,PARX659WB,6,1.83,38.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX6-59WB,PARX659WB,6,1.83,38.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX6-59WB,PARX659WB,6,1.83,38.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX6-59W RF,PARX659WRF,6,1.83,38.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX6-65,PARX665,6,1.83,38.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PARX6-65,PARX665,6,1.83,38.4,Comsearch's Frequency Coordination Database +ANDREW,parx6-65,PARX665,6,1.83,38.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,Parx6-65,PARX665,6,1.83,38.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PARX6-65A,PARX665A,6,1.83,38.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,PARX6-95A,PARX695A,6,1.83,38.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PARX8,PARX8,8,2.44,,No band indicated in model so cannot determine gain. +ANDREW,PARX8-59,PARX859,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX8-59,PARX859,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX8-59A,PARX859A,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX8-59-PXA,PARX859PXA,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX8-59-PXA,PARX859PXA,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX8?59W,PARX859W,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX859W,PARX859W,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX8-59W,PARX859W,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX8-59W,PARX859W,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX8 59WA,PARX859WA,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX8-59 WA,PARX859WA,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX859WA,PARX859WA,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX8-59WA,PARX859WA,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX8-59WA,PARX859WA,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PARX8-59WR,PARX859WR,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX8-59W RF,PARX859WRF,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX8-59W-RF,PARX859WRF,8,2.44,40.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PARX8-65,PARX865,8,2.44,41.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PARX8-65,PARX865,8,2.44,41.2,Comsearch's Frequency Coordination Database +ANDREW,Parx8-65,PARX865,8,2.44,41.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +Andrew Corp,PARX8-65-PXA,PARX865PXA,8,2.44,41.2,https://objects.eanixter.com/PD355711.PDF +CABLEWAVE SYSTEMS,PAS8-59,PAS859,8,2.44,41.3,Typo should be PAD859. Using that gain. +RFS,PAX10-59A,PAX1059A,10,3.05,43.2,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAX10-59A S94100,PAX1059AS94100,10,3.05,43.2,Comsearch's Frequency Coordination Database +RFS,PAX10-65A,PAX1065A,10,3.05,43.9,Comsearch's Frequency Coordination Database +RFS,PAX12-65,PAX1265,12,3.66,45.6,Comsearch's Frequency Coordination Database +ANDREW,PAX6-59,PAX659,6,1.83,38.8,Comsearch's Frequency Coordination Database +RFS,PAX6-59,PAX659,6,1.83,38.8,Comsearch's Frequency Coordination Database +RFS,PAX6-65A,PAX665A,6,1.83,39.7,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAX8-59,PAX859,8,2.44,41.3,Comsearch's Frequency Coordination Database +RFS,PAX8-59A,PAX859A,8,2.44,41.3,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAX8-59 S91400,PAX859S91400,8,2.44,41.3,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAX8-59 S93100,PAX859S93100,8,2.44,41.3,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAX8-65,PAX865,8,2.44,42.2,Comsearch's Frequency Coordination Database +RFS,PAX8-65,PAX865,8,2.44,42.2,Comsearch's Frequency Coordination Database +RFS,PAX8-65A,PAX865A,8,2.44,42.2,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,PAX8-65B,PAX865B,8,2.44,42.2,Comsearch's Frequency Coordination Database +RFS,PAX8-W59A,PAX8W59A,8,2.44,41.7,Comsearch's Frequency Coordination Database +RFS,PD6-W59B,PD6W59B,6,1.83,39.1,Typo should be PAD6W59B. Using that gain +CABLEWAVE SYSTEMS,PD8-59AC,PD859AC,8,2.44,41.3,Typo should be PAD859AC. Using that gain +ANDREW,PDH8-65 MAIN,PDH865MAIN,8,2.44,42.4,https://www.datasheets360.com/pdf/-4500121692955819986 +ANDREW,PDH8-65 main,PDH865MAIN,8,2.44,42.4,https://www.datasheets360.com/pdf/-4500121692955819986 +RFS,PDX8-W57A,PDX8W57A,8,2.44,41.4,Should be PADX8W57A. Gain from Comsearch frequency coordination database. +Commscope/Andrew,PL1057W,PL1057W,10,3.05,42.9,https://objects.eanixter.com/PD355726.PDF +ANDREW,PL10-57W,PL1057W,10,3.05,42.9,https://objects.eanixter.com/PD355726.PDF +Commscope/Andrew,PL1059,PL1059,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL10-59,PL1059,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL10-59,PL1059,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,PL10-59C,PL1059C,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL10-59D,PL1059D,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL10-59D,PL1059D,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL10-59E,PL1059E,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL10-59E,PL1059E,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL10-59F,PL1059F,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL10-59W,PL1059W,10,3.05,43.6,https://www.jmu.edu/wmra-eng/archive/AndrewCatalog38.pdf +COMMSCOPE,PL10-59W,PL1059W,10,3.05,43.6,https://www.jmu.edu/wmra-eng/archive/AndrewCatalog38.pdf +ANDREW,pl10-59w,PL1059W,10,3.05,43.6,https://www.jmu.edu/wmra-eng/archive/AndrewCatalog38.pdf +ANDREW,PL10-59WA,PL1059WA,10,3.05,43.6,https://www.jmu.edu/wmra-eng/archive/AndrewCatalog38.pdf +COMMSCOPE,PL10-59WA,PL1059WA,10,3.05,43.6,https://www.jmu.edu/wmra-eng/archive/AndrewCatalog38.pdf +Commscope/Andrew,PL1065,PL1065,10,3.05,43.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL10-65,PL1065,10,3.05,43.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +COMMSCOPE,PL10-65,PL1065,10,3.05,43.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +RFS,PL10-65A,PL1065A,10,3.05,43.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL10-65C,PL1065C,10,3.05,43.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL10-65D,PL1065D,10,3.05,43.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +COMMSCOPE,PL10-65D,PL1065D,10,3.05,43.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +COMMSCOPE,Pl10-65D,PL1065D,10,3.05,43.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL10-65E,PL1065E,10,3.05,43.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +COMMSCOPE,PL10-65E,PL1065E,10,3.05,43.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +Commscope/Andrew,PL1259,PL1259,12,3.66,45,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL12-59,PL1259,12,3.66,45,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL12-59D,PL1259D,12,3.66,45,Comsearch's Frequency Coordination Database +ANDREW,PL12-59E,PL1259E,12,3.66,45,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL12-59E,PL1259E,12,3.66,45,Comsearch's Frequency Coordination Database +COMMSCOPE,PL12-59F,PL1259F,12,3.66,45,Comsearch's Frequency Coordination Database +ANDREW,PL12-59WB,PL1259WB,12,3.66,45,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope/Andrew,PL1265,PL1265,12,3.66,45.6,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL12-65,PL1265,12,3.66,45.6,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL12-65D,PL1265D,12,3.66,45.6,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +COMMSCOPE,PL12-65D,PL1265D,12,3.66,45.6,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL12-65E,PL1265E,12,3.66,45.6,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +COMMSCOPE,PL12-65E,PL1265E,12,3.66,45.6,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL12-65F,PL1265F,12,3.66,45.6,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +Commscope/Andrew,PL1559,PL1559,15,4.57,46.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL15-59D,PL1559D,15,4.57,46.4,Comsearch's Frequency Coordination Database +Commscope/Andrew,PL1565,PL1565,15,4.57,47.4,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL15-65D,PL1565D,15,4.57,47.4,https://objects.eanixter.com/PD400419.PDF +ANDREW,PL4-107E,PL4107E,,,,Model does not belong in this band. +COMMSCOPE,PL4-65D,PL465D,4,1.22,36.3,Comsearch's Frequency Coordination Database +ANDREW,PL*-57W,PL57W,8,2.44,41.2,https://objects.eanixter.com/PD355887.PDF +ANDREW,PL6-107E,PL6107E,,,,Model does not belong in this band. +Commscope/Andrew,PL657W,PL657W,6,1.83,38.5,https://objects.eanixter.com/PD355844.PDF +ANDREW,PL6-57W,PL657W,6,1.83,38.5,https://objects.eanixter.com/PD355844.PDF +COMMSCOPE,PL6-57W,PL657W,6,1.83,38.5,Comsearch's Frequency Coordination Database +ANDREW,PL6-57WA,PL657WA,6,1.83,38.5,https://objects.eanixter.com/PD355844.PDF +COMMSCOPE,PL6-57WA,PL657WA,6,1.83,38.5,Comsearch's Frequency Coordination Database +COMMSCOPE,PL6-58WB,PL658WB,6,1.83,39.5,Typo should be PL659WB. Using that gain +Commscope/Andrew,PL659,PL659,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL6-59,PL659,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL6-59,PL659,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL6-59A,PL659A,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL6-59C,PL659C,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL6-59C,PL659C,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope,PL659D,PL659D,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL6-59D,PL659D,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL6-59D,PL659D,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL6-59E,PL659E,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL6-59E,PL659E,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope,PL659F,PL659F,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL6-59F,PL659F,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL6-59F,PL659F,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL6-59 PXA,PL659PXA,6,1.83,39.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL6-59W,PL659W,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL6-59W,PL659W,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL6-59WB,PL659WB,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL6-59WB,PL659WB,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL6-59WC,PL659WC,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL6-59WC,PL659WC,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL6-59W-PXA,PL659WPXA,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL65-F,PL65F,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +Commscope/Andrew,PL665,PL665,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL6-65,PL665,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +COMMSCOPE,PL6-65,PL665,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL6-65C,PL665C,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL6-65C (FCC A95100),PL665CFCCA95100,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL6 65D,PL665D,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL6-65D,PL665D,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +COMMSCOPE,PL6-65D,PL665D,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL6-65DA63170,PL665DA63170,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL6-65da63170,PL665DA63170,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL6-65E,PL665E,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +COMMSCOPE,PL6-65E,PL665E,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL6-65F,PL665F,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +COMMSCOPE,PL6-65F,PL665F,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL6-65G,PL665G,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +COMMSCOPE,PL6-65G,PL665G,6,1.83,39.9,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL8-186C V,PL8186CV,8,2.44,42,Comsearch's Frequency Coordination Database +COMMSCOPE,PL8-186C V,PL8186CV,8,2.44,42,Comsearch's Frequency Coordination Database +Andrew,PL8-56D,PL856D,8,2.44,42.3,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +ANDREW,PL857W,PL857W,8,2.44,41.2,https://objects.eanixter.com/PD355886.PDF +Commscope/Andrew,PL857W,PL857W,8,2.44,41.2,https://objects.eanixter.com/PD355886.PDF +ANDREW,PL8-57W,PL857W,8,2.44,41.2,https://objects.eanixter.com/PD355886.PDF +COMMSCOPE,PL8-57W,PL857W,8,2.44,41.2,Comsearch's Frequency Coordination Database +ANDREW,PL8 59,PL859,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope/Andrew,PL859,PL859,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL8-59,PL859,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL8-59,PL859,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL8-59A,PL859A,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL8-59C,PL859C,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL8-59C,PL859C,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope,PL859D,PL859D,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL8-59D,PL859D,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL8-59D,PL859D,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL8-59E,PL859E,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL8-59E,PL859E,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL8-59.F,PL859F,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL8-59F,PL859F,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL8-59F,PL859F,8,2.44,41.6,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL8 59W,PL859W,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL8-59W,PL859W,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL8-59W,PL859W,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL8-59WB,PL859WB,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PL8-59WB,PL859WB,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL8-59wb,PL859WB,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PL8-59w rf,PL859WRF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope/Andrew,PL865,PL865,8,2.44,42.4,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PL8-65,PL865,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +COMMSCOPE,PL8-65,PL865,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +ANDREW,PL8-65C,PL865C,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +COMMSCOPE,PL8-65C,PL865C,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +ANDREW,PL8 - 65D,PL865D,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +ANDREW,PL8 65D,PL865D,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +ANDREW,PL8-65D,PL865D,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +COMMSCOPE,PL8-65D,PL865D,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +ANDREW,PL8-65d,PL865D,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +COMMSCOPE,Pl8-65D,PL865D,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +ANDREW,PL8-65-D,PL865D,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +COMMSCOPE,PL8-65.F,PL865F,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +ANDREW,PL8-65F,PL865F,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +COMMSCOPE,PL8-65F,PL865F,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +COMMSCOPE,Pl8-65F,PL865F,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +ANDREW,PL8-65G,PL865G,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +ANDREW,PL8-65WB,PL865WB,8,2.44,42.4,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FPL8-65D.pdf +ANDREW,PL8-69F,PL869F,8,2.44,,Indeterminate. Band designator 69 has multiple options +ANDREW,PLX8-59D,PLX859D,8,2.44,41.3,Typo should be PXL859D. Using that gain. +ANDREW,PLX8-65D,PLX865D,8,2.44,42,Typo should be PXL865D. Using that gain. +RFS,PPAD6-65B,PPAD665B,6,1.83,39.6,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +RFS,PS8-65A,PS865A,8,2.44,42.4,Typo should be PA865A. Using that gain +COMMSCOPE,PX10-59C,PX1059C,10,3.05,43.1,Comsearch's Frequency Coordination Database +ANDREW,PX10-59D,PX1059D,10,3.05,42.9,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,PX6-59,PX659,6,1.83,38.6,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +COMMSCOPE,PX6-59,PX659,6,1.83,38.6,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,PX6-59C,PX659C,6,1.83,38.6,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,PX6-59E,PX659E,6,1.83,38.6,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +Andrew Corporation,PX8-59,PX859,8,2.44,41,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +COMMSCOPE,PX8-59C,PX859C,8,2.44,41.2,Comsearch's Frequency Coordination Database +ANDREW,PX8-59D,PX859D,8,2.44,41,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +ANDREW,PX8-65D,PX865D,8,2.44,41.9,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +COMMSCOPE,PX8-65D,PX865D,8,2.44,41.9,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-26-1969.pdf +RFS,PXA8-65,PXA865,8,2.44,42.2,Typo should be PAX865. Using that Gain. +Commscope/Andrew,PXL1059,PXL1059,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PXL10-59,PXL1059,10,3.05,43.1,Comsearch's Frequency Coordination Database +Andrew Corporation,PXL10-59C,PXL1059C,10,3.05,43.1,Comsearch's Frequency Coordination Database +ANDREW,PXL10-59D,PXL1059D,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PXL10-59D,PXL1059D,10,3.05,43.1,Comsearch's Frequency Coordination Database +Commscope/Andrew,PXL1065,PXL1065,10,3.05,44,https://www.launch3telecom.com/commscopeandrew/pxl1065d7a.html +ANDREW,PXL10-65,PXL1065,10,3.05,44,https://www.launch3telecom.com/commscopeandrew/pxl1065d7a.html +COMMSCOPE,PXL10-65,PXL1065,10,3.05,44,Comsearch's Frequency Coordination Database +ANDREW,PXL10-65D,PXL1065D,10,3.05,44,https://www.launch3telecom.com/commscopeandrew/pxl1065d7a.html +COMMSCOPE,PXL10-65D,PXL1065D,10,3.05,44,Comsearch's Frequency Coordination Database +Commscope/Andrew,PXL1259,PXL1259,12,3.66,45,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PXL12-59F,PXL1259F,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PXL12-59F,PXL1259F,12,3.66,44.8,Comsearch's Frequency Coordination Database +ANDREW,PXl12-59F,PXL1259F,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope/Andrew,PXL1265,PXL1265,12,3.66,45.4,https://www.launch3telecom.com/commscopeandrew/pxl1265.html +ANDREW,PXL12-65E,PXL1265E,12,3.66,45.4,https://www.launch3telecom.com/commscopeandrew/pxl1265.html +COMMSCOPE,PXL12-65E,PXL1265E,12,3.66,45.4,Comsearch's Frequency Coordination Database +ANDREW,PXL1O-59D,PXL1O59D,10,3.05,43.1,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope/Andrew,PXL659,PXL659,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PXL6-59,PXL659,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PXL6-59,PXL659,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PXL6-59D,PXL659D,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PXL6-59E,PXL659E,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PXL6-59E,PXL659E,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PXL6-59F,PXL659F,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PXL6-59F,PXL659F,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope/Andrew,PXL665,PXL665,6,1.83,39.4,https://www.datasheet.live/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.live%2Fdatasheets-1%2Fandrew%2FPL6-65.pdf +ANDREW,PXL6-65D,PXL665D,6,1.83,39.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,PXL6-65D,PXL665D,6,1.83,39.4,Comsearch's Frequency Coordination Database +Commscope/Andrew,PXL859,PXL859,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,PXL8-59,PXL859,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PXL8-59C,PXL859C,8,2.44,41.3,Comsearch's Frequency Coordination Database +COMMSCOPE,PXL859D,PXL859D,8,2.44,41.3,Comsearch's Frequency Coordination Database +ANDREW,PXL8-59D,PXL859D,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,PXL8-59D,PXL859D,8,2.44,41.3,Comsearch's Frequency Coordination Database +ANDREW,PXL8-59 RF,PXL859RF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope/Andrew,PXL865,PXL865,8,2.44,42,https://objects.eanixter.com/PD356081.PDF +ANDREW,PXL8-65,PXL865,8,2.44,42,https://objects.eanixter.com/PD356081.PDF +RFS,PXL8-65,PXL865,8,2.44,42,Comsearch's Frequency Coordination Database +ANDREW,PXL8-65C,PXL865C,8,2.44,42,https://objects.eanixter.com/PD356081.PDF +ANDREW,PXL8-65D,PXL865D,8,2.44,42,https://objects.eanixter.com/PD356081.PDF +COMMSCOPE,PXL8-65D,PXL865D,8,2.44,42,Comsearch's Frequency Coordination Database +RFS,RAD6-65A,RAD665A,6,1.83,39.6,Typo should be PAD665A. Using that gain. +GABRIEL,RF10P-2J23,RF10P2J23,10,3.05,43.5,Comsearch's Frequency Coordination Database +GABRIEL ELECTRONICS,RF8C-J45,RF8CJ45,8,2.44,42,Comsearch's Frequency Coordination Database +,,RFMA0736UH12WS,4,1.22,35.6,https://rfeq.com/wp-content/uploads/2018/10/RFMA-0736UH1.2WSxx-new.pdf +RF ENGINEERING AND ENERGY,RFMA-0736UH1.2WS,RFMA0736UH12WS,4,1.22,35.6,https://rfeq.com/wp-content/uploads/2018/10/RFMA-0736UH1.2WSxx-new.pdf +RFS,SB4-W60,SB4W60,4,1.22,35.7,https://www.rfsworld.com/pim/product/html/SB4-W60EC +RFS,SB4-W60A,SB4W60A,4,1.22,34.8,https://www.rfsworld.com/pim/product/html/SB4-W60EC +RFS,SB4-W60C,SB4W60C,4,1.22,35.7,https://www.rfsworld.com/pim/product/html/SB4-W60EC +RFS,SB4-W60CMPT,SB4W60CMPT,4,1.22,35.7,https://www.rfsworld.com/pim/product/html/SB4-W60EC +RFS,Sb4-W60CMPT,SB4W60CMPT,4,1.22,35.7,https://www.rfsworld.com/pim/product/html/SB4-W60EC +RFS,SB4-W60D,SB4W60D,4,1.22,35.7,https://www.rfsworld.com/pim/product/html/SB4-W60EC +RFS,SB4-W60E,SB4W60E,4,1.22,35.7,"https://www.rfsworld.com/images/sma/rpe/sb4/sb4-w60e,%20140601.pdf" +RFS,SB6-W60,SB6W60,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SB6-W60DC2 +RFS,SB6-W60A,SB6W60A,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SB6-W60DC2 +RFS,SB6-W60AMPT,SB6W60AMPT,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SB6-W60DC2 +RFS,SB6-W60B,SB6W60B,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SB6-W60DC2 +RFS,SB6-W60BMPT,SB6W60BMPT,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SB6-W60DC2 +RFS,SB6-W60C,SB6W60C,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SB6-W60DC2 +RFS,SB6-W60CMPT,SB6W60CMPT,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SB6-W60DC2 +RFS,SB6-W60D,SB6W60D,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SB6-W60DC2 +RFS,SB6-W60DMPT,SB6W60DMPT,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SB6-W60DC2 +RFS,SBX4?W60A,SBX4W60A,4,1.22,34.8,https://www.rfsworld.com/pim/product/html/SBX4-W60EC +COMMSCOPE,SBX4-W60A,SBX4W60A,4,1.22,34.8,Comsearch's Frequency Coordination Database +RFS,SBX4-W60A,SBX4W60A,4,1.22,34.8,https://www.rfsworld.com/pim/product/html/SBX4-W60EC +RFS,SBX4?W60C,SBX4W60C,4,1.22,35.7,https://www.rfsworld.com/pim/product/html/SBX4-W60EC +RFS,SBX4W60C,SBX4W60C,4,1.22,35.7,https://www.rfsworld.com/pim/product/html/SBX4-W60EC +RFS,SBX4-W60C,SBX4W60C,4,1.22,35.7,https://www.rfsworld.com/pim/product/html/SBX4-W60EC +RFS,SBX4-W60CMPT,SBX4W60CMPT,4,1.22,35.7,https://www.rfsworld.com/pim/product/html/SBX4-W60EC +RFS,SBX4-W60D,SBX4W60D,4,1.22,35.7,https://www.rfsworld.com/pim/product/html/SBX4-W60EC +RFS,SBX4-W60DIPN,SBX4W60DIPN,4,1.22,35.7,https://www.rfsworld.com/pim/product/html/SBX4-W60EC +RFS,SBX6-W30C,SBX6W30C,6,1.83,,Indeterminate. Band designator 30 unknown +RFS,SBX6-W60A,SBX6W60A,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SBX6-W60DC2 +RFS,SBX6-W60B,SBX6W60B,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SBX6-W60DC2 +RFS,SBX6-W60BMPT,SBX6W60BMPT,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SBX6-W60DC2 +RFS,SBX6?W60C,SBX6W60C,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SBX6-W60DC2 +RFS,SBX6-W60C,SBX6W60C,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SBX6-W60DC2 +RFS,SBX6-W60CC,SBX6W60CC,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SBX6-W60DC2 +RFS,SBX6-W60CIPN,SBX6W60CIPN,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SBX6-W60DC2 +RFS,SBX6-W60CMPT,SBX6W60CMPT,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SBX6-W60DC2 +RFS,SBX6-W60D,SBX6W60D,6,1.83,39.4,https://www.rfsworld.com/pim/product/html/SBX6-W60DC2 +RFS,SC3-W60,SC3W60,3,0.91,33.2,https://www.rfsworld.com/pim/product/html/SC3-W60BC +RFS,SC3W60A,SC3W60A,3,0.91,33.2,https://www.rfsworld.com/pim/product/html/SC3-W60BC +RFS,SC3-W60A,SC3W60A,3,0.91,33.2,https://www.rfsworld.com/pim/product/html/SC3-W60BC +RFS,SC3-W60A SC3-,SC3W60ASC3,3,0.91,33.2,https://www.rfsworld.com/pim/product/html/SC3-W60BC +RFS,SCX3-W60A,SCX3W60A,3,0.91,33.2,https://www.rfsworld.com/pim/product/html/SCX3-W60BC +RFS,SCX3-W60AMPT,SCX3W60AMPT,3,0.91,33.2,https://www.rfsworld.com/pim/product/html/SCX3-W60BC +RFS,SCX3-W60W,SCX3W60W,3,0.91,33.2,https://www.rfsworld.com/pim/product/html/SCX3-W60BC +COMMSCOPE,SEUHX6-59 RF,SEUHX659RF,6,1.83,38.8,Comsearch's Frequency Coordination Database +Commscope,SHP3-6W,SHP36W,3,1.22,33.6,https://www.commscope.com/globalassets/digizuite/696934-p360-shp3-6w-4wh-b-external.pdf +RADIO WAVES,SHP6-59,SHP659,6,1.83,39,https://www.radiowaves.com/getmedia/22d052e7-9370-4f2b-81b5-0dafc7c72849/SHP6-5.9.aspx +RADIO WAVES,SHP6-6,SHP66,6,1.83,39.3,https://www.radiowaves.com/getmedia/f938b911-2fb8-4c71-8b06-53a59d88df1f/SHP6-6.aspx +RADIO WAVES,SHPD6-5.9,SHPD659,6,1.83,38.8,https://www.radiowaves.com/getmedia/12291e54-5884-4572-a349-1f5c81405ccb/SHPD6-5.9.aspx +RADIO WAVES,SHPD6-59,SHPD659,6,1.83,38.8,https://www.radiowaves.com/getmedia/12291e54-5884-4572-a349-1f5c81405ccb/SHPD6-5.9.aspx +RADIO WAVES,SHPD6-6,SHPD66,6,1.83,39.1,https://www.radiowaves.com/getmedia/542466f7-b439-4bc1-8eeb-cd23cbfff1a1/SHPD6-6.aspx +RADIO WAVES,SHPD6-6.4,SHPD664,6,1.83,38.9,https://www.radiowaves.com/getmedia/8ce005bb-c2e5-4edf-9cb8-289e91e58fdd/SHPD6-6.4.aspx +RADIO WAVES,SHPD8-5.9,SHPD859,8,2.44,41.7,Comsearch's Frequency Coordination Database +RADIO WAVES,SHPD8-6,SHPD86,8,2.44,41.9,https://www.everythingrf.com/product-datasheet/822-902-shpd8-6 +RADIO WAVES,SHPD8-6.4,SHPD864,8,2.44,42.1,Comsearch's Frequency Coordination Database +Commscope,SHPX4-6W,SHPX46W,4,1.22,35.5,https://www.commscope.com/globalassets/digizuite/263736-p360-shpx4-6w-6wh-external.pdf +COMMSCOPE,SHPX6-6W,SHPX66W,6,1.83,39.4,Comsearch's Frequency Coordination Database +RADIO WAVES,SP4-5.9,SP459,4,1.22,36.2,https://www.radiowaves.com/en/product/sp4-5-9 +RFS,SP6-107A,SP6107A,,,,Model does not belong in this band. +RADIO WAVES,SP6-5.9,SP659,6,1.83,39.2,https://www.radiowaves.com/getmedia/36695c42-a711-4a3b-bba8-b7a40bde172e/SP6-5.9.aspx +Radiowaves,SP659,SP659,6,1.83,39.2,https://www.radiowaves.com/getmedia/36695c42-a711-4a3b-bba8-b7a40bde172e/SP6-5.9.aspx +RADIO WAVES,SP6-59,SP659,6,1.83,39.2,https://www.radiowaves.com/getmedia/36695c42-a711-4a3b-bba8-b7a40bde172e/SP6-5.9.aspx +RFS,SP6-59A,SP659A,6,1.83,39.2,https://www.radiowaves.com/getmedia/36695c42-a711-4a3b-bba8-b7a40bde172e/SP6-5.9.aspx +RFS,SP6-59B,SP659B,6,1.83,39.2,https://www.radiowaves.com/getmedia/36695c42-a711-4a3b-bba8-b7a40bde172e/SP6-5.9.aspx +Radiowaves,SP664,SP664,6,1.83,39.2,https://www.radiowaves.com/getmedia/9abed637-a49d-44a6-a588-b2e3d343d8ec/SP6-6.4.aspx +RADIO WAVES,SP6-64,SP664,6,1.83,39.2,https://www.radiowaves.com/en/product/sp6-6-4 +RFS,SP6-65,SP665,6,1.83,39.9,Comsearch's Frequency Coordination Database +RFS,SP6-65A,SP665A,6,1.83,39.9,Comsearch's Frequency Coordination Database +RFS,SP6-65B,SP665B,6,1.83,39.9,Comsearch's Frequency Coordination Database +RADIO WAVES,SP8-5.9,SP859,8,2.44,41.9,Comsearch's Frequency Coordination Database +RADIO WAVES,SP8-59,SP859,8,2.44,41.9,Comsearch's Frequency Coordination Database +RADIO WAVES,SPD6-5.9,SPD659,6,1.83,38.9,https://www.radiowaves.com/en/product/spd6-5-9 +RADIO WAVES,SPD8-5.9,SPD859,8,2.44,41.6,Comsearch's Frequency Coordination Database +GABRIEL,SR10-59ASE,SR1059ASE,10,3.05,43,Comsearch's Frequency Coordination Database +GABRIEL,SR10-59SE,SR1059SE,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,SR10-64,SR1064,10,3.05,44,Comsearch's Frequency Coordination Database +GABRIEL,SR10-64B,SR1064B,10,3.05,43.8,Comsearch's Frequency Coordination Database +GABRIEL,SR10-64BSE,SR1064BSE,10,3.05,43.8,Comsearch's Frequency Coordination Database +GABRIEL,SR12-59ASE,SR1259ASE,12,3.66,44.6,Comsearch's Frequency Coordination Database +GABRIEL,SR12-59SE,SR1259SE,12,3.66,44.9,Comsearch's Frequency Coordination Database +GABRIEL,SR12-64A,SR1264A,12,3.66,45.4,Comsearch's Frequency Coordination Database +GABRIEL,SR4-64A,SR464A,4,1.22,,Unknown Model +GABRIEL,SR6-5971SE,SR65971SE,6,1.83,39,Comsearch's Frequency Coordination Database +GABRIEL,SR6-59ASE,SR659ASE,6,1.83,38.8,Comsearch's Frequency Coordination Database +GABRIEL,SR6-59CSE,SR659CSE,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,SR6-64B,SR664B,6,1.83,39.7,Comsearch's Frequency Coordination Database +GABRIEL,SR6-64CSE,SR664CSE,6,1.83,39.9,Comsearch's Frequency Coordination Database +GABRIEL,SR6-64DSE,SR664DSE,6,1.83,39.4,Comsearch's Frequency Coordination Database +GABRIEL,SR8-59ASE,SR859ASE,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,SR8-59SE,SR859SE,8,2.44,41.3,Comsearch's Frequency Coordination Database +GABRIEL,SR8-64,SR864,8,2.44,42,Comsearch's Frequency Coordination Database +GABRIEL,SR8-64A,SR864A,8,2.44,42.2,Comsearch's Frequency Coordination Database +GABRIEL,SR8-64B,SR864B,8,2.44,42.2,Comsearch's Frequency Coordination Database +GABRIEL,SR8-64CSE,SR864CSE,8,2.44,41.9,Comsearch's Frequency Coordination Database +GABRIEL,SR8-64DSE,SR864DSE,8,2.44,41.9,Comsearch's Frequency Coordination Database +GABRIEL,SRD10-64,SRD1064,10,3.05,43.7,Comsearch's Frequency Coordination Database +GABRIEL,SRD10-64BSE,SRD1064BSE,10,3.05,43.8,Comsearch's Frequency Coordination Database +GABRIEL,SRD10P-3J23A,SRD10P3J23A,10,3.05,43,Comsearch's Frequency Coordination Database +GABRIEL,SRD6-59BSE,SRD659BSE,6,1.83,38.5,Comsearch's Frequency Coordination Database +GABRIEL,SRD6-64DSE,SRD664DSE,6,1.83,39.3,Comsearch's Frequency Coordination Database +GABRIEL,SRD8-59ASE,SRD859ASE,8,2.44,41,Comsearch's Frequency Coordination Database +GABRIEL,SRDD10P-1J23107,SRDD10P1J23107,10,3.05,42.3,Comsearch's Frequency Coordination Database +GABRIEL,SRDD10P-J59107A,SRDD10PJ59107A,10,3.05,42.3,Comsearch's Frequency Coordination Database +GABRIEL,SRDD12P-J59107A,SRDD12PJ59107A,12,3.66,44.2,Comsearch's Frequency Coordination Database +GABRIEL,SRDD6P-J59107,SRDD6PJ59107,6,1.83,37.8,Comsearch's Frequency Coordination Database +RFS,SU4-59D,SU459D,4,1.22,35.3,https://www.rfsworld.com/pim/product/html/SU4-59DC2H +RFS,SU4-W60,SU4W60,4,1.22,35.6,Comsearch's Frequency Coordination Database +RFS,SU6-59A,SU659A,6,1.83,38.8,https://www.rfsworld.com/pim/product/html/SU6-59BC2H +RFS,SU6-59B,SU659B,6,1.83,38.8,https://www.rfsworld.com/pim/product/html/SU6-59BC2H +RFS,SU6-65B,SU665B,6,1.83,39.7,https://www.rfsworld.com/pim/product/html/SU6-65BC2H +RFS,SU6-B59,SU6B59,6,1.83,39.7,https://www.rfsworld.com/pim/product/html/SU6-65BC2H +RFS,SU6B-W60B,SU6BW60B,6,1.83,39.4,Comsearch's Frequency Coordination Database +RFS,SU6-W60B,SU6W60B,6,1.83,39.1,Comsearch's Frequency Coordination Database +RFS,SUX4-59A,SUX459A,4,1.22,34.5,https://www.rfsworld.com/pim/product/html/SUX4-59AC2H +RFS,SUX4-65A,SUX465A,4,1.22,35.3,https://www.rfsworld.com/pim/product/html/SUX4-65AC2H +RFS,SUX4B-W60D,SUX4BW60D,4,1.22,35.7,Comsearch's Frequency Coordination Database +RFS,SUX4-W60C,SUX4W60C,4,1.22,,Indeterminate. +RFS,SUX6-59B,SUX659B,6,1.83,38.6,https://www.rfsworld.com/pim/product/html/SUX6-59BC2H +RFS,SUX6-59BC2H,SUX659BC2H,6,1.83,38.6,https://alliancecorporation.ca/images/documents/wireless-infrastructure-documents/RFS/Microwave_antennas/Alliance_distributor_RFS_Microwave_Antenna_SUX6-59BC2H.pdf +RFS,SUX6-59B RF,SUX659BRF,6,1.83,38.6,https://www.rfsworld.com/pim/product/html/SUX6-59BC2H +RFS,SUX6-65B,SUX665B,6,1.83,39.5,https://www.rfsworld.com/pim/product/html/SUX6-65BC2H +,,THP12059DWB,4,1.22,35.5,https://www.fainitelecommunication.com/pdf2.php?id=215 +,,THP12059DWB,4,1.22,35.5,https://www.fainitelecommunication.com/pdf2.php?id=215 +Faini Telecommunication S,THP 12 059 D WB,THP12059DWB,4,1.22,35.5,https://www.fainitelecommunication.com/pdf2.php?id=215 +TONGYU,TYA12U06WS,TYA12U06WS,4,1.22,35.6,http://www.belcoproducts.com/prodotti/wp-content/uploads/Tongyu-Microwave-Antenna-Catalogue.pdf +TONGYU,TYA18U06WS,TYA18U06WS,6,1.83,39.4,http://www.belcoproducts.com/prodotti/wp-content/uploads/Tongyu-Microwave-Antenna-Catalogue.pdf +TONGYU,TYA24U06WS,TYA24U06WS,8,2.44,41.7,http://www.belcoproducts.com/prodotti/wp-content/uploads/Tongyu-Microwave-Antenna-Catalogue.pdf +RFS,UA10-59,UA1059,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/UA10-59AC +RFS,UA10 59A,UA1059A,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/UA10-59AC +RFS,UA10-59A,UA1059A,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/UA10-59AC +CABLEWAVE SYSTEMS,UA10-59AC,UA1059AC,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/UA10-59AC +RFS,UA10-59A (P),UA1059AP,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/UA10-59AC +CABLEWAVE SYSTEMS,UA10-59W,UA1059W,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/UA10-59AC +RFS,UA10-59W,UA1059W,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/UA10-59AC +CABLEWAVE SYSTEMS,UA10-65,UA1065,10,3.05,44.1,https://www.rfsworld.com/pim/product/html/UA10-65AC +RFS,UA10-W59A,UA10W59A,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/UA10-59AC +RFS,UA12-65A,UA1265A,12,3.66,45.8,https://www.rfsworld.com/pim/product/html/UA12-65AC +Cablewave,UA6-59,UA659,6,1.83,38.8,https://www.rfsworld.com/userfiles/pdf/299.pdf +RFS,UA6-59A,UA659A,6,1.83,39,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +CABLEWAVE SYSTEMS,UA6-59AC,UA659AC,6,1.83,39,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +RFS,UA6-59B,UA659B,6,1.83,39,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +RFS,UA6-65,UA665,6,1.83,39.8,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +RFS,UA6-65A,UA665A,6,1.83,39.8,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +RFS,UA6-65B,UA665B,6,1.83,39.8,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +RFS,UA6-W59A,UA6W59A,6,1.83,39,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +CABLEWAVE SYSTEMS,UA8-59,UA859,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UA8-59AC +RFS,UA8-59,UA859,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UA8-59AC +CABLEWAVE SYSTEMS,UA8-59A,UA859A,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UA8-59AC +RFS,UA8-59A,UA859A,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UA8-59AC +CABLEWAVE SYSTEMS,UA8-59AC,UA859AC,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UA8-59AC +GABRIEL,UA8-59AC,UA859AC,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UA8-59AC +RFS,UA8-59W,UA859W,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UA8-59AC +CABLEWAVE SYSTEMS,UA8 65,UA865,8,2.44,42.3,https://www.rfsworld.com/pim/product/html/UA8-65AC +CABLEWAVE SYSTEMS,UA8-65,UA865,8,2.44,42.3,https://www.rfsworld.com/pim/product/html/UA8-65AC +RFS,UA8-65,UA865,8,2.44,42.3,https://www.rfsworld.com/pim/product/html/UA8-65AC +RFS,UA8-65A,UA865A,8,2.44,42.3,https://www.rfsworld.com/pim/product/html/UA8-65AC +ANDREW,UA8-65AC,UA865AC,8,2.44,42.3,https://alliancecorporation.ca/images/documents/wireless-infrastructure-documents/RFS/Microwave_antennas/Alliance_distributor_RFS_Microwave_Antenna_UA8-65AC.pdf +RFS,UA8-65AC,UA865AC,8,2.44,42.3,https://www.rfsworld.com/pim/product/html/UA8-65AC +RFS,UA8-65A(P) RF,UA865APRF,8,2.44,42.3,https://www.rfsworld.com/pim/product/html/UA8-65AC +RFS,UA8-W59A,UA8W59A,8,2.44,41.6,http://www.dadehnama.ir/uploads/4_313508659575390274.pdf +CABLEWAVE SYSTEMS,UAX10-59A RF,UAX1059ARF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +CABLEWAVE SYSTEMS,UAX10-59A RF,UAX1059ARF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +RFS,UAX8-59B RF,UAX859BRF,8,2.44,41.3,Typo should be UXA859BRF. Using that gain +CABLEWAVE SYSTEMS,UAX8-59W RF C64007,UAX859WRFC64007,8,2.44,41.3,Typo should be UXA859WRFC64007. Using that gain. +RFS,UAX8-W59A RF,UAX8W59ARF,8,2.44,41.3,Typo should be UXA8W59ARF. Using that gain. +GABRIEL,UCC10-59A LF,UCC1059ALF,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10-59A RF,UCC1059ARF,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10-59B L,UCC1059BL,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10-59B LF,UCC1059BLF,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10-59B RF,UCC1059BRF,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10-59 CSE (L),UCC1059CSEL,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10-59C SE L,UCC1059CSEL,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC1059CSE L,UCC1059CSEL,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10-59CSE L,UCC1059CSEL,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10-59CSE(L),UCC1059CSEL,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10-59C SE R,UCC1059CSER,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10-59CSE R,UCC1059CSER,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10-59 L,UCC1059L,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10-59 LF,UCC1059LF,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL ELECTRONICS,UCC10-59 R,UCC1059R,10,3.05,43.2,https://its.ntia.gov/umbraco/surface/download/publication?reportNumber=90-267_ocr.pdf +GABRIEL,UCC10-59 RF,UCC1059RF,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10P-59BSE,UCC10P59BSE,10,3.05,43.2,Likely typo. Used this coordination for the UCC10-59BSE: https://wireless2.fcc.gov/UlsEntry/attachments/attachmentViewRD.jsp?applType=search&fileKey=2066524072&attachmentKey=17826249&attachmentInd=applAttach +GABRIEL,UCC10W-59B RF,UCC10W59BRF,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC10X-59DSE RF,UCC10X59DSERF,10,3.05,43.2,Comsearch's Frequency Coordination Database +GABRIEL,UCC12-59A L,UCC1259AL,12,3.66,44.8,Comsearch's Frequency Coordination Database +Gabriel Electronics,UCC12-59A (LF),UCC1259ALF,12,3.66,44.8,https://its.ntia.gov/umbraco/surface/download/publication?reportNumber=90-267_ocr.pdf +GABRIEL,UCC12-59A LF,UCC1259ALF,12,3.66,44.8,Comsearch's Frequency Coordination Database +GABRIEL,UCC12-59A RF,UCC1259ARF,12,3.66,44.8,Comsearch's Frequency Coordination Database +GABRIEL ELECTRONICS,UCC12-59BSE(LF),UCC1259BSELF,12,3.66,44.8,https://its.ntia.gov/umbraco/surface/download/publication?reportNumber=90-267_ocr.pdf +GABRIEL ELECTRONICS,UCC12-59BSE(R),UCC1259BSER,12,3.66,44.8,https://its.ntia.gov/umbraco/surface/download/publication?reportNumber=90-267_ocr.pdf +GABRIEL,UCC12-59 LF,UCC1259LF,12,3.66,44.8,Comsearch's Frequency Coordination Database +GABRIEL,UCC12-59 RF,UCC1259RF,12,3.66,44.8,Comsearch's Frequency Coordination Database +GABRIEL,UCC12W-59A RF,UCC12W59ARF,12,3.66,44.8,Comsearch's Frequency Coordination Database +GABRIEL,UCC6-59A LF,UCC659ALF,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,UCC6-59ASE,UCC659ASE,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,UCC6-59ASE LF,UCC659ASELF,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,UCC6-59ASE R,UCC659ASER,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,UCC6.59ASE RF,UCC659ASERF,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,UCC6-59A SE RF,UCC659ASERF,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,UCC6-59ASE (RF),UCC659ASERF,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,UCC6-59ASE RF,UCC659ASERF,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,UCC6-59ASE(RF),UCC659ASERF,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,UCC6X-59A,UCC6X59A,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,UCC6X-59A (R),UCC6X59AR,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,UCC8-59A,UCC859A,8,2.44,41.1,Comsearch's Frequency Coordination Database +Gabriel Electronics,UCC8-59A (LF),UCC859ALF,8,2.44,41.1,https://its.ntia.gov/umbraco/surface/download/publication?reportNumber=90-267_ocr.pdf +GABRIEL,UCC859A LF,UCC859ALF,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,UCC8-59A LF,UCC859ALF,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,UCC8-59A LF,UCC859ALF,8,2.44,41.1,Comsearch's Frequency Coordination Database +CABLEWAVE,UCC8-59A LF,UCC859ALF,8,2.44,41.1,https://its.ntia.gov/umbraco/surface/download/publication?reportNumber=90-267_ocr.pdf +Gabriel,UCC8-59A (RF),UCC859ARF,8,2.44,41.1,https://its.ntia.gov/umbraco/surface/download/publication?reportNumber=90-267_ocr.pdf +GABRIEL,UCC8-59A RF,UCC859ARF,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,UCC8-59ASE RF,UCC859ASERF,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,UCC8-59B LF,UCC859BLF,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,UCC8-59BSE (LF),UCC859BSELF,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,UCC8-59BSE LF,UCC859BSELF,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,UCC8-59BSE(R),UCC859BSER,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,UCC8-59BSE (RF),UCC859BSERF,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,UCC8-59BSE RF,UCC859BSERF,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,UCC8W-59SE LF,UCC8W59SELF,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,UCC8X-59CSE RF,UCC8X59CSERF,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,UCC8x-59CSE RF,UCC8X59CSERF,8,2.44,41.1,Comsearch's Frequency Coordination Database +RFS,UDA10-59A,UDA1059A,10,3.05,43.2,Comsearch's Frequency Coordination Database +RFS,UDA10-59A RF,UDA1059ARF,10,3.05,43.2,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,UDA10-59C RF,UDA1059CRF,10,3.05,43.2,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,UDA10-59C-RF,UDA1059CRF,10,3.05,43.2,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,UDA12-59A RF,UDA1259ARF,12,3.66,44.8,Comsearch's Frequency Coordination Database +RFS,UDA12-59A RF,UDA1259ARF,12,3.66,44.8,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,UDA6-59,UDA659,6,1.83,38.7,Comsearch's Frequency Coordination Database +RFS,UDA6-59A,UDA659A,6,1.83,38.7,Comsearch's Frequency Coordination Database +RFS,UDA6-59A LF,UDA659ALF,6,1.83,38.7,Comsearch's Frequency Coordination Database +RFS,UDA6-59A RF,UDA659ARF,6,1.83,38.7,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,UDA6-59 RF,UDA659RF,6,1.83,38.7,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,UDA6-59RF,UDA659RF,6,1.83,38.7,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,UDA6-59-RF,UDA659RF,6,1.83,38.7,Comsearch's Frequency Coordination Database +RFS,UDA6-65A RF,UDA665ARF,6,1.83,39.7,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,UDA8-59A,UDA859A,8,2.44,41.3,Comsearch's Frequency Coordination Database +RFS,UDA8-59A,UDA859A,8,2.44,41.3,Comsearch's Frequency Coordination Database +RFS,UDA8-59A LF,UDA859ALF,8,2.44,41.3,Comsearch's Frequency Coordination Database +RFS,UDA8-59A RF,UDA859ARF,8,2.44,41.3,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,UDA8-59C,UDA859C,8,2.44,41.3,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,UDA8-59C LF,UDA859CLF,8,2.44,41.3,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,UDA8-59C RF,UDA859CRF,8,2.44,41.3,Comsearch's Frequency Coordination Database +RFS,UDA8-65A RF,UDA865ARF,8,2.44,42.2,Comsearch's Frequency Coordination Database +CABLEWAVE SYSTEMS,UDA8-65 LF,UDA865LF,8,2.44,42.2,Comsearch's Frequency Coordination Database +COMMSCOPE,UH6-59WB RF,UH659WBRF,6,1.83,,Indeterminate. Third character has multiple options. +RFS,UHA8-59A LF,UHA859ALF,8,2.44,41.3,Typo should be UDA859ALF. Using that gain. +COMMSCOPE,UHK6-59K RF,UHK659KRF,6,1.83,,Indeterminate. Third character has multiple options. +ANDREW,UHP10-59W,UHP1059W,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHP10-59W,UHP1059W,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHP10-59W LF,UHP1059WLF,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP10-59W RF,UHP1059WRF,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHP10-59W RF,UHP1059WRF,10,3.05,43.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP12-59W R,UHP1259WR,12,3.66,45.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP12-59W RF,UHP1259WRF,12,3.66,45.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHP12-59W RF,UHP1259WRF,12,3.66,45.2,Comsearch's Frequency Coordination Database +ANDREW,UHP6-59,UHP659,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope/Andrew,UHP659W,UHP659W,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP6-59W,UHP659W,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP6-59WA R,UHP659WAR,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP6-59WA RF,UHP659WARF,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP6-59WB LF,UHP659WBLF,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHP6-59WB LF,UHP659WBLF,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP6-59WB RF,UHP659WBRF,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHP6-59WB RF,UHP659WBRF,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP6-59W LF,UHP659WLF,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHP6-59W LF,UHP659WLF,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP6-59W R,UHP659WR,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP6-59W RF,UHP659WRF,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHP6-59W RF,UHP659WRF,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP6-59W-RF,UHP659WRF,6,1.83,39.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP8 50W LF,UHP850WLF,8,2.44,,Indeterminate. Band designator 50 has multiple options. +Commscope/Andrew,UHP859W,UHP859W,8,2.44,41.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP8-59W,UHP859W,8,2.44,41.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP8 59W LF,UHP859WLF,8,2.44,41.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP8-59W LF,UHP859WLF,8,2.44,41.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHP8-59W LF,UHP859WLF,8,2.44,41.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP8-59WLF,UHP859WLF,8,2.44,41.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP8-59W RF,UHP859WRF,8,2.44,41.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHP8-59W RF,UHP859WRF,8,2.44,41.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHP8-59W RW,UHP859WRW,8,2.44,41.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +RADIO WAVES,UHPD6-59,UHPD659,6,1.83,39,Comsearch's Frequency Coordination Database +RADIO WAVES,UHPD6-64,UHPD664,6,1.83,39.4,Comsearch's Frequency Coordination Database +ARC WIRELESS,UHP-MW-6-4,UHPMW64,4,1.22,,Multiple gains for this model exist in Comsearch's frequency coordination database. +GABRIEL,UHR-10B-B,UHR10BB,10,3.05,44.5,Comsearch's Frequency Coordination Database +Gabriel,UHR-10C,UHR10C,10,3.05,44.2,Comsearch's Frequency Coordination Database +GABRIEL,UHR-6-B,UHR6B,6,1.83,39.9,Comsearch's Frequency Coordination Database +ANDREW,UHX10-49J RF,UHX1049JRF,10,3.05,,Indeterminate. Band designator 49 has multiple options. +COMMSCOPE,UHX10-569K LF,UHX10569KLF,10,3.05,,Indeterminate. Band designator 569 has multiple options. +ANDREW,UHX10-58J LF,UHX1058JLF,10,3.05,42.1,https://www.launch3telecom.com/commscopeandrew/uhx1058w.html +ANDREW,UHX10-58WA,UHX1058WA,10,3.05,42.1,https://www.launch3telecom.com/commscopeandrew/uhx1058w.html +Commscope/Andrew,UHX1059,UHX1059,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59,UHX1059,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10 59C LF,UHX1059CLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59C LF,UHX1059CLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59C LF,UHX1059CLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10?59C RF,UHX1059CRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59C RF,UHX1059CRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59C RF,UHX1059CRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59C RF,UHX1059CRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59D,UHX1059D,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59-D3A-LEFT,UHX1059D3ALEFT,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59-D3A-RIGHT,UHX1059D3ARIGHT,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59D LF,UHX1059DLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59D LF,UHX1059DLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59D RF,UHX1059DRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59D RF,UHX1059DRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59D RF,UHX1059DRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX10-59D(RF),UHX1059DRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59E LF,UHX1059ELF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59E RF,UHX1059ERF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59E RF,UHX1059ERF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59F RF,UHX1059FRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59H,UHX1059H,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59H LF,UHX1059HLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59H LF,UHX1059HLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59H LF,UHX1059HLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX10-59H LF,UHX1059HLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59H RF,UHX1059HRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59H RF,UHX1059HRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59H RF,UHX1059HRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX1059J,UHX1059J,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59J,UHX1059J,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59J,UHX1059J,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX10-59J L,UHX1059JL,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW CORPORATION,UHX10-59JL,UHX1059JL,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59J LF,UHX1059JLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59J LF,UHX1059JLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59J LF,UHX1059JLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59J LF,UHX1059JLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX10-59J LF,UHX1059JLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59j LF,UHX1059JLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59JLF,UHX1059JLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59J R,UHX1059JR,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX 10-59J RF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10 59J RF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10?59J RF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59 J RF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope,UHX10-59 J RF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59J (RF),UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX1059J RF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59J RF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59J RF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59J RF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59J RF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX10-59J RF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX10-59J RF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59JRF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59J-RF,UHX1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59K,UHX1059K,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59K,UHX1059K,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59K LF,UHX1059KLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59K LF,UHX1059KLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX 10-59K RF,UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10?59K RF,UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10?59K RF,UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59K (RF),UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX1059K RF,UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59K RF,UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59K RF,UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59K RF,UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59K Rf,UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59K- RF,UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59-K RF,UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59KRF,UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59K-RF,UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59K-RF,UHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59L,UHX1059L,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59 LF,UHX1059LF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59 (RF),UHX1059RF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59 RF,UHX1059RF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59SWB,UHX1059SWB,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59W,UHX1059W,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59W,UHX1059W,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59WA,UHX1059WA,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59WA LF,UHX1059WALF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX1059WA RF,UHX1059WARF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59WA RF,UHX1059WARF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59WA RF,UHX1059WARF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59WB,UHX1059WB,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59WB LF,UHX1059WBLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59WB RF,UHX1059WBRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59WB RF,UHX1059WBRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59WB-RF,UHX1059WBRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59W LF,UHX1059WLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59W LF,UHX1059WLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59W LF,UHX1059WLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59W LF,UHX1059WLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX10-59W LF,UHX1059WLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope,UHX10-59W LF,UHX1059WLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59W-LF,UHX1059WLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59W-P3A,UHX1059WP3A,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59W-P3A (LF),UHX1059WP3ALF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59W-P3A (RF),UHX1059WP3ARF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59W-P3A (RF),UHX1059WP3ARF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59W RF,UHX1059WRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX10-59W RF,UHX1059WRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,uhx10-59w rf,UHX1059WRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59w rf,UHX1059WRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59WRF,UHX1059WRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-59W-RF,UHX1059WRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10-5J LF,UHX105JLF,10,3.05,,Indeterminate. Band designator 5 has multiple options. +ANDREW,UHX10-65,UHX1065,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX10-65D,UHX1065D,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX10-65-D3A-LEFT,UHX1065D3ALEFT,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX10-65-D3A-RIGHT,UHX1065D3ARIGHT,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX10-65D LF,UHX1065DLF,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX10-65D RF,UHX1065DRF,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX10-65D RF,UHX1065DRF,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX1065E,UHX1065E,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX10-65E,UHX1065E,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX10-65E LF,UHX1065ELF,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX10-65E LF,UHX1065ELF,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX10-65E RF,UHX1065ERF,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX10-65E RF,UHX1065ERF,10,3.05,44,Comsearch's Frequency Coordination Database +COMMSCOPE,UHX10-65E RF,UHX1065ERF,10,3.05,44,Comsearch's Frequency Coordination Database +ANDREW,UHX10-65E- RF,UHX1065ERF,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX10-65E-RF,UHX1065ERF,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX10-65-P3A (RF),UHX1065P3ARF,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX10-65 RF,UHX1065RF,10,3.05,44,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +Andrew Corporation,UHX10C-59C RF,UHX10C59CRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10J-59J RF,UHX10J59JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX10X-59C LF,UHX10X59CLF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW CORPORATION,UHX10X-59C RF,UHX10X59CRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX10X-59D,UHX10X59D,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX10X-65,UHX10X65,10,3.05,43.8,Comsearch's Frequency Coordination Database +ANDREW,UHX10X-65A RF,UHX10X65ARF,10,3.05,43.8,Comsearch's Frequency Coordination Database +ANDREW,UHX10X-65 RF,UHX10X65RF,10,3.05,43.8,Comsearch's Frequency Coordination Database +COMMSCOPE,UHX10X-65 RF,UHX10X65RF,10,3.05,43.8,Comsearch's Frequency Coordination Database +COMMSCOPE,UHX10X-65-RF,UHX10X65RF,10,3.05,43.8,Comsearch's Frequency Coordination Database +Commscope/Andrew,UHX1259,UHX1259,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59,UHX1259,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59C,UHX1259C,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59C RF,UHX1259CRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59D,UHX1259D,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59D LF,UHX1259DLF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX12-59D LF,UHX1259DLF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX1259D RF,UHX1259DRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59D RF,UHX1259DRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX12-59D RF,UHX1259DRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX1259DRF,UHX1259DRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59E,UHX1259E,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX12-59E (LF),UHX1259ELF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59E LF,UHX1259ELF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX12-59E LF,UHX1259ELF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX12-59E (RF),UHX1259ERF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX12-59E RF,UHX1259ERF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX12-59E(RF),UHX1259ERF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59F RF,UHX1259FRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX12-59F RF,UHX1259FRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59H,UHX1259H,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59H LF,UHX1259HLF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59H (RF),UHX1259HRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59H RF,UHX1259HRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew,UHX12-59H Rf,UHX1259HRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59H-RF,UHX1259HRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59J,UHX1259J,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59J LF,UHX1259JLF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX12-59J LF,UHX1259JLF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59J (RF),UHX1259JRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59J RF,UHX1259JRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX12-59J RF,UHX1259JRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX12-59J RF,UHX1259JRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,uhx12-59j rf,UHX1259JRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59 LF,UHX1259LF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59 RF,UHX1259RF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX12-59W,UHX1259W,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59W LF,UHX1259WLF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59W LF,UHX1259WLF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX12-59W LF,UHX1259WLF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope,UHX12-59W LF,UHX1259WLF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX 12-59W RF,UHX1259WRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX12-59W RF,UHX1259WRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX12-59W RF,UHX1259WRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope/Andrew,UHX1265,UHX1265,12,3.66,45.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX12-65J,UHX1265J,12,3.66,45.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX12-65J RF,UHX1265JRF,12,3.66,45.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX12-6W,UHX126W,12,3.66,,Indeterminate. Band designator 6W unknown. +COMMSCOPE,UHX15-59D RF,UHX1559DRF,15,4.57,46.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX15-59H LF,UHX1559HLF,15,4.57,46.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX15-59H LF,UHX1559HLF,15,4.57,46.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX15-59H RF,UHX1559HRF,15,4.57,46.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX15-59H RF,UHX1559HRF,15,4.57,46.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX15-59H RF,UHX1559HRF,15,4.57,46.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX15-59H RF,UHX1559HRF,15,4.57,46.4,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX15-65E RF,UHX1565ERF,15,4.57,46.9,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6059J RF,UHX6059JRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-059L RF,UHX6059LRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-56H R,UHX656HR,6,1.83,,Indeterminate. Band designator 56 unknown. +COMMSCOPE,UHX6-56L RF,UHX656LRF,6,1.83,,Indeterminate. Band designator 56 unknown. +Commscope/Andrew,UHX659,UHX659,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59,UHX659,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59B RF,UHX659BRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59C,UHX659C,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59C LF,UHX659CLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59C LF,UHX659CLF,6,1.83,38.8,Comsearch's Frequency Coordination Database +ANDREW,UHX6 59C RF,UHX659CRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59C RF,UHX659CRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59C RF,UHX659CRF,6,1.83,38.8,Comsearch's Frequency Coordination Database +ANDREW,UHX6-59C-RF,UHX659CRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59D RF,UHX659DRF,6,1.83,38.8,Comsearch's Frequency Coordination Database +ANDREW,UHX6-59H,UHX659H,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59H L,UHX659HL,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59H LF,UHX659HLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59H LF,UHX659HLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59H R,UHX659HR,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59H RF,UHX659HRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59H RF,UHX659HRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59H RF,UHX659HRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59H RF,UHX659HRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew Corporation,UHX6-59H RF,UHX659HRF,6,1.83,38.8,https://www.launch3telecom.com/shared_media/datasheet/commscope/uhx6-59-l_specifications.pdf +ANDREW,UHX6-59H-RF,UHX659HRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59J,UHX659J,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6 59J LF,UHX659JLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW CORPORATION,UHX6-59J (LF),UHX659JLF,6,1.83,38.8,https://objects.eanixter.com/PD357070.PDF +ANDREW,UHX6-59J LF,UHX659JLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59J LF,UHX659JLF,6,1.83,38.8,Comsearch's Frequency Coordination Database +COMMSCOPE,UHX6-59J-LF,UHX659JLF,6,1.83,38.8,Comsearch's Frequency Coordination Database +ANDREW,UHX6 59J RF,UHX659JRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59J (RF),UHX659JRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59J RF,UHX659JRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59J RF,UHX659JRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59J RF,UHX659JRF,6,1.83,38.8,Comsearch's Frequency Coordination Database +ANDREW,UHX6-59JRF,UHX659JRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59J-RF,UHX659JRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59J-RF,UHX659JRF,6,1.83,38.8,Comsearch's Frequency Coordination Database +ANDREW,UHX6-59J VLF,UHX659JVLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59K,UHX659K,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59K L,UHX659KL,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59K LE,UHX659KLE,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59K LF,UHX659KLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59K LF,UHX659KLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59K LF,UHX659KLF,6,1.83,38.8,Comsearch's Frequency Coordination Database +Andrew Corporation,UHX6-59K LF,UHX659KLF,6,1.83,38.8,https://objects.eanixter.com/PD357070.PDF +ANDREW,UHX6-59K lF,UHX659KLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59KLF,UHX659KLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59K-LF,UHX659KLF,6,1.83,38.8,Comsearch's Frequency Coordination Database +ANDREW,UHX6-59K R,UHX659KR,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59 K RF,UHX659KRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59K (RF),UHX659KRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59K RF,UHX659KRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59K RF,UHX659KRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59KRF,UHX659KRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59K-RF,UHX659KRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59K-RF,UHX659KRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59L,UHX659L,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59L,UHX659L,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59 LF,UHX659LF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59 LF,UHX659LF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59L LF,UHX659LLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59L LF,UHX659LLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59L LF,UHX659LLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59L LF,UHX659LLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59L RE,UHX659LRE,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6?59L RF,UHX659LRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59L RF,UHX659LRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59L RF,UHX659LRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59L RF,UHX659LRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59L RF,UHX659LRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59L- RF,UHX659LRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX-6-59L RF,UHX659LRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59LRF,UHX659LRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59L-RF,UHX659LRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59-P3A/L (LF),UHX659P3ALLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59-P3A/L (RF),UHX659P3ALRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59 RF,UHX659RF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59 RF,UHX659RF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59-RF,UHX659RF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59 RFL,UHX659RFL,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59W,UHX659W,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope,UHX659WA,UHX659WA,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59WA,UHX659WA,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59WA,UHX659WA,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59WA L,UHX659WAL,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59WA LF,UHX659WALF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59WA LF,UHX659WALF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59WA RF,UHX659WARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59WA RF,UHX659WARF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59WB,UHX659WB,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59WB LF,UHX659WBLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59WB LF,UHX659WBLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59WB LF,UHX659WBLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6?59WB RF,UHX659WBRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6?59WB RF,UHX659WBRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59WB RF,UHX659WBRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59WB RF,UHX659WBRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59WB RF,UHX659WBRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHx6-59WB RF,UHX659WBRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59WB-RF,UHX659WBRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59WB-RF,UHX659WBRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59-WB-RF,UHX659WBRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59WC,UHX659WC,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59WC LF,UHX659WCLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59WC RF,UHX659WCRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59WC RF,UHX659WCRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59WC RF,UHX659WCRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59W LF,UHX659WLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59W-LF,UHX659WLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59W-LF,UHX659WLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59W-P3A/B,UHX659WP3AB,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59W-P3A/B (LF),UHX659WP3ABLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59W-P3A/B (LF),UHX659WP3ABLF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59W P3A/B RF,UHX659WP3ABRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59W-P3A/B (RF),UHX659WP3ABRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59W-P3A/B (RF),UHX659WP3ABRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59W-P3A/B RF,UHX659WP3ABRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59W R,UHX659WR,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59W (RF),UHX659WRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59W RF,UHX659WRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59W RF,UHX659WRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX6-59W-RF,UHX659WRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX6-59W-RF,UHX659WRF,6,1.83,38.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope/Andrew,UHX665,UHX665,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-65,UHX665,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-65A RF,UHX665ARF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65D,UHX665D,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-65D RF,UHX665DRF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65D RF,UHX665DRF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-65d rf,UHX665DRF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-65E,UHX665E,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-65E L,UHX665EL,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-65E LF,UHX665ELF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65E LF,UHX665ELF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65E-LF,UHX665ELF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-65E R,UHX665ER,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-65E RF,UHX665ERF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65E RF,UHX665ERF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-65E-RF,UHX665ERF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65E-RF,UHX665ERF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65F,UHX665F,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65F LE,UHX665FLE,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-65F LF,UHX665FLF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65F LF,UHX665FLF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65F R,UHX665FR,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6 65F RF,UHX665FRF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6?65F RF,UHX665FRF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-65F RF,UHX665FRF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65F RF,UHX665FRF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65F RF,UHX665FRF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65F-RF,UHX665FRF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-65 LF,UHX665LF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX6-65-P3A/F (RF),UHX665P3AFRF,6,1.83,39.5,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX6-69L,UHX669L,6,1.83,39.5,Typo should be UHX665LF Using that gain +COMMSCOPE,UHX6-9L RF,UHX69LRF,6,1.83,,Indeterminate. Band designator 9 unknown. +COMMSCOPE,UHX8-56J RF,UHX856JRF,8,2.44,,Indeterminate. Band designator 58 unknown. +ANDREW,UHX8-58J RF,UHX858JRF,8,2.44,41.3,Typo should be UHX859JRF Using that gain +COMMSCOPE,UHX8-58J RF,UHX858JRF,8,2.44,41.3,Typo should be UHX859JRF Using that gain +Commscope/Andrew,UHX859,UHX859,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59,UHX859,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59C LF,UHX859CLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59C LF,UHX859CLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8 - 59C RF,UHX859CRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59C RF,UHX859CRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59C RF,UHX859CRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59D,UHX859D,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59D LF,UHX859DLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59D RF,UHX859DRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59D RF,UHX859DRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59D RF,UHX859DRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59D RF,UHX859DRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Andrew,UHX8-59D RF,UHX859DRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59E,UHX859E,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59E-R,UHX859ER,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59E RF,UHX859ERF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59E RF,UHX859ERF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59F,UHX859F,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59H,UHX859H,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX-8-59H,UHX859H,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8 59H LF,UHX859HLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8?59H LF,UHX859HLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59H (LF),UHX859HLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59H LF,UHX859HLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59H LF,UHX859HLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHx8-59H LF,UHX859HLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59h LF,UHX859HLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59H-LF,UHX859HLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59H LP,UHX859HLP,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8- 59H RF,UHX859HRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59H (RF),UHX859HRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59H (RF),UHX859HRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59H RF,UHX859HRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59H RF,UHX859HRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59H RF,UHX859HRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +GABRIEL,UHX8-59H RF,UHX859HRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW CORP,UHX8-59H RF,UHX859HRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59HRF,UHX859HRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59H-RF,UHX859HRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +Commscope,UHX859J,UHX859J,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59J,UHX859J,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59J,UHX859J,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59-J,UHX859J,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8--59J,UHX859J,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59J F,UHX859JF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59J L,UHX859JL,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX 8-59J LF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8 - 59J LF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8 59J LF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8 -59JLF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8?59J LF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59 J LF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59J (LF),UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59J LF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59J LF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59J LF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59J LF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +RFS,UHX8-59J LF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59J LF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59JLF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59J-LF,UHX859JLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59J R,UHX859JR,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8- 59J RF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8/-59J RF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8?59J RF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-?59J RF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59J (RF),UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59J RF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59J RF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59J RF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59J RF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59J RF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59j RF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,uhx8-59j rf,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59JRF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59JRF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59J-RF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59J-RF,UHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59K LF,UHX859KLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59KLF,UHX859KLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59K RF,UHX859KRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59K RF,UHX859KRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59KRF,UHX859KRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59KRF,UHX859KRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59 (LF),UHX859LF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59 LF,UHX859LF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59L LF,UHX859LLF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59L RF,UHX859LRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59 (RF),UHX859RF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59 RF,UHX859RF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59-RF,UHX859RF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59W,UHX859W,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59W,UHX859W,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59WA,UHX859WA,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59WA LF,UHX859WALF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59 WA RF,UHX859WARF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59WA RF,UHX859WARF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59WA-RF,UHX859WARF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59WB RF,UHX859WBRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59WB RF,UHX859WBRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59W-D3A-RIGHT,UHX859WD3ARIGHT,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59WJ RF,UHX859WJRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59W L,UHX859WL,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59W LF,UHX859WLF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59W LF,UHX859WLF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59W LF,UHX859WLF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHx8-59W LF,UHX859WLF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59W-P3A (LF),UHX859WP3ALF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59W-P3A (LF),UHX859WP3ALF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59W-P3A (RF),UHX859WP3ARF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59W-P3A (RF),UHX859WP3ARF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59W RD,UHX859WRD,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59 W RF,UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59W (RF),UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59W RF,UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59W RF,UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59W RF,UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59W RF,UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,uhx8-59W RF,UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59w rf,UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59w RF,UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59W rf,UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-59-W RF,UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59W/RF,UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59WRF,UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UHX8-59W-RF,UHX859WRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UHX8-5D RF,UHX85DRF,8,2.44,,Indeterminate. Band designator 5 unknown. +ANDREW,UHX8-5H RF,UHX85HRF,8,2.44,,Indeterminate. Band designator 5 unknown. +Commscope/Andrew,UHX865,UHX865,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65,UHX865,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65B LF,UHX865BLF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65D,UHX865D,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8 65D LF,UHX865DLF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65D LF,UHX865DLF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65D LF,UHX865DLF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX8-65D LF,UHX865DLF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65D RF,UHX865DRF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX8-65D RF,UHX865DRF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX865E,UHX865E,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65E,UHX865E,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX8-65E,UHX865E,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65e,UHX865E,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX8-65E EF,UHX865EEF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65E LF,UHX865ELF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX8-65E LF,UHX865ELF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65E R,UHX865ER,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65E RF,UHX865ERF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65E RF,UHX865ERF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX8-65E RF,UHX865ERF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65E- RF,UHX865ERF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX8-65E-RF,UHX865ERF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-65F,UHX865F,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX8-65 (LF),UHX865LF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX8-65-P3A (LF),UHX865P3ALF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX8-65-P3A (RF),UHX865P3ARF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +COMMSCOPE,UHX8-65 (RF),UHX865RF,8,2.44,42,https://www.digchip.com/datasheets/parts/datasheet/3751/HP10-65E-pdf.php +ANDREW,UHX8-86E LF,UHX886ELF,8,2.44,,Indeterminate. Band designator 86 unknown. +RFS,UHXA6-59B LF,UHXA659BLF,6,1.83,,Indeterminate. UHXA unknown could be UHX or UXA. +RFS,UHXA6-59B RF,UHXA659BRF,6,1.83,,Indeterminate. UHXA unknown could be UHX or UXA. +RFS,UHXA6-W57A RF,UHXA6W57ARF,6,1.83,,Indeterminate. UHXA unknown could be UHX or UXA. +RFS,UHXA8-W59A RF,UHXA8W59ARF,8,2.44,,Indeterminate. UHXA unknown could be UHX or UXA. +COMMSCOPE,UHXC10-59K RF,UHXC1059KRF,10,3.05,43.2,Typo should be UHX1059KRF +ANDREW,UJX8-59J RF,UJX859JRF,8,2.44,41.3,Typo should be UHX859JRF Using that gain +ERICSSON,UKY 220 11/DC12,UKY22011DC12,4,1.22,35.8,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY 220 12/DC12,UKY22012DC12,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY22012/DC12,UKY22012DC12,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY 220 12/DC12 R2X,UKY22012DC12R2X,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY 220 12/DC15,UKY22012DC15,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY22012/DC15,UKY22012DC15,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY 220 12/DC15 R3X,UKY22012DC15R3X,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY 220 12/DC/ R2X,UKY22012DCR2X,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY 220 12/SC11,UKY22012SC11,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY 220 12/SC15,UKY22012SC15,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY22012/SC15,UKY22012SC15,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY 220 12/SC15 R2X,UKY22012SC15R2X,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY 220 12/SC15_R2X,UKY22012SC15R2X,6,1.83,39.3,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY22013/DC,UKY22013DC,8,2.44,42.1,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY 220 13/DC12,UKY22013DC12,8,2.44,42.1,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY 220 13/DC12 R3X,UKY22013DC12R3X,8,2.44,42.1,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY 220 13/SC12,UKY22013SC12,8,2.44,42.1,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY22013/SC12,UKY22013SC12,8,2.44,42.1,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY22014/DC12,UKY22014DC12,10,3.05,43.5,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ERICSSON,UKY 220 24/SC15,UKY22024SC15,3,0.91,33.9,https://fccid.io/ANATEL/01164-10-01882/Manual/6B919BD1-C385-4160-AEA5-A11D16F3B516/PDF +ANDREW,UMX10-459,UMX10459,10,3.05,43.1,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FUMX12-459.pdf +COMMSCOPE,UMX10-459B,UMX10459B,10,3.05,43.1,Comsearch's Frequency Coordination Database +Andrew Corporation,UMX10-611A LF,UMX10611ALF,10,3.05,42.2,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-1997.pdf +ANDREW,UMX10-611A RF,UMX10611ARF,10,3.05,42.2,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-1997.pdf +Andrew Corporation,UMX10-611A RF,UMX10611ARF,10,3.05,42.2,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-1997.pdf +COMMSCOPE,UMX10-611 RF,UMX10611RF,10,3.05,42.2,https://worldradiohistory.com/Archive-Catalogs/Miscellaneous-Manufacturers/Andrew-Catalog-1997.pdf +ANDREW,UMX10-6477 (6.4)RF,UMX10647764RF,10,3.05,43.4,https://www.datasheet.wiki/pdfviewer?url=https%3A%2F%2Fpdf.datasheet.wiki%2Fdatasheets-1%2Fandrew%2FUMX10-6477.pdf +COMMSCOPE,UMX10-6477 (6.4)RF,UMX10647764RF,10,3.05,43.4,Comsearch's Frequency Coordination Database +ANDREW,UMX12-459,UMX12459,12,3.66,45.3,https://www.datasheet.directory/pdfviewer?url=https%3A%2F%2Fdatasheet.iiic.cc%2Fdatasheets-1%2Fandrew%2FUMX12-459.pdf +COMMSCOPE,UMX12-459,UMX12459,12,3.66,45.3,Comsearch's Frequency Coordination Database +COMMSCOPE,UMX12-459A,UMX12459A,12,3.66,45.3,Comsearch's Frequency Coordination Database +ANDREW,UMX12-59J RF,UMX1259JRF,12,3.66,44.8,Typo should be UHX1259JRF. Using that gain +ALCOMA,UNI3-6,UNI36,3,0.91,32.7,http://www.al-wireless.com/media/document/uni3-6.pdf +ALCOMA,UNI3-6-FS3S,UNI36FS3S,3,0.91,32.7,http://www.al-wireless.com/media/document/uni3-6.pdf +ALCOMA,UNI4-6,UNI46,4,1.22,35.6,http://us.al-wireless.com/media/document/uni4-6.pdf +ALCOMA,UNI4-6-FS3S,UNI46FS3S,4,1.22,35.6,http://us.al-wireless.com/media/document/uni4-6.pdf +ALCOMA,UNI4-6-RF3S,UNI46RF3S,4,1.22,35.6,http://us.al-wireless.com/media/document/uni4-6.pdf +ANDREW,UNX12-59J RF,UNX1259JRF,12,3.66,44.8,Typo should be UHX1259JRF. Using that gain +ANDREW,UNX6-59H LF,UNX659HLF,6,1.83,38.8,Typo should be UHX659HLF. Using that gain. +ANDREW,UNX8-59J RF,UNX859JRF,8,2.44,41.3,Typo should be UHX859JRF Using that gain +GABRIEL,UOF 10-64A LF,UOF1064ALF,10,3.05,44,Comsearch's Frequency Coordination Database +GABRIEL,UOF10-64A LF,UOF1064ALF,10,3.05,44,Comsearch's Frequency Coordination Database +GABRIEL,UOF-10-64A LF,UOF1064ALF,10,3.05,44,Comsearch's Frequency Coordination Database +GABRIEL,UOF10-64B LF,UOF1064BLF,10,3.05,44,Comsearch's Frequency Coordination Database +GABRIEL,UOF10-64CSE(L),UOF1064CSEL,10,3.05,43.7,Comsearch's Frequency Coordination Database +GABRIEL,UOF10-64CSE LF,UOF1064CSELF,10,3.05,43.7,Comsearch's Frequency Coordination Database +GABRIEL,UOF10-64CSE (RF),UOF1064CSERF,10,3.05,43.7,Comsearch's Frequency Coordination Database +GABRIEL,UOF10-64CSE RF,UOF1064CSERF,10,3.05,43.7,Comsearch's Frequency Coordination Database +GABRIEL,UOF6-64DSE RF,UOF664DSERF,6,1.83,39.3,Comsearch's Frequency Coordination Database +GABRIEL,UOF8-64CSE,UOF864CSE,8,2.44,41.8,Comsearch's Frequency Coordination Database +GABRIEL,UOF8-64CSE LF,UOF864CSELF,8,2.44,41.8,Comsearch's Frequency Coordination Database +GABRIEL,UOF8-64CSE LF,UOF864CSELF,8,2.44,41.8,Comsearch's Frequency Coordination Database +GABRIEL,UOF8-64CSE RF,UOF864CSERF,8,2.44,41.8,Comsearch's Frequency Coordination Database +GABRIEL,UOF8-64 LF,UOF864LF,8,2.44,41.9,Comsearch's Frequency Coordination Database +ANDREW,UPX8-59E,UPX859E,8,2.44,41.3,"Typo could be UHX or HPX, both have same gain, so using that. " +COMMSCOPE,UPX8-59E,UPX859E,8,2.44,41.3,"Typo could be UHX or HPX, both have same gain, so using that. " +GABRIEL,US10P-3J23C,US10P3J23C,10,3.05,43.1,Typo should be USR10P3J23C. Using that gain. +RFS,USA6-757A,USA6757A,6,1.83,39,"Typo. Should be this antenna, likely: https://www.rfsworld.com/pim/product/html/UXA6-U57AC" +GABRIEL,USR10P,USR10P,10,3.05,43.1,Comsearch's Frequency Coordination Database +GABRIEL,USR10P-3J23C,USR10P3J23C,10,3.05,43.1,Comsearch's Frequency Coordination Database +GABRIEL,USR10P-59,USR10P59,10,3.05,43.1,Comsearch's Frequency Coordination Database +GABRIEL,USR12P-3J23C,USR12P3J23C,12,3.66,44.7,Comsearch's Frequency Coordination Database +GABRIEL,USR12P-59,USR12P59,12,3.66,44.7,Comsearch's Frequency Coordination Database +GABRIEL,USR6P-59,USR6P59,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,USR6P-59A,USR6P59A,6,1.83,38.6,Comsearch's Frequency Coordination Database +GABRIEL,USR8P-3J23C,USR8P3J23C,8,2.44,41.1,Comsearch's Frequency Coordination Database +GABRIEL,USR8P-59,USR8P59,8,2.44,41.1,Comsearch's Frequency Coordination Database +ANDREW,USX10-6W,USX106W,10,3.05,43.2,https://www.commscope.com/globalassets/digizuite/261414-p360-usx10-6w-external.pdf +COMMSCOPE,USX10-6W,USX106W,10,3.05,43.2,https://www.commscope.com/globalassets/digizuite/261414-p360-usx10-6w-external.pdf +COMMSCOPE,USX10-6W-6GF,USX106W6GF,10,3.05,43.2,https://www.commscope.com/globalassets/digizuite/261414-p360-usx10-6w-external.pdf +COMMSCOPE,USX12-6W,USX126W,12,3.66,45,Comsearch's Frequency Coordination Database +COMMSCOPE,USX4-59A RF,USX459ARF,4,1.22,34.4,"Typo could be HSX, using that gain. " +COMMSCOPE,USX6-59L RF,USX659LRF,6,1.83,38.8,Typo could be UHX using that gain. +ANDREW,USX6-6W,USX66W,6,1.83,38.8,https://www.commscope.com/globalassets/digizuite/261438-p360-usx6-6w-external.pdf +COMMSCOPE,USX6-6W,USX66W,6,1.83,38.8,https://www.commscope.com/globalassets/digizuite/261438-p360-usx6-6w-external.pdf +COMMSCOPE,USX6--6W,USX66W,6,1.83,38.8,https://www.commscope.com/globalassets/digizuite/261438-p360-usx6-6w-external.pdf +COMMSCOPE,USX6-6W-6GR,USX66W6GR,6,1.83,38.8,https://www.commscope.com/globalassets/digizuite/261438-p360-usx6-6w-external.pdf +RFS,USX-6W,USX6W,,,,No diameter available in the model so cannot discern proper gain +ANDREW,USX8-62,USX862,8,2.44,42.4,https://www.commscope.com/globalassets/digizuite/261452-p360-usx8-6w-external.pdf +ANDREW,USX8-6W,USX86W,8,2.44,41.6,https://www.commscope.com/globalassets/digizuite/261452-p360-usx8-6w-external.pdf +COMMSCOPE,USX8-6W,USX86W,8,2.44,41.6,Comsearch's Frequency Coordination Database +COMMSCOPE,USX8--6W,USX86W,8,2.44,41.6,Comsearch's Frequency Coordination Database +COMMSCOPE,USX8+8W,USX88W,8,2.44,,Indeterminate. Band designator 8 is not known. +COMMSCOPE,USX8-8W,USX88W,8,2.44,,Indeterminate. Band designator 8 is not known. +GABRIEL,UW8-5968 SE LF,UW85968SELF,8,2.44,41.6,Typo should be UWB85968SELF. Using that gain. +GABRIEL,UWB10-5968SE LF,UWB105968SELF,10,3.05,43.5,Comsearch's Frequency Coordination Database +GABRIEL,UWB10-5968SE RF,UWB105968SERF,10,3.05,43.5,Comsearch's Frequency Coordination Database +GABRIEL,UWB10-5968(SS)RF,UWB105968SSRF,10,3.05,42.9,Comsearch's Frequency Coordination Database +GABRIEL,UWB6-5968SE RF,UWB65968SERF,6,1.83,39.1,Comsearch's Frequency Coordination Database +GABRIEL,UWB6-5971SE LF,UWB65971SELF,6,1.83,38.7,Comsearch's Frequency Coordination Database +GABRIEL,UWB6-5971SE RF,UWB65971SERF,6,1.83,38.7,Comsearch's Frequency Coordination Database +GABRIEL,UWB8-5968 SE LF,UWB85968SELF,8,2.44,41.6,Comsearch's Frequency Coordination Database +GABRIEL,UWB8-5968SE LF,UWB85968SELF,8,2.44,41.6,Comsearch's Frequency Coordination Database +GABRIEL,UWB8-5968 SE RF,UWB85968SERF,8,2.44,41.6,Comsearch's Frequency Coordination Database +GABRIEL,UWB8-5968SE RF,UWB85968SERF,8,2.44,41.6,Comsearch's Frequency Coordination Database +GABRIEL,UWB8-5971A RF,UWB85971ARF,8,2.44,41.6,Comsearch's Frequency Coordination Database +COMMSCOPE,UX10-59W,UX1059W,10,3.05,43.2,Typo should be UHX1059W. Using that gain. +COMMSCOPE,UX15-59H LF,UX1559HLF,15,4.57,46.4,Typo should be UHX1559HLF. Using that gain. +RFS,UX6-W59B LF,UX6W59BLF,6,1.83,39.1,"Typo, should be UXA6W59BLF. Using that gain. " +CABLEWAVE SYSTEMS,UXA10-59A,UXA1059A,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AD2 +RFS,UXA10-59A,UXA1059A,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +CABLEWAVE SYSTEMS,UXA10-59AC LF,UXA1059ACLF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AD2 +CABLEWAVE SYSTEMS,UXA10-59AC RF,UXA1059ACRF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AD2 +CABLEWAVE SYSTEMS,UXA10-59A LF,UXA1059ALF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AD2 +RFS,UXA10-59A LF,UXA1059ALF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +RFS,UXA10-59A LF,UXA1059ALF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +CABLEWAVE SYSTEMS,UXA10-59A RF,UXA1059ARF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AD2 +RFS,UXA10-59A RF,UXA1059ARF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +RFS,UXA10-59A RF,UXA1059ARF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +COMMSCOPE,UXA10-59B RF,UXA1059BRF,10,3.05,43.2,Comsearch's Frequency Coordination Database +RFS,UXA10-59B RF,UXA1059BRF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +RFS,UXA10-59 LF,UXA1059LF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +CABLEWAVE SYSTEMS,UXA10-59 RF,UXA1059RF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AD2 +RFS,UXA10-59 RF,UXA1059RF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +RFS,UXA10-59W,UXA1059W,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +CABLEWAVE SYSTEMS,UXA10-59W L,UXA1059WL,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AD2 +COMMSCOPE,UXA10-59W RF,UXA1059WRF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +RFS,UXA10-59W RF,UXA1059WRF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +RFS,UXA10-59W-RF,UXA1059WRF,10,3.05,43.2,https://www.rfsworld.com/pim/product/html/UXA10-59AC +CABLEWAVE SYSTEMS,UXA10-65,UXA1065,10,3.05,43.9,https://www.rfsworld.com/pim/product/html/UXA10-65AD2 +RFS,UXA10-65A,UXA1065A,10,3.05,43.9,https://www.rfsworld.com/pim/product/html/UXA10-65AC +RFS,UXA10-65A RF,UXA1065ARF,10,3.05,43.9,https://www.rfsworld.com/pim/product/html/UXA10-65AC +CABLEWAVE SYSTEMS,UXA10-65 RF,UXA1065RF,10,3.05,43.9,https://www.rfsworld.com/pim/product/html/UXA10-65AD2 +RFS,UXA10-U57A,UXA10U57A,10,3.05,43.4,https://www.rfsworld.com/pim/product/pdf/UXA10-U57ACSQGE +RFS,UXA10-U57AC,UXA10U57AC,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/UXA10-U57AC +RFS,UXA10-U57AC RF,UXA10U57ACRF,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/UXA10-U57AC +RFS,UXA10-U57A LF,UXA10U57ALF,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/UXA10-U57AC +RFS,UXA10-U57A RF,UXA10U57ARF,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/UXA10-U57AC +RFS,UXA10-W50A RF,UXA10W50ARF,10,3.05,,Indeterminate. Band designator 50 has multiple options. +RFS,UXA10-W57A RF,UXA10W57ARF,10,3.05,43.4,https://www.rfsworld.com/pim/product/html/UXA10-W57AC +RFS,UXA10-W59A,UXA10W59A,10,3.05,43.5,https://www.rfsworld.com/pim/product/html/UXA10-W59AC +RFS,UXA10-W59A LF,UXA10W59ALF,10,3.05,43.5,https://www.rfsworld.com/pim/product/html/UXA10-W59AC +RFS,UXA10-W59A-LF,UXA10W59ALF,10,3.05,43.5,https://www.rfsworld.com/pim/product/html/UXA10-W59AC +COMMSCOPE,UXA10-W59A RF,UXA10W59ARF,10,3.05,43.5,Comsearch's Frequency Coordination Database +RFS,UXA10-W59A RF,UXA10W59ARF,10,3.05,43.5,https://www.rfsworld.com/pim/product/html/UXA10-W59AC +RFS,UXA10-W59A-RF,UXA10W59ARF,10,3.05,43.5,https://www.rfsworld.com/pim/product/html/UXA10-W59AC +RFS,UXA10-W59 RF,UXA10W59RF,10,3.05,43.5,https://www.rfsworld.com/pim/product/html/UXA10-W59AC +RFS,UXA12-59A,UXA1259A,12,3.66,44.8,https://www.rfsworld.com/pim/product/html/UXA12-59AC +RFS,UXA12-59A LF,UXA1259ALF,12,3.66,44.8,https://www.rfsworld.com/pim/product/html/UXA12-59AC +Radio Frequency Systems,UXA 12 59A RF,UXA1259ARF,12,3.66,44.8,https://www.rfsworld.com/pim/product/html/UXA12-59AC +Radio Frequency Systems,UXA12 59A RF,UXA1259ARF,12,3.66,44.8,https://www.rfsworld.com/pim/product/html/UXA12-59AC +RFS,UXA12-59A RF,UXA1259ARF,12,3.66,44.8,https://www.rfsworld.com/pim/product/html/UXA12-59AC +CABLEWAVE SYSTEMS,UXA12-65,UXA1265,12,3.66,45.6,https://www.rfsworld.com/pim/product/html/UXA12-65AD4 +RFS,UXA12-65A RF,UXA1265ARF,12,3.66,45.6,https://www.rfsworld.com/pim/product/html/UXA12-65AC +RFS,UXA12-U57A RF,UXA12U57ARF,12,3.66,45.1,https://www.rfsworld.com/pim/product/html/UXA12-U57AC +RFS,UXA12-W59,UXA12W59,12,3.66,45.1,https://www.rfsworld.com/pim/product/html/UXA12-W59AC +RFS,UXA12-W59 (P),UXA12W59P,12,3.66,45.1,https://www.rfsworld.com/pim/product/html/UXA12-W59AC +RFS,UXA4?59A,UXA459A,4,1.22,34.5,https://www.rfsworld.com/pim/product/html/UXA4-59AC +RFS,UXA4-59A,UXA459A,4,1.22,34.5,https://www.rfsworld.com/pim/product/html/UXA4-59AC +RFS,UXA4-65A LF,UXA465ALF,4,1.22,35.3,https://www.rfsworld.com/pim/product/html/UXA4-65AC +RFS,UXA6-57A RF,UXA657ARF,6,1.83,,Indeterminate. +RFS,UXA6-59A,UXA659A,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +CABLEWAVE SYSTEMS,UXA6-59 AC LF,UXA659ACLF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59BD2H +CABLEWAVE SYSTEMS,UXA6-59AC LF,UXA659ACLF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59BD2H +CABLEWAVE SYSTEMS,UXA6-59A LF,UXA659ALF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59BD2H +RFS,UXA6-59A LF,UXA659ALF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +CABLEWAVE SYSTEMS,UXA6-59A-LF,UXA659ALF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59BD2H +RFS,UXA6?59A RF,UXA659ARF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA659A RF,UXA659ARF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6-59A RF,UXA659ARF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6-59B LF,UXA659BLF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6-59b LF,UXA659BLF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6-59B RF,UXA659BRF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6-59b rf,UXA659BRF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA659C,UXA659C,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6-59C,UXA659C,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6-59CC RF,UXA659CCRF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6-59C LF,UXA659CLF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6?59C RF,UXA659CRF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6?59C RF,UXA659CRF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +COMMSCOPE,UXA6-59C RF,UXA659CRF,6,1.83,38.7,Comsearch's Frequency Coordination Database +RFS,UXA6-59C RF,UXA659CRF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6-59C-RF,UXA659CRF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6-59 L,UXA659L,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59CC +RFS,UXA6-59 LF,UXA659LF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +Radio Frequency Systems,UXA6-59LF,UXA659LF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6-59R,UXA659R,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +CABLEWAVE SYSTEMS,UXA6-59 RF,UXA659RF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59BD2H +RFS,UXA6-59 RF,UXA659RF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +RFS,UXA6-59WAC RF,UXA659WACRF,6,1.83,38.7,https://alliancecorporation.ca/images/documents/wireless-infrastructure-documents/RFS/Microwave_antennas/Alliance_distributor_RFS_Microwave_Antenna_UXA6-59CC.pdf +ANDREW,UXA6-59W LF,UXA659WLF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59CC +RFS,UXA6-59W LF,UXA659WLF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59ACSQ1E +CABLEWAVE SYSTEMS,UXA6-59W LF C43002,UXA659WLFC43002,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59BD2H +CABLEWAVE SYSTEMS,UXA6-59W RF,UXA659WRF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59BD2H +CABLEWAVE,UXA6-59W RF,UXA659WRF,6,1.83,38.7,https://www.rfsworld.com/pim/product/html/UXA6-59BD2H +RFS,UXA6-65A LF,UXA665ALF,6,1.83,39.7,https://www.rfsworld.com/pim/product/html/UXA6-65BC +RFS,UXA6-65A RF,UXA665ARF,6,1.83,39.7,https://www.rfsworld.com/pim/product/html/UXA6-65BC +RFS,UXA6-65B LF,UXA665BLF,6,1.83,39.7,https://www.rfsworld.com/pim/product/html/UXA6-65BC +RFS,UXA6-65B LF,UXA665BLF,6,1.83,39.7,https://www.rfsworld.com/pim/product/html/UXA6-65BC +RFS,UXA6-65B RF,UXA665BRF,6,1.83,39.7,https://www.rfsworld.com/pim/product/html/UXA6-65BC +RFS,UXA6-65B-RF,UXA665BRF,6,1.83,39.7,https://www.rfsworld.com/pim/product/html/UXA6-65BC +RFS,UXA6-U57A,UXA6U57A,6,1.83,39,https://www.rfsworld.com/pim/product/html/UXA6-U57AC +RFS,UXA6-U57A LF,UXA6U57ALF,6,1.83,39,https://www.rfsworld.com/pim/product/html/UXA6-U57AC +RFS,UXA6-U57A RF,UXA6U57ARF,6,1.83,39,https://www.rfsworld.com/pim/product/html/UXA6-U57AC +RFS,UXA6-U57A RF,UXA6U57ARF,6,1.83,39,https://www.rfsworld.com/pim/product/html/UXA6-U57AC +RFS,UXA6W57A,UXA6W57A,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/UXA6-W57AC +RFS,UXA6-W57A,UXA6W57A,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/UXA6-W57AC +RFS,UXA6-W57A LF,UXA6W57ALF,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/UXA6-W57AC +RFS,UXA6-W57A-LF,UXA6W57ALF,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/UXA6-W57AC +RFS,UXA6?W57A RF,UXA6W57ARF,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/UXA6-W57AC +RFS,UXA6?W57A RF,UXA6W57ARF,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/UXA6-W57AC +RFS,UXA6W57A RF,UXA6W57ARF,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/UXA6-W57AC +COMMSCOPE,UXA6-W57A RF,UXA6W57ARF,6,1.83,38.9,Comsearch's Frequency Coordination Database +RFS,UXA6-W57A RF,UXA6W57ARF,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/UXA6-W57AC +RFS,UXA6-W57 RF,UXA6W57RF,6,1.83,38.9,https://www.rfsworld.com/pim/product/html/UXA6-W57AC +RFS,UXA6-W59A,UXA6W59A,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6W59A LF,UXA6W59ALF,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6-W59A LF,UXA6W59ALF,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6-W59A RF,UXA6W59ARF,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6W59B,UXA6W59B,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6-W59B,UXA6W59B,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6-W59BC,UXA6W59BC,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6-W59B LF,UXA6W59BLF,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6?W59B RF,UXA6W59BRF,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6W59B RF,UXA6W59BRF,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6-W59B RF,UXA6W59BRF,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6-W59B-RF,UXA6W59BRF,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6-W59C RF,UXA6W59CRF,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6-W59 RF,UXA6W59RF,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +RFS,UXA6-W9B RF,UXA6W9BRF,6,1.83,39.1,https://www.rfsworld.com/pim/product/html/UXA6-W59ACSQ1E +CABLEWAVE SYSTEMS,UXA8059 RF S92250,UXA8059RFS92250,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AC2 +RFS,UXA8-56A-RF,UXA856ARF,8,2.44,,Indeterminate. Band designator 56 unknown. +RFS,UXA 8 - 59A,UXA859A,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AC +CABLEWAVE SYSTEMS,UXA8-59A,UXA859A,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +RFS,UXA8-59A,UXA859A,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AC +Cablewave Systems,UXA8-59AC LF,UXA859ACLF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +CABLEWAVE SYSTEMS,UXA8-59AC RF,UXA859ACRF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +RFS,UXA8-59A LF,UXA859ALF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AC +RFS,UXA8-59A LF,UXA859ALF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AC +RFS,UXA8-59A RF,UXA859ARF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AC +RADIO FREQUENCY SYSTEMS,UXA8-59A RF,UXA859ARF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AC +RFS,UXA8-59B,UXA859B,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AC +RFS,UXA8-59B LF,UXA859BLF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AC +COMMSCOPE,UXA8-59B RF,UXA859BRF,8,2.44,41.3,Comsearch's Frequency Coordination Database +RFS,UXA8-59B RF,UXA859BRF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AC +CABLEWAVE SYSTEMS,UXA8-59 LF,UXA859LF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +CABLEWAVE SYSTEMS,UXA8-59LF,UXA859LF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +CABLEWAVE SYSTEMS,UXA8-59-LF,UXA859LF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +CABLEWAVE SYSTEMS,UXA8 59 RF,UXA859RF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AC2 +CABLEWAVE SYSTEMS,UXA8-59 RF,UXA859RF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +RFS,UXA8-59 RF,UXA859RF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AC +CABLEWAVE SYSTEMS,UXA8-59RF,UXA859RF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +CABLEWAVE SYSTEMS,UXA8-59 RF S92250,UXA859RFS92250,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +RFS,UXA8-59W,UXA859W,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AC +CABLEWAVE SYSTEMS,UXA8-59WAC RF,UXA859WACRF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +CABLEWAVE SYSTEMS,UXA8-59W RF,UXA859WRF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +COMMSCOPE,UXA8-59W RF,UXA859WRF,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +CABLEWAVE SYSTEMS,UXA8-59W RF C64007,UXA859WRFC64007,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +CABLEWAVE SYSTEMS,UXA8-59WRF C64007,UXA859WRFC64007,8,2.44,41.3,https://www.rfsworld.com/pim/product/html/UXA8-59AD2H +RFS,UXA8-65A,UXA865A,8,2.44,42.2,https://www.rfsworld.com/pim/product/html/UXA8-65AC +RFS,UXA8-65AC LF,UXA865ACLF,8,2.44,42.2,https://www.rfsworld.com/pim/product/html/UXA8-65AC +RFS,UXA8-65A LF,UXA865ALF,8,2.44,42.2,https://www.rfsworld.com/pim/product/html/UXA8-65AC +RFS,UXA8-65A RF,UXA865ARF,8,2.44,42.2,https://www.rfsworld.com/pim/product/html/UXA8-65AC +RFS,UXA8-65a RF,UXA865ARF,8,2.44,42.2,https://www.rfsworld.com/pim/product/html/UXA8-65AC +RFS,UXA8-65A-RF,UXA865ARF,8,2.44,42.2,https://www.rfsworld.com/pim/product/html/UXA8-65AC +CABLEWAVE SYSTEMS,UXA8-65 LF,UXA865LF,8,2.44,42.5,https://www.rfsworld.com/pim/product/html/UXA8-65AD4 +CABLEWAVE SYSTEMS,UXA8-65 RF,UXA865RF,8,2.44,42.5,https://www.rfsworld.com/pim/product/html/UXA8-65AD4 +RFS,UXA8-U57,UXA8U57,8,2.44,41.6,https://www.rfsworld.com/pim/product/pdf/UXA8-U57AC +RFS,UXA8-U57A,UXA8U57A,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-U57AC +RFS,UXA8-U57AC,UXA8U57AC,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-U57AC +RFS,UXA8-U57AC LF,UXA8U57ACLF,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-U57AC +RFS,UXA8-U57AC RF,UXA8U57ACRF,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-U57AC +RFS,UXA8-U57A LF,UXA8U57ALF,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-U57AC +RFS,UXA8-U57A RF,UXA8U57ARF,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-U57AC +RFS,UXA8-W57A,UXA8W57A,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/UXA8-W57AC +RFS,UXA8-W57A LF,UXA8W57ALF,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/UXA8-W57AC +COMMSCOPE,UXA8-W57A RF,UXA8W57ARF,8,2.44,41.4,Comsearch's Frequency Coordination Database +RFS,UXA8-W57A RF,UXA8W57ARF,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/UXA8-W57AC +RFS,UXA8-W57A RF,UXA8W57ARF,8,2.44,41.4,https://www.rfsworld.com/pim/product/html/UXA8-W57AC +RFS,UXA8-W59A,UXA8W59A,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-W59AC +RFS,UXA8-W59AC,UXA8W59AC,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-W59AC +RFS,UXA8-W59A LF,UXA8W59ALF,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-W59AC +RFS,UXA8-W59A-LF,UXA8W59ALF,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-W59AC +RFS,UXA8-W59A (RF),UXA8W59ARF,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-W59AC +COMMSCOPE,UXA8-W59A RF,UXA8W59ARF,8,2.44,41.6,Comsearch's Frequency Coordination Database +RFS,UXA8-W59A RF,UXA8W59ARF,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-W59AC +RFS,UXA8-W59A-RF,UXA8W59ARF,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-W59AC +RFS,UXA8-WA59A RF,UXA8WA59ARF,8,2.44,41.6,https://www.rfsworld.com/pim/product/html/UXA8-W59AC +RFS,UXA8-WF9A RF,UXA8WF9ARF,8,2.44,41.6,Typo should be UXA8W59ARF. Using that gain. +COMMSCOPE,UXH10-59J RF,UXH1059JRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UXH10-59K RF,UXH1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UXH12-59E LF,UXH1259ELF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UXH6-59L RF,UXH659LRF,6,1.83,38.8,Typo should be UHX659LRF. Using that gain. +ANDREW,UXH8-59H,UXH859H,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,UXH8-59H LF,UXH859HLF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,UXH8-59J RF,UXH859JRF,8,2.44,41.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,VHLP3-6W,VHLP36W,3,0.91,33.3,https://www.commscope.com/globalassets/digizuite/471591-p360-vhlp3-6w-a-external.pdf +ANDREW,VHLP3-6W-4WH,VHLP36W4WH,3,0.91,33.3,https://www.commscope.com/globalassets/digizuite/471588-p360-vhlp3-6w-6wh-a-external.pdf +ANDREW,VHLP3-6WA,VHLP36WA,3,0.91,33.3,https://www.commscope.com/globalassets/digizuite/471591-p360-vhlp3-6w-a-external.pdf +ANDREW,VHLP4-6W,VHLP46W,4,1.22,35,Comsearch's Frequency Coordination Database +COMMSCOPE,VHLP4-6W,VHLP46W,4,1.22,35,Comsearch's Frequency Coordination Database +ANDREW,VHLP4-6W-6WH/B,VHLP46W6WHB,4,1.22,35,Comsearch's Frequency Coordination Database +COMMSCOPE,VHLP4-6W/B,VHLP46WB,4,1.22,35,Comsearch's Frequency Coordination Database +COMMSCOPE,VHLP4-6WB,VHLP46WB,4,1.22,35,Comsearch's Frequency Coordination Database +ANDREW,VHLP4-6W/C,VHLP46WC,4,1.22,35.5,https://www.commscope.com/globalassets/digizuite/471694-p360-vhlp4-6w-c-external.pdf +COMMSCOPE,VHLP4-6WC,VHLP46WC,4,1.22,35.5,https://www.commscope.com/globalassets/digizuite/471694-p360-vhlp4-6w-c-external.pdf +ANDREW,VHLP6-6W,VHLP66W,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/471777-p360-vhlp6-6w-b-external.pdf +COMMSCOPE,VHLP6-6W,VHLP66W,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/471777-p360-vhlp6-6w-b-external.pdf +ANDREW,VHLP6-6W-4GR/A,VHLP66W4GRA,6,1.83,39,https://objects.eanixter.com/PD357641.PDF +ANDREW,VHLP6-6W/A,VHLP66WA,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/471777-p360-vhlp6-6w-b-external.pdf +ANDREW,VHLP6-6WA,VHLP66WA,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/471777-p360-vhlp6-6w-b-external.pdf +COMMSCOPE,VHLP6-6WA,VHLP66WA,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/471777-p360-vhlp6-6w-b-external.pdf +COMMSCOPE,VHLP6-6WA (CAT A),VHLP66WACATA,6,1.83,39.3,Comsearch's Frequency Coordination Database +COMMSCOPE,VHLP6-6WA (CAT B),VHLP66WACATB,6,1.83,39,Comsearch's Frequency Coordination Database +COMMSCOPE,VHLP6-6WA (CATB),VHLP66WACATB,6,1.83,39,Comsearch's Frequency Coordination Database +ANDREW,VHLP6-6WB,VHLP66WB,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/471777-p360-vhlp6-6w-b-external.pdf +COMMSCOPE,VHLP6-6WB,VHLP66WB,6,1.83,39.3,Comsearch's Frequency Coordination Database +COMMSCOPE,vhlp6-6wb,VHLP66WB,6,1.83,39.3,Comsearch's Frequency Coordination Database +ANDREW,VHLP6-6WB (CAT A),VHLP66WBCATA,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/471777-p360-vhlp6-6w-b-external.pdf +COMMSCOPE,VHLP6-6WB (CAT A),VHLP66WBCATA,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/471777-p360-vhlp6-6w-b-external.pdf +ANDREW,VHLP6-6W-SE1B,VHLP66WSE1B,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/471777-p360-vhlp6-6w-b-external.pdf +COMMSCOPE,VHLP6-WA,VHLP6WA,6,1.83,,No band indicated in model so cannot determine gain. +COMMSCOPE,VHLP6-^WB,VHLP6WB,6,1.83,,No band indicated in model so cannot determine gain. +COMMSCOPE,VHLPA4-6WB80.2,VHLPA46WB802,4,1.22,35,Typo should be VHLP46WB +ANDREW,VHLPX3-6W,VHLPX36W,3,0.91,33.3,https://www.commscope.com/globalassets/digizuite/472015-p360-vhlpx3-6w-a-external.pdf +Commscope,VHLPX3-6WA,VHLPX36WA,3,1.22,33.3,https://www.commscope.com/globalassets/digizuite/472015-p360-vhlpx3-6w-a-external.pdf +COMMSCOPE,VHLPX4?6W,VHLPX46W,4,1.22,35,Comsearch's Frequency Coordination Database +ANDREW,VHLPX4-6W,VHLPX46W,4,1.22,35,Comsearch's Frequency Coordination Database +COMMSCOPE,VHLPX4-6W,VHLPX46W,4,1.22,35,Comsearch's Frequency Coordination Database +ANDREW,VHLPX4-6W-6WH/C,VHLPX46W6WHC,4,1.22,35,Comsearch's Frequency Coordination Database +COMMSCOPE,VHLPX4-6W-6WH/C,VHLPX46W6WHC,4,1.22,35,Comsearch's Frequency Coordination Database +ANDREW,VHLPX4-6WB,VHLPX46WB,4,1.22,35,Comsearch's Frequency Coordination Database +COMMSCOPE,VHLPX4-6WB,VHLPX46WB,4,1.22,35,Comsearch's Frequency Coordination Database +COMMSCOPE,VHLPX4?6WC,VHLPX46WC,4,1.22,35.5,Comsearch's Frequency Coordination Database +ANDREW,VHLPX4-6W/C,VHLPX46WC,4,1.22,35.5,Comsearch's Frequency Coordination Database +COMMSCOPE,VHLPX4-6WC,VHLPX46WC,4,1.22,35.5,Comsearch's Frequency Coordination Database +ANDREW,VHLPX606W,VHLPX606W,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/472152-p360-vhlpx6-6w-b-external.pdf +ANDREW,VHLPX6-6W,VHLPX66W,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/472152-p360-vhlpx6-6w-b-external.pdf +COMMSCOPE,VHLPX6-6W,VHLPX66W,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/472152-p360-vhlpx6-6w-b-external.pdf +ANDREW,VHLPX6-6W-6WH/A,VHLPX66W6WHA,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/472149-p360-vhlpx6-6w-6wh-b-external.pdf +COMMSCOPE,VHLPX6-6W-6WH/A,VHLPX66W6WHA,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/472152-p360-vhlpx6-6w-b-external.pdf +COMMSCOPE,VHLPX6-6WA,VHLPX66WA,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/472152-p360-vhlpx6-6w-b-external.pdf +COMMSCOPE,VHLPX6-6WA (CAT B ),VHLPX66WACATB,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/472152-p360-vhlpx6-6w-b-external.pdf +COMMSCOPE,VHLPX6-6WA (CAT B),VHLPX66WACATB,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/472152-p360-vhlpx6-6w-b-external.pdf +COMMSCOPE,VHLPX6?6WB,VHLPX66WB,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/472152-p360-vhlpx6-6w-b-external.pdf +Commscope,VHLPX66WB,VHLPX66WB,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/472152-p360-vhlpx6-6w-b-external.pdf +ANDREW,VHLPX6-6WB,VHLPX66WB,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/472152-p360-vhlpx6-6w-b-external.pdf +COMMSCOPE,VHLPX6-6WB,VHLPX66WB,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/472152-p360-vhlpx6-6w-b-external.pdf +ANDREW,VHP4-107,VHP4107,,,,Model does not belong in this band. +ANDREW,VHP4-107A,VHP4107A,,,,Model does not belong in this band. +ANDREW,VHP6-107,VHP6107,,,,Model does not belong in this band. +ANDREW,VHPX10-6W,VHPX106W,10,3.05,43.5,https://www.launch3telecom.com/commscopeandrew/vhpx106w.html +COMMSCOPE,VHPX10-6W,VHPX106W,10,3.05,43.5,Comsearch's Frequency Coordination Database +COMMSCOPE,VHPX12-6W,VHPX126W,12,3.66,45.4,Comsearch's Frequency Coordination Database +COMMSCOPE,VHPX8-6W,VHPX86W,8,2.44,42.1,Comsearch's Frequency Coordination Database +,,VLHP66WB,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/471777-p360-vhlp6-6w-b-external.pdf +COMMSCOPE,VLHP6-6WB,VLHP66WB,6,1.83,39.3,https://www.commscope.com/globalassets/digizuite/471777-p360-vhlp6-6w-b-external.pdf +COMMSCOPE,WEHP6-59K,WEHP659K,6,1.83,38.9,Comsearch's Frequency Coordination Database +COMMSCOPE,WEUHX10-59K RF,WEUHX1059KRF,10,3.05,43.2,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,WHP6-59J,WHP659J,6,1.83,38.9,Comsearch's Frequency Coordination Database +COMMSCOPE,WHP6-59WC,WHP659WC,6,1.83,39.4,Comsearch's Frequency Coordination Database +ANDREW,WHP8-59F,WHP859F,8,2.44,41.5,Comsearch's Frequency Coordination Database +COMMSCOPE,WHP8-59F,WHP859F,8,2.44,41.5,Comsearch's Frequency Coordination Database +ANDREW,WPAR6-59B,WPAR659B,6,1.83,38.2,Comsearch's Frequency Coordination Database +COMMSCOPE,WPAR6-59B,WPAR659B,6,1.83,38.2,Comsearch's Frequency Coordination Database +COMMSCOPE,WPAR6-59WA RF,WPAR659WARF,6,1.83,38.7,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +ANDREW,WPAR6-65B RF,WPAR665BRF,6,1.83,38.8,Comsearch's Frequency Coordination Database +ANDREW,WPAR8-59A,WPAR859A,8,2.44,40.8,Comsearch's Frequency Coordination Database +COMMSCOPE,WPAR8-59A,WPAR859A,8,2.44,40.8,Comsearch's Frequency Coordination Database +COMMSCOPE,WUHP6-59WB RF,WUHP659WBRF,6,1.83,39.3,Comsearch's Frequency Coordination Database +COMMSCOPE,WUHX12-59W RF,WUHX1259WRF,12,3.66,44.8,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +COMMSCOPE,WUHX15-59H RF,WUHX1559HRF,15,4.57,46.4,Comsearch's Frequency Coordination Database +COMMSCOPE,WUHX6-59L LF,WUHX659LLF,6,1.83,38.8,Comsearch's Frequency Coordination Database +COMMSCOPE,WUHX6-59L RF,WUHX659LRF,6,1.83,38.8,Comsearch's Frequency Coordination Database +COMMSCOPE,WUHX6-59WB RF,WUHX659WBRF,6,1.83,38.8,Comsearch's Frequency Coordination Database +COMMSCOPE,WUHX8-59J RF,WUHX859JRF,8,2.44,41.3,https://www.digchip.com/datasheets/parts/datasheet/3751/HP12-59E-pdf.php +RFS,WXA6-W57A RF,WXA6W57ARF,6,1.83,38.9,Typo should be UXA6W57ARF. Using that gain +RFS,WXA8-W57A RF,WXA8W57ARF,8,2.44,41.4,Typo should be UXA8W57ARF. Using that gain. \ No newline at end of file diff --git a/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/billboard_reflector.csv b/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/billboard_reflector.csv new file mode 100644 index 0000000..75b7199 --- /dev/null +++ b/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/billboard_reflector.csv @@ -0,0 +1,218 @@ +manufacturer,antennaModel,height_ft,width_ft,height_m,width_m,notes +ALL MANUFA CTURERS,6X8 FT PASSIVE,6,8,1.83,2.44, +ALL MANUFACTURERS,6X8 FT PASSIVE,6,8,1.83,2.44, +MICROFLECT,6 X 8 ft,6,8,1.83,2.44, +ALL,0810RF,8,10,2.44,3.05, +ALL MANUFACTURERS,0810RF,8,10,2.44,3.05, +ALL MANUFACTURERS,8 x10,8,10,2.44,3.05, +ALL MANUFACTURERS,8' X10' Passive,8,10,2.44,3.05, +ALL MANUFACTURERS,8x10 Foot Passive,8,10,2.44,3.05, +ALL MANUFACTURERS,8x10 Ft Passive,8,10,2.44,3.05, +ALL MANUFACTURERS,8X10 FT PASSIVE`,8,10,2.44,3.05, +ALL MANUFACTURERS,8X10 Ft. Passive,8,10,2.44,3.05, +ALL MANUFACTURES,8' X 10' Passive,8,10,2.44,3.05, +ALL MANUFACTURES,8X10 FT PASSIVE,8,10,2.44,3.05, +MICROFLECT,0810RF,8,10,2.44,3.05, +MICROFLECT,2.4m x 3.0 m,8,10,2.44,3.05, +MICROFLECT,2.4m x 3.0m,8,10,2.44,3.05, +MICROFLECT,8' x 10',8,10,2.44,3.05, +MICROFLECT,8 x 10 Ft,8,10,2.44,3.05, +MICROFLECT,8x10 Ft,8,10,2.44,3.05, +NA,8 X 10,8,10,2.44,3.05, +PASSIVE 8X10,0810RF,8,10,2.44,3.05, +ALL MANUFACTURERS,0812RF,8,12,2.44,3.66,From Peter E's 06062021 data +ALL MANUFACTURERS,8 x 12,8,12,2.44,3.66, +ALL MANUFACTURERS,8 x 12 FT PASSIVE,8,12,2.44,3.66, +ALL MANUFACTURERS,8 x 12 PASSIVE,8,12,2.44,3.66, +ALL MANUFACTURERS,8 x 12FT PASSIVE,8,12,2.44,3.66, +ALL MANUFACTURERS,8X12,8,12,2.44,3.66, +ALL MANUFACTURERS,8X12 FT PASSIVE,8,12,2.44,3.66, +ALL MANUFACTURERS,8X12FT PASSIVE,8,12,2.44,3.66, +ALL MANUFACTURERS,8X12FTPassive,8,12,2.44,3.66, +ALL MANUFACTURERS,Passive 08 x 12,8,12,2.44,3.66, +ALL MANUFACTURERS,PASSIVE 08X12,8,12,2.44,3.66, +ALL MANUFACTURES,8X12 FT PASSIVE,8,12,2.44,3.66, +MICROFLECT,812,8,12,2.44,3.66, +MICROFLECT,0812,8,12,2.44,3.66, +MICROFLECT,2.4m x 3.7m,8,12,2.44,3.66, +MICROFLECT,8x12 Ft. Passive,8,12,2.44,3.66, +MICROFLECT,Microflect 8X12,8,12,2.44,3.66, +MICROFLECT,RTM812,8,12,2.44,3.66, +VALMONT,0812-8,8,12,2.44,3.66, +VALMONT MICROFLECT,RTM812,8,12,2.44,3.66, +ALL MANUFACTURERS,8X19 FT PASSIVE,8,19,2.44,5.79, +ALL MANUFACTURERS,10 X 15 FT Passive,10,15,3.05,4.57, +ALL MANUFACTURERS,1015RF,10,15,3.05,4.57, +ALL MANUFACTURERS,10X15 FT PASSIVE,10,15,3.05,4.57, +ALL,10x16 FT PASSIVE,10,16,3.05,4.88, +ALL,Passive 10x16,10,16,3.05,4.88, +ALL MANUFACTURER,10X16,10,16,3.05,4.88, +ALL MANUFACTURERS,10 x 16 FT PASSIVE,10,16,3.05,4.88, +ALL MANUFACTURERS,10 x16 ' PASSIVE,10,16,3.05,4.88, +ALL MANUFACTURERS,1016RF,10,16,3.05,4.88, +ALL MANUFACTURERS,10x16,10,16,3.05,4.88, +ALL MANUFACTURERS,10X16 FT PASSIVE,10,16,3.05,4.88, +ALL MANUFACTURERS,PASSIVE 10 X 16,10,16,3.05,4.88, +ALL MANUFACTURERS,Passive 10x16,10,16,3.05,4.88, +ALL MANUFACTURES,10 X 16 FT PASSIVE,10,16,3.05,4.88, +ALL MODELS,10x16 FT Passive,10,16,3.05,4.88, +MICORFLECT,P1016,10,16,3.05,4.88, +MICROFLECT,1016,10,16,3.05,4.88, +MICROFLECT,1016RF,10,16,3.05,4.88, +MICROFLECT,10x16,10,16,3.05,4.88, +MICROFLECT,3.0m x 4.9m,10,16,3.05,4.88, +MICROFLECT,P1016,10,16,3.05,4.88, +MICROFLECT,PR3x5,10,16,3.05,4.88, +MICROFLECT COMPANY,1016RF,10,16,3.05,4.88, +PASSIVE REFLECTOR,10 FOOT X 16 FOOT,10,16,3.05,4.88, +PASSIVE REFLECTOR,10 X 16 FOOT,10,16,3.05,4.88, +ALL MANUFACTURERS,PASSIVE 10 X 18,10,18,3.05,5.49, +ALL MANUFACTURERS,10X24 FT PASSIVE,10,24,3.05,7.32, +ALL MANUFACTURERES,Passive 12 x 16 foot,12,16,3.66,4.88, +ALL MANUFACTURERS,1216 RF,12,16,3.66,4.88, +ALL MANUFACTURERS,12FT X16FT,12,16,3.66,4.88, +ALL MANUFACTURERS,12X16 FT PASSIVE,12,16,3.66,4.88, +ALL MANUFACTURERS,Passive 12 x 16 foot,12,16,3.66,4.88, +GENERIC PASSIVE,12 x 16,12,16,3.66,4.88, +MICROFLECT,12 X 16,12,16,3.66,4.88, +MICROFLECT,12 X 16 Passive,12,16,3.66,4.88, +MICROFLEX,12-16,12,16,3.66,4.88, +VALMONT,1216-8,12,16,3.66,4.88, +ALL,Passive 14x16,14,16,4.27,4.88, +ALL MANAUFACTURERS,14x16 FT PASSIVE,14,16,4.27,4.88, +ALL MANUFACTURERS,14 x 16,14,16,4.27,4.88, +ALL MANUFACTURERS,14 x 16 FT PASSIVE,14,16,4.27,4.88, +ALL MANUFACTURERS,1416RF,14,16,4.27,4.88, +ALL MANUFACTURERS,14X16 FT PASSIVE,14,16,4.27,4.88, +ALL MANUFACTURERS,14X16FT PASSIVE,14,16,4.27,4.88, +ALL MANUFACTURERS,PASSIVE 14 X 16,14,16,4.27,4.88, +ALL MANUFACTURERS,PASSIVE 14 X 16 FT,14,16,4.27,4.88, +MICROFLECT,14' x 16',14,16,4.27,4.88, +MICROFLECT,1416 8,14,16,4.27,4.88, +MICROFLECT,1416-8,14,16,4.27,4.88, +MICROFLECT,4.3m x 4.9m,14,16,4.27,4.88, +MTS,14 x 16 PR,14,16,4.27,4.88, +16 X 20 FT PASSIVE,All Manufactures,16,20,4.88,6.1, +16 X20 FT PASSIVE,All Manufacturers,16,20,4.88,6.1, +ALL,16X20 ft passive,16,20,4.88,6.1, +ALL MANUFACTURERS,16 x 20,16,20,4.88,6.1, +ALL MANUFACTURERS,16 x 20 FT PASSIVE,16,20,4.88,6.1, +ALL MANUFACTURERS,1620 RF,16,20,4.88,6.1, +ALL MANUFACTURERS,1620RF,16,20,4.88,6.1, +ALL MANUFACTURERS,16x20,16,20,4.88,6.1, +ALL MANUFACTURERS,16X20 FT PASSIVE,16,20,4.88,6.1, +ALL MANUFACTURERS,16x20 PASSIVE,16,20,4.88,6.1, +ALL MANUFACTURERS,16X20FTPassive,16,20,4.88,6.1, +ALL MANUFACTURERS,Passive 16 X 20 feet,16,20,4.88,6.1, +ALL MANUFACTURERS,Passive 16 X 20 Ft,16,20,4.88,6.1, +ALL MANUFACTURERS,Passive 16 X 20,16,20,4.88,6.1, +ALL MANUFACTURERS,PASSIVE 4.9 X 6.1,16,20,4.88,6.1, +ALL MANUFACTURES,16X20,16,20,4.88,6.1, +ALL MANUFACTURES,16X20 FT PASSIVE,16,20,4.88,6.1, +ALL MANUFACTURES,16x20 Passive,16,20,4.88,6.1, +ALL MANUFATURERS,16 x 20 FT PASSIVE,16,20,4.88,6.1, +MICROFLECT,1620,16,20,4.88,6.1, +MICROFLECT,1620-15,16,20,4.88,6.1, +MICROFLECT,1620-8,16,20,4.88,6.1, +MICROFLECT,1620RF,16,20,4.88,6.1, +MICROFLECT,4.9m x 6.1m,16,20,4.88,6.1, +MTS SERVICE,4.9m x 6.1m,16,20,4.88,6.1, +VELMONT,4.9m x 6.1m,16,20,4.88,6.1, +ALL,16x24 FT PASSIVE,16,24,4.88,7.32, +ALL AMANUFACTURERS,16X24 FT PASSIVE,16,24,4.88,7.32, +ALL MANUFACTURERS,1624RF,16,24,4.88,7.32, +ALL MANUFACTURERS,16X24,16,24,4.88,7.32, +ALL MANUFACTURERS,16X24 FT PASSIVE,16,24,4.88,7.32, +ALL MANUFACTURERS,Passive 16x24,16,24,4.88,7.32, +ALL MANUFACTURES,16X24 FT PASSIVE,16,24,4.88,7.32, +ALL MAUNFACTURES,16X24 FT PASSIVE,16,24,4.88,7.32, +ALL NAMUFACTURERS,16X24 FT PASSIVE,16,24,4.88,7.32, +GENERIC PASSIVE,16x24,16,24,4.88,7.32, +MELI DITCH,16 X 24,16,24,4.88,7.32, +MICROFLECT,16' x 24',16,24,4.88,7.32, +MICROFLECT,1624RF,16,24,4.88,7.32, +ALL MANUFACTURERS,202RF,20,2,6.10,0.61, +ALL,20X24 FT Passive,20,24,6.10,7.32, +ALL MANUFACTRURES,20 X 24 FDt Passive,20,24,6.10,7.32, +ALL MANUFACTURER,20X24 FT PASSIVE,20,24,6.10,7.32, +ALL MANUFACTURERE,20 by 24 Passive,20,24,6.10,7.32, +ALL MANUFACTURERS,20 X 24,20,24,6.10,7.32, +ALL MANUFACTURERS,20 x 24 ' Passive,20,24,6.10,7.32, +ALL MANUFACTURERS,20' x 24' Passive,20,24,6.10,7.32, +ALL MANUFACTURERS,20 X24 Passive,20,24,6.10,7.32, +ALL MANUFACTURERS,2024RF,20,24,6.10,7.32, +ALL MANUFACTURERS,20X24,20,24,6.10,7.32, +ALL MANUFACTURERS,20X24 FOOT PASSIVE,20,24,6.10,7.32, +ALL MANUFACTURERS,20X24 FT PASSIVE,20,24,6.10,7.32, +ALL MANUFACTURERS,PASSIVE 20 X 24,20,24,6.10,7.32, +ALL MANUFACTURES,20 x 24 FT PASSIVE,20,24,6.10,7.32, +ALL MANUFACTURES,20 X 24 PASSIVE,20,24,6.10,7.32, +ALL MANUFACTURES,20X24 FT PASSIVE,20,24,6.10,7.32, +ALL MANUFACTURES,6.1m x 7.3m,20,24,6.10,7.32, +ALL MANUFACTURRES,20 X 24 Ft Passive,20,24,6.10,7.32, +Commscope,20x24 passive,20,24,6.10,7.32, +GENERIC PASSIVE,20x24,20,24,6.10,7.32, +MICROFLECT,20' x 24',20,24,6.10,7.32, +MICROFLECT,6.1m x 7.3m,20,24,6.10,7.32, +MICROFLECT,PR20x24,20,24,6.10,7.32, +MICROFLECT / VALMONT,2024-8 (20' x 24')),20,24,6.10,7.32, +MICROFLEX,20-24,20,24,6.10,7.32, +PASSIVE REFLECTOR,20 FOOT X 24 FOOT,20,24,6.10,7.32, +VALMONT,2024,20,24,6.10,7.32, +VALMONT MICROFLECT,20 x 24 Ft Passive,20,24,6.10,7.32, +All Manufacturers,2028RF,20,28,6.10,8.53,From Peter E's 06062021 data +ALL MANUFACTURERS,20 X 30 FT PASSIVE,20,30,6.10,9.14, +ALL MANUFACTURERS,2030RF,20,30,6.10,9.14, +ALL MANUFACTURERS,20X30 FT PASSIVE,20,30,6.10,9.14, +ALL MANUFACTURES,20 X 30 FT PASSIVE,20,30,6.10,9.14, +ALL MANUFACTURERS,20 X 32,20,32,6.10,9.75, +ALL MANUFACTURERS,20 X 32 Ft,20,32,6.10,9.75, +ALL MANUFACTURERS,20 X 32 FT Passive,20,32,6.10,9.75, +ALL MANUFACTURERS,2032RF,20,32,6.10,9.75, +ALL MANUFACTURERS,20X32 FT PASSIVE,20,32,6.10,9.75, +ALL MANUFACTURERS,PASSIVE 20 x 32,20,32,6.10,9.75, +MICROFLECT,2032-15,20,32,6.10,9.75, +MICROFLECT,2032-15A,20,32,6.10,9.75, +ALL MANUFACTURERS,24 X 30,24,30,7.32,9.14, +ALL MANUFACTURERS,24 X 30 Ft,24,30,7.32,9.14, +ALL MANUFACTURERS,24 X 30 Ft Passive,24,30,7.32,9.14, +ALL MANUFACTURERS,24 X 30 Ft Passvie,24,30,7.32,9.14, +ALL MANUFACTURERS,24 X 30 Passive,24,30,7.32,9.14, +ALL MANUFACTURERS,24.30 ft Passive,24,30,7.32,9.14, +ALL MANUFACTURERS,2430RF,24,30,7.32,9.14, +ALL MANUFACTURERS,24x30,24,30,7.32,9.14, +ALL MANUFACTURERS,24X30 FT PASSIVE,24,30,7.32,9.14, +ALL MANUFACTURERS,24X30 Ft. Passive,24,30,7.32,9.14, +ALL MANUFACTURERS,24X30 PASSIVE,24,30,7.32,9.14, +ALL MANUFACTURERS,7.3 X 9.1 M PASSIVE,24,30,7.32,9.14, +ALL MANUFACTURERS,7.3 X 9.1 M. PASSIVE,24,30,7.32,9.14, +ALL MANUFACTURERS,Passive 24 x 30,24,30,7.32,9.14, +ALL MANUFACTURERS,Passive 24 x 30,24,30,7.32,9.14, +ALL MANUFACTURES,24 X 30,24,30,7.32,9.14, +ALL MANUFACTURES,2430RF,24,30,7.32,9.14, +ALL MANUFATURERS,24x30 FT Passive,24,30,7.32,9.14, +ALL MANURFACTURES,24 x 30,24,30,7.32,9.14, +MICROFLECT,24-30,24,30,7.32,9.14, +MICROFLECT,2430-15,24,30,7.32,9.14, +MICROFLECT,2430-8,24,30,7.32,9.14, +MICROFLECT,24X30 FT PASSIVE,24,30,7.32,9.14, +MICROFLECT,7.3m x 9.1m,24,30,7.32,9.14, +N/A,PASSIVE 24 X 30,24,30,7.32,9.14, +VALMONT,24 X 30 FT PASSIVE,24,30,7.32,9.14, +VALMONT/MICROFLECT,2430RF,24,30,7.32,9.14, +ALL MANUFACTURERS,3032RF,30,32,9.14,9.75, +ALL MANUFACTURERS,30X32 FT PASSIVE,30,32,9.14,9.75, +MICROFLECT,30-32,30,32,9.14,9.75, +MICROFLECT,3032RF,30,32,9.14,9.75, +VALMONT MICROFLECT,30X32 FT PASSIVE,30,32,9.14,9.75, +ALL MANUFACRURERS,3040RF,30,40,9.14,12.19, +ALL MANUFACTURERS,30 X 40 Passive,30,40,9.14,12.19, +ALL MANUFACTURERS,3040RF,30,40,9.14,12.19, +ALL MANUFACTURERS,30X40 FT PASSIVE,30,40,9.14,12.19, +ALL MANUFACTURES,30 X 40 Passive,30,40,9.14,12.19, +VALMONT STRUCTURES,3040-8,30,40,9.14,12.19, +ALL MANUFACTURERS,30X48 FT PASSIVE,30,48,9.14,14.63, +ALL MANUFACTURERES,40x50 FT PASSIVE,40,50,12.19,15.24, +ALL MANUFACTURERS,4050RF,40,50,12.19,15.24, +ALL MANUFACTURERS,40x50 FT PASSIVE,40,50,12.19,15.24, +ALL MANUFATURERS,4050RF,40,50,12.19,15.24, diff --git a/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/category_b1_antennas.csv b/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/category_b1_antennas.csv new file mode 100644 index 0000000..958528d --- /dev/null +++ b/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/category_b1_antennas.csv @@ -0,0 +1,68 @@ +manufacturerNormalized,antennaModelPrefix,note +RFS/Cablewave,DA659, +RFS/Cablewave,DA665, +RFS/Cablewave,DA6W57, +RFS/Cablewave,DAX1065, +RFS/Cablewave,DAX665, +RFS/Cablewave,DAX865, +Gabriel Electronics,DD664DSE, +Commscope/Andrew,HDH1065, +Commscope/Andrew,HDH665, +Commscope/Andrew,HDH865, +Commscope/Andrew,HDV665, +Commscope/Andrew,HDV865, +Commscope/Andrew,HP1057W, +Commscope/Andrew,HP657W, +Commscope/Andrew,HP857W, +Commscope/Andrew,P1059, +Commscope/Andrew,P1065, +Commscope/Andrew,P1259, +Commscope/Andrew,P1265, +Commscope/Andrew,P1559, +Commscope/Andrew,P1565, +Commscope/Andrew,P657W, +Commscope/Andrew,P659,"P6-59. This prefix also matches the Mark Antenna P-6596, also a category B1 antenna. " +Commscope/Andrew,P665, +Commscope/Andrew,P857W, +Commscope/Andrew,P859, +Commscope/Andrew,P865, +RFS/Cablewave,PA1059, +RFS/Cablewave,PA1065, +RFS/Cablewave,PA10W57, +RFS/Cablewave,PA10W59, +RFS/Cablewave,PA1259, +RFS/Cablewave,PA1265, +RFS/Cablewave,PA12W59, +RFS/Cablewave,PA659, +RFS/Cablewave,PA665, +RFS/Cablewave,PA6W57, +RFS/Cablewave,PA6W59, +RFS/Cablewave,PA859, +RFS/Cablewave,PA865, +RFS/Cablewave,PA8W57, +RFS/Cablewave,PA8W59, +Commscope/Andrew,PL1057W, +Commscope/Andrew,PL1059, +Commscope/Andrew,PL1065, +Commscope/Andrew,PL1259, +Commscope/Andrew,PL1265, +Commscope/Andrew,PL1559, +Commscope/Andrew,PL1565, +Commscope/Andrew,PL657W, +Commscope/Andrew,PL659, +Commscope/Andrew,PL665, +Commscope/Andrew,PL857W, +Commscope/Andrew,PL859, +Commscope/Andrew,PL865, +Commscope/Andrew,PXL1059, +Commscope/Andrew,PXL1065, +Commscope/Andrew,PXL1259, +Commscope/Andrew,PXL1265, +Commscope/Andrew,PXL659, +Commscope/Andrew,PXL665, +Commscope/Andrew,PXL859, +Commscope/Andrew,PXL865, +Gabriel Electronics,RFD8264BSE, +Radiowaves,SP659, +Radiowaves,SP664, +Radiowaves,SP859, diff --git a/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/fcc_fixed_service_channelization.csv b/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/fcc_fixed_service_channelization.csv new file mode 100644 index 0000000..9007dcf --- /dev/null +++ b/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/fcc_fixed_service_channelization.csv @@ -0,0 +1,777 @@ +channelFrequency,channelBandwidth,Notes +5925.225,0.4, +5925.425,0.8, +5925.625,1.25, +5926.05,0.4, +5926.25,2.5, +5926.45,0.4, +5926.875,1.25, +5927.075,0.8, +5927.275,0.4, +5927.725,0.4, +5927.925,0.8, +5928.125,1.25, +5928.55,0.4, +5928.75,2.5, +5928.95,0.4, +5929.375,1.25, +5929.575,0.8, +5929.775,0.4, +5935.32,10, +5945.2,30, +5955.08,10, +5960.025,60, +5964.97,10, +5974.85,30, +5984.73,10, +5994.62,10, +6004.5,30, +6014.38,10, +6019.325,60, +6024.27,10, +6034.15,30, +6044.03,10, +6053.92,10, +6063.8,30, +6073.68,10, +6078.625,60, +6083.57,10, +6093.45,30, +6103.33,10, +6108.893,1.25, +6109.51,2.5, +6110.128,1.25, +6110.75,5, +6111.364,3.75, +6111.981,2.5, +6112.599,1.25, +6113.22,10, +6113.834,1.25, +6114.452,2.5, +6115.07,1.25, +6115.69,5, +6116.305,3.75, +6116.394,2.5, +6116.923,2.5, +6117.541,1.25, +6118.776,1.25, +6120.011,1.25, +6120.63,5, +6121.247,3.75, +6121.865,2.5, +6122.482,1.25, +6123.1,30, +6123.718,1.25, +6124.335,2.5, +6124.953,1.25, +6125.57,5, +6126.189,3.75, +6126.806,2.5, +6127.424,1.25, +6128.659,1.25, +6129.277,2.5, +6129.895,1.25, +6130.51,5, +6131.13,3.75, +6131.748,2.5, +6132.366,1.25, +6132.98,10, +6133.601,1.25, +6134.219,2.5, +6134.836,1.25, +6135.45,5, +6136.072,3.75, +6136.69,2.5, +6137.307,1.25, +6137.925,60, +6138.543,1.25, +6139.16,2.5, +6139.778,1.25, +6140.4,5, +6141.014,3.75, +6141.631,2.5, +6142.249,1.25, +6142.87,10, +6143.484,1.25, +6144.102,2.5, +6144.72,1.25, +6145.34,5, +6145.955,3.75, +6146.573,2.5, +6147.191,1.25, +6148.426,1.25, +6149.044,2.5, +6149.661,1.25, +6150.28,5, +6150.897,3.75, +6151.515,2.5, +6152.132,1.25, +6152.75,30, +6153.368,1.25, +6153.985,2.5, +6154.603,1.25, +6155.22,5, +6155.839,3.75, +6156.456,2.5, +6157.074,1.25, +6158.309,1.25, +6158.927,2.5, +6159.545,1.25, +6160.16,5, +6160.78,3.75, +6161.398,2.5, +6162.016,1.25, +6162.63,10, +6163.251,1.25, +6163.869,2.5, +6164.486,1.25, +6165.1,5, +6165.722,3.75, +6166.34,2.5, +6166.957,1.25, +6168.35,0.4, +6168.55,0.8, +6168.75,1.25, +6169.175,0.4, +6169.375,2.5, +6169.575,0.4, +6170,1.25, +6170.2,0.8, +6170.4,0.4, +6170.85,0.4, +6171.05,0.8, +6171.25,1.25, +6171.675,0.4, +6171.875,2.5, +6172.075,0.4, +6172.5,1.25, +6172.7,0.8, +6172.9,0.4, +6177.1,0.4, +6177.3,0.8, +6177.5,1.25, +6177.925,0.4, +6178.125,2.5, +6178.325,0.4, +6178.75,1.25, +6178.95,0.8, +6179.15,0.4, +6179.6,0.4, +6179.8,0.8, +6180,1.25, +6180.425,0.4, +6180.625,2.5, +6180.825,0.4, +6181.25,1.25, +6181.45,0.8, +6181.65,0.4, +6187.36,10, +6197.24,30, +6207.12,10, +6212.065,60, +6217.01,10, +6226.89,30, +6236.77,10, +6246.66,10, +6256.54,30, +6266.42,10, +6271.365,60, +6276.31,10, +6286.19,30, +6296.07,10, +6305.96,10, +6315.84,30, +6325.72,10, +6330.665,60, +6335.61,10, +6345.49,30, +6355.37,10, +6360.993,1.25, +6361.55,2.5, +6362.168,1.25, +6362.79,5, +6363.404,3.75, +6364.021,2.5, +6364.639,1.25, +6365.26,10, +6365.874,1.25, +6366.492,2.5, +6367.11,1.25, +6367.73,5, +6368.345,3.75, +6368.963,2.5, +6369.581,1.25, +6370.816,1.25, +6371.434,2.5, +6372.051,1.25, +6372.67,5, +6373.287,3.75, +6373.905,2.5, +6374.522,1.25, +6375.14,30, +6375.758,1.25, +6376.375,2.5, +6376.993,1.25, +6377.61,5, +6378.229,3.75, +6378.846,2.5, +6379.464,1.25, +6380.699,1.25, +6381.317,2.5, +6381.935,1.25, +6382.55,5, +6383.17,3.75, +6383.788,2.5, +6384.406,1.25, +6385.02,10, +6385.641,1.25, +6386.259,2.5, +6386.876,1.25, +6387.49,5, +6388.112,3.75, +6388.73,2.5, +6389.347,1.25, +6389.965,60, +6390.583,1.25, +6391.2,2.5, +6391.818,1.25, +6392.44,5, +6393.054,3.75, +6393.671,2.5, +6394.289,1.25, +6394.91,10, +6395.524,1.25, +6396.142,2.5, +6396.76,1.25, +6397.38,5, +6397.995,3.75, +6398.613,2.5, +6399.231,1.25, +6400.466,1.25, +6401.084,2.5, +6401.701,1.25, +6402.32,5, +6402.937,3.75, +6403.555,2.5, +6404.172,1.25, +6404.79,30, +6405.408,1.25, +6406.025,2.5, +6406.643,1.25, +6407.26,5, +6407.879,3.75, +6408.496,2.5, +6409.114,1.25, +6410.349,1.25, +6410.967,2.5, +6411.585,1.25, +6412.2,5, +6412.82,3.75, +6413.438,2.5, +6414.056,1.25, +6414.67,10, +6415.291,1.25, +6415.909,2.5, +6416.526,1.25, +6417.14,5, +6417.762,3.75, +6418.38,2.5, +6418.997,1.25, +6420.225,0.4, +6420.425,0.8, +6420.625,1.25, +6421.05,0.4, +6421.25,2.5, +6421.45,0.4, +6421.875,1.25, +6422.075,0.8, +6422.275,0.4, +6422.725,0.4, +6422.925,0.8, +6423.125,1.25, +6423.55,0.4, +6423.75,2.5, +6423.95,0.4, +6424.375,1.25, +6424.575,0.8, +6425.775,0.4, +6525.225,0.4, +6525.425,0.8, +6525.625,1.25, +6526.05,0.4, +6526.25,2.5, +6526.45,0.4, +6526.875,1.25, +6527.075,0.8, +6527.275,0.4, +6527.725,0.4, +6527.925,0.8, +6528.125,1.25, +6528.55,0.4, +6528.75,2.5, +6528.95,0.4, +6529.375,1.25, +6529.575,0.8, +6529.775,0.4, +6540.625,1.25, +6541.25,2.5, +6541.875,1.25, +6543.125,1.25, +6543.75,2.5, +6544.375,1.25, +6545,10, +6545.625,3.75, +6546.25,2.5, +6546.875,1.25, +6548.125,1.25, +6548.75,2.5, +6549.375,1.25, +6550,5, +6550.625,3.75, +6551.25,2.5, +6551.875,1.25, +6553.125,1.25, +6553.75,2.5, +6554.375,1.25, +6555,30, +6555.625,3.75, +6556.25,2.5, +6556.875,1.25, +6558.125,1.25, +6558.75,2.5, +6559.375,1.25, +6560,5, +6560.625,3.75, +6561.25,2.5, +6561.875,1.25, +6563.125,1.25, +6563.75,2.5, +6564.375,1.25, +6565,10, +6565.625,3.75, +6566.25,2.5, +6566.875,1.25, +6568.125,1.25, +6568.75,2.5, +6569.375,1.25, +6580.625,1.25, +6581.25,2.5, +6581.875,1.25, +6583.125,1.25, +6583.75,2.5, +6584.375,1.25, +6585,10, +6585.625,3.75, +6586.25,2.5, +6586.875,1.25, +6588.125,1.25, +6588.75,2.5, +6589.375,1.25, +6590,5, +6590.625,3.75, +6591.25,2.5, +6591.875,1.25, +6593.125,1.25, +6593.75,2.5, +6594.375,1.25, +6595,30, +6595.625,3.75, +6596.25,2.5, +6596.875,1.25, +6598.125,1.25, +6598.75,2.5, +6599.375,1.25, +6600,5, +6600.625,3.75, +6601.25,2.5, +6601.875,1.25, +6603.125,1.25, +6603.75,2.5, +6604.375,1.25, +6605,10, +6605.625,3.75, +6606.25,2.5, +6606.875,1.25, +6608.125,1.25, +6608.75,2.5, +6609.375,1.25, +6610,5, +6610.625,3.75, +6611.25,2.5, +6611.875,1.25, +6613.125,1.25, +6613.75,2.5, +6614.375,1.25, +6615,10, +6615.625,3.75, +6616.25,2.5, +6616.875,1.25, +6618.125,1.25, +6618.75,2.5, +6619.375,1.25, +6620,5, +6620.625,3.75, +6621.25,2.5, +6621.875,1.25, +6623.125,1.25, +6623.75,2.5, +6624.375,1.25, +6625,30, +6625.625,3.75, +6626.25,2.5, +6626.875,1.25, +6628.125,1.25, +6628.75,2.5, +6629.378,1.25, +6630,5, +6630.625,3.75, +6631.25,2.5, +6631.875,1.25, +6633.125,1.25, +6633.75,2.5, +6634.375,1.25, +6635,10, +6635.625,3.75, +6636.25,2.5, +6636.875,1.25, +6638.125,1.25, +6638.75,2.5, +6639.375,1.25, +6640,5, +6640.625,3.75, +6641.25,2.5, +6641.875,1.25, +6643.125,1.25, +6643.75,2.5, +6644.375,1.25, +6645,10, +6645.625,3.75, +6646.25,2.5, +6646.875,1.25, +6648.125,1.25, +6648.75,2.5, +6649.375,1.25, +6650,5, +6650.625,3.75, +6651.25,2.5, +6651.875,1.25, +6653.125,1.25, +6653.75,2.5, +6654.375,1.25, +6655,30, +6655.625,3.75, +6656.25,2.5, +6656.875,1.25, +6658.125,1.25, +6658.75,2.5, +6659.375,1.25, +6660,5, +6660.625,3.75, +6661.25,2.5, +6661.875,1.25, +6663.125,1.25, +6663.75,2.5, +6664.375,1.25, +6665,10, +6665.625,3.75, +6666.25,2.5, +6666.875,1.25, +6668.125,1.25, +6668.75,2.5, +6669.375,1.25, +6670,5, +6670.625,3.75, +6671.25,2.5, +6671.875,1.25, +6673.125,1.25, +6673.75,2.5, +6674.375,1.25, +6675,10, +6675.625,3.75, +6676.25,2.5, +6676.875,1.25, +6678.125,1.25, +6678.75,2.5, +6679.375,1.25, +6680,5, +6680.625,3.75, +6681.25,2.5, +6681.875,1.25, +6683.125,1.25, +6683.75,2.5, +6684.375,1.25, +6685,30, +6685.625,3.75, +6686.25,2.5, +6686.875,1.25, +6688.125,1.25, +6688.75,2.5, +6689.375,1.25, +6690,5, +6690.625,3.75, +6691.25,2.5, +6691.875,1.25, +6693.125,1.25, +6693.75,2.5, +6694.375,1.25, +6695,10, +6695.625,3.75, +6696.25,2.5, +6696.875,1.25, +6698.125,1.25, +6698.75,2.5, +6699.375,1.25, +6700,5, +6700.625,3.75, +6701.25,2.5, +6701.875,1.25, +6703.125,1.25, +6703.75,2.5, +6704.375,1.25, +6705,10, +6705.625,3.75, +6706.25,2.5, +6706.875,1.25, +6708.125,1.25, +6708.75,2.5, +6709.375,1.25, +6710.625,3.75, +6711.25,2.5, +6711.875,1.25, +6713.125,1.25, +6713.75,2.5, +6714.375,1.25, +6715,10, +6715.625,3.75, +6716.25,2.5, +6716.875,1.25, +6718.125,1.25, +6718.75,2.5, +6719.375,1.25, +6720.625,3.75, +6721.25,2.5, +6721.875,1.25, +6723.125,1.25, +6723.75,2.5, +6724.375,1.25, +6725,30, +6725.625,3.75, +6726.25,2.5, +6726.875,1.25, +6728.125,1.25, +6728.75,2.5, +6729.375,1.25, +6730,5, +6730.625,3.75, +6731.25,2.5, +6731.875,1.25, +6733.125,1.25, +6733.75,2.5, +6734.375,1.25, +6735,10, +6735.625,3.75, +6736.25,2.5, +6736.875,1.25, +6738.125,1.25, +6738.75,2.5, +6739.375,1.25, +6740,5, +6740.625,3.75, +6741.25,2.5, +6741.875,1.25, +6743.125,1.25, +6743.75,2.5, +6744.375,1.25, +6745,10, +6745.625,3.75, +6746.25,2.5, +6746.875,1.25, +6748.125,1.25, +6748.75,2.5, +6749.375,1.25, +6750,5, +6750.625,3.75, +6751.25,2.5, +6751.875,1.25, +6753.125,1.25, +6753.75,2.5, +6754.375,1.25, +6755,30, +6755.625,3.75, +6756.25,2.5, +6756.875,1.25, +6758.125,1.25, +6758.75,2.5, +6759.375,1.25, +6760,5, +6760.625,3.75, +6761.25,2.5, +6761.875,1.25, +6763.125,1.25, +6763.75,2.5, +6764.375,1.25, +6765,10, +6765.625,3.75, +6766.25,2.5, +6766.875,1.25, +6768.125,1.25, +6768.75,2.5, +6769.375,1.25, +6770,5, +6770.625,3.75, +6771.25,2.5, +6771.875,1.25, +6773.125,1.25, +6773.75,2.5, +6774.375,1.25, +6775,10, +6775.625,3.75, +6776.25,2.5, +6776.875,1.25, +6778.125,1.25, +6778.75,2.5, +6779.375,1.25, +6780,5, +6780.625,3.75, +6781.25,2.5, +6781.875,1.25, +6783.125,1.25, +6783.75,2.5, +6784.375,1.25, +6785,30, +6785.625,3.75, +6786.25,2.5, +6786.875,1.25, +6788.125,1.25, +6788.75,2.5, +6789.375,1.25, +6790,5, +6790.625,3.75, +6791.25,2.5, +6791.875,1.25, +6793.125,1.25, +6793.75,2.5, +6794.375,1.25, +6795,10, +6795.625,3.75, +6796.25,2.5, +6796.875,1.25, +6798.125,1.25, +6798.75,2.5, +6799.375,1.25, +6800,5, +6800.625,3.75, +6801.25,2.5, +6801.875,1.25, +6803.125,1.25, +6803.75,2.5, +6804.375,1.25, +6805,10, +6805.625,3.75, +6806.25,2.5, +6806.875,1.25, +6808.125,1.25, +6808.75,2.5, +6809.375,1.25, +6810,5, +6810.625,3.75, +6811.25,2.5, +6811.875,1.25, +6813.125,1.25, +6813.75,2.5, +6814.375,1.25, +6815,30, +6815.625,3.75, +6816.25,2.5, +6816.875,1.25, +6818.125,1.25, +6818.75,2.5, +6819.375,1.25, +6820,5, +6820.625,3.75, +6821.25,2.5, +6821.875,1.25, +6823.125,1.25, +6823.75,2.5, +6824.375,1.25, +6825,10, +6825.625,3.75, +6826.25,2.5, +6826.875,1.25, +6828.125,1.25, +6828.75,2.5, +6829.375,1.25, +6830,5, +6830.625,3.75, +6831.25,2.5, +6831.875,1.25, +6833.125,1.25, +6833.75,2.5, +6834.375,1.25, +6835,10, +6835.625,3.75, +6836.25,2.5, +6836.875,1.25, +6838.125,1.25, +6838.75,2.5, +6839.375,1.25, +6840,5, +6840.625,3.75, +6841.25,2.5, +6841.875,1.25, +6843.125,1.25, +6843.75,2.5, +6844.375,1.25, +6845,30, +6845.625,3.75, +6846.25,2.5, +6846.875,1.25, +6848.125,1.25, +6848.75,2.5, +6849.375,1.25, +6850,5, +6850.625,3.75, +6851.25,2.5, +6851.875,1.25, +6853.125,1.25, +6853.75,2.5, +6854.375,1.25, +6855,10, +6855.625,3.75, +6856.25,2.5, +6856.875,1.25, +6858.125,1.25, +6858.75,2.5, +6859.375,1.25, +6860,5, +6860.625,3.75, +6861.25,2.5, +6861.875,1.25, +6863.125,1.25, +6863.75,2.5, +6864.375,1.25, +6865,10, +6865.625,3.75, +6866.25,2.5, +6866.875,1.25, +6868.125,1.25, +6868.75,2.5, +6869.375,1.25, +6870.225,0.4, +6870.425,0.8, +6870.625,1.25, +6871.05,0.4, +6871.25,2.5, +6871.45,0.4, +6871.875,1.25, +6872.075,0.8, +6872.275,0.4, +6872.725,0.4, +6872.925,0.8, +6873.125,1.25, +6873.55,0.4, +6873.75,2.5, +6873.95,0.4, +6874.375,1.25, +6874.575,0.8, +6874.775,0.4, diff --git a/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/high_performance_antennas.csv b/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/high_performance_antennas.csv new file mode 100644 index 0000000..f1ca34b --- /dev/null +++ b/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/high_performance_antennas.csv @@ -0,0 +1,23 @@ +manufacturerNormalized,antennaModelPrefix,note +Gabriel Electronics,UCC1059, +Gabriel Electronics,UCC10X59DSE, +Gabriel Electronics,UCC1259A, +Gabriel Electronics,UCC659A, +Gabriel Electronics,UCC6X59A, +Gabriel Electronics,UCC859A, +Gabriel Electronics,UCC859B, +Gabriel Electronics,UCC8X59CSE, +Commscope/Andrew,UHP659W, +Commscope/Andrew,UHP859W, +Commscope/Andrew,UHX1059, +Commscope/Andrew,UHX1259, +Commscope/Andrew,UHX1265, +Commscope/Andrew,UHX659, +Commscope/Andrew,UHX665, +Commscope/Andrew,UHX859, +Commscope/Andrew,UHX865, +Gabriel Electronics,UWB105968, +Gabriel Electronics,UWB65968SE, +Gabriel Electronics,UWB65971SE, +Gabriel Electronics,UWB85968, +Gabriel Electronics,UWB85971A, diff --git a/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/transmit_radio_unit_architecture.csv b/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/transmit_radio_unit_architecture.csv new file mode 100644 index 0000000..d3c74f7 --- /dev/null +++ b/src/ratapi/ratapi/db/raw_wireless_innovation_forum_files/transmit_radio_unit_architecture.csv @@ -0,0 +1,3354 @@ +manufacturerNormalized,radioModelPrefix,architecture,notes +HARRIS,06G155M,IDU,"This prefix is currently unique to Aviat/Harris, and an indoor unit. " +AVIAT,100V3SU610M64QDS3,Unknown, +AVIAT,1600HL,IDU,1 interpreted as I meaning Indoor in Aviat's notation +AVIAT,1600HU,IDU,1 interpreted as I meaning Indoor in Aviat's notation +AVIAT,1600SL,IDU,1 interpreted as I meaning Indoor in Aviat's notation +AVIAT,1600SU,IDU,1 interpreted as I meaning Indoor in Aviat's notation +AVIAT,1600V,IDU,1 interpreted as I meaning Indoor in Aviat's notation +AVIAT,160SU65M128Q16T24,Unknown, +AVIAT,163HL630MACMR70,Unknown, +AVIAT,16V3EHPU610M45,Unknown, +AVIAT,16V3HL630M189AMP,Unknown, +AVIAT,16V3HL630M189AMR,Unknown, +AVIAT,16V3SL630M189,Unknown, +AVIAT,16V3SU65M24,Unknown, +AVIAT,16V4HL630MACMR70,Unknown, +AVIAT,16V4SL630M128Q155R70,Unknown, +NORTEL,1717000,Unknown, +CERAGON,1IP20B6HP30XACM,ODU, +CERAGON,1P106T30128QAM5,Unknown, +MDS,1P106T30256QAN7,Unknown, +NEC,20000SDH,Unknown, +NEC,2000NEC,Unknown, +NEC,2000SDH,IDU,https://www.slideshare.net/Walter1111/nec-2000-series +NEC,2004SDH,Unknown, +WESTERN MULTIPLEX,26011,Unknown, +NORTEL,27D201,Unknown, +ROCKWELL,2K7801,Unknown, +MOTOROLA,2YHI01,Unknown, +VARIOUS,30M0D7W,Unknown, +VARIOUS,30M0D9W,Unknown, +VARIOUS,30M0F7W,Unknown, +EXALT,3600511256Q30MHZ,Unknown, +VARIOUS,3M50D7W,Unknown, +ALCATEL,4000E,Unknown, +ALCATEL,4306EC,Unknown, +HARRIS,45641,Unknown, +HARRIS,45642,Unknown, +CAMBIUM,48630,Unknown, +ALCATEL,5000SERIES,Unknown, +AVIAT,500HL630MO64139MB,Unknown, +ALCATEL,5606,Unknown, +ALCATEL,5MPR61L1024A30227,IDU, +STAR MICROWAVE,6000,Unknown, +ALCATEL,60045000,Unknown, +AVIAT,600H111,Unknown, +AVIAT,600HL,Unknown, +AVIAT,600HU,Unknown, +AVIAT,600KL630M0100T1LO,Unknown, +AVIAT,600L630M064135MB,Unknown, +AVIAT,600SL30M0256115T,Unknown, +AVIAT,600SL610M029T1LO,Unknown, +AVIAT,600SL610M06429T,Unknown, +AVIAT,600SL630M0100T1LO,Unknown, +AVIAT,600SL630M0115T1HG,Unknown, +HARRIS,600SL630M0115T1HG,Unknown, +AVIAT,600SL630M0128100T,Unknown, +AVIAT,600SL630M0128OC3,Unknown, +AVIAT,600SL630M01688MB,Unknown, +AVIAT,600SL630M0256115T,Unknown, +AVIAT,600SL630M0256179M,Unknown, +AVIAT,600SL630M0256189M,Unknown, +AVIAT,600SL630M025664Q,Unknown, +AVIAT,600SL630M064135MB,Unknown, +AVIAT,600SL630M064139MB,Unknown, +AVIAT,600SL630M06487T,Unknown, +AVIAT,600SL630M087T1LO,Unknown, +AVIAT,600SL630M0OC3HG,Unknown, +AVIAT,600SL630M0QPSK44M,Unknown, +AVIAT,600SL630M128Q100T1,Unknown, +AVIAT,600SL630MO64135MB,Unknown, +AVIAT,600SL63M758T1,Unknown, +AVIAT,600SL65M0012816T1,Unknown, +AVIAT,600SL65M0016T1,Unknown, +AVIAT,600SL65M0128Q16T1,Unknown, +AVIAT,600SL65MO128Q16T1,Unknown, +AVIAT,600SL6M0012816T1,Unknown, +AVIAT,600SU610M012832T1,Unknown, +AVIAT,600SU610M025655MB,Unknown, +AVIAT,600SU610M029T1LO,Unknown, +AVIAT,600SU610M036T1,Unknown, +AVIAT,600SU610M06429T1,Unknown, +AVIAT,600SU610M06429TI,Unknown, +AVIAT,600SU610M06445MB,Unknown, +AVIAT,600SU610M128Q32T1,Unknown, +AVIAT,600SU610MAMR,Unknown, +AVIAT,600SU610MO6445MB,Unknown, +AVIAT,600SU630M0128100T,Unknown, +AVIAT,600SU630M0256115T,Unknown, +AVIAT,600SU630M0256179M,Unknown, +AVIAT,600SU630M0256189M,Unknown, +AVIAT,600SU630M064135MB,Unknown, +AVIAT,600SU630M064139MB,Unknown, +AVIAT,600SU63M7532Q8T,Unknown, +AVIAT,600SU63M7532Q8T1,Unknown, +AVIAT,600SU65M0012816T1,Unknown, +AVIAT,600SU65M0016T1,Unknown, +AVIAT,600SU65M128Q16T,Unknown, +AVIAT,600V3HL620M256Q116MB,Unknown, +AVIAT,600V3HL630M128Q154MB,Unknown, +AVIAT,600V3HL630M256Q179MB,Unknown, +AVIAT,600V3SU65M64Q19MB,Unknown, +NUCOMM,60FT6FR6,Unknown, +CAMBIUM,6137925S,Unknown, +NEC,621,Unknown, +HARRIS,628DS1,Unknown, +ERICSSON,64QAM,Unknown, +ALCATEL,65MPR61L,IDU, +ERICSSON,666L63AAA,ODU, +ERICSSON,666L63AAA,ODU,Comsearch Frequency Coordination Database +ERICSSON,666U63AAA,ODU, +AVIAT,699HL630M0128OC3,Unknown, +HUAWEI,6GXMC2,ODU,Comsearch Frequency Coordination Database +HUAWEI,6GXMC2,ODU, +DRAGONWAVE,6LCH28EFC143,Unknown, +DRAGONWAVE,6LES30HFC270V01HAAM,Unknown, +DRAGONWAVE,6LES60HET073V01,Unknown, +DRAGONWAVE,6LES60HET169V01,Unknown, +DRAGONWAVE,6LES60HET218V01,Unknown, +DRAGONWAVE,6LES60HET266V01,Unknown, +DRAGONWAVE,6LES60HET314V01,Unknown, +DRAGONWAVE,6LES60HET363V01,Unknown, +DRAGONWAVE,6LHC28EFC143,Unknown, +DRAGONWAVE,6LHC28EFC143V01,Unknown, +DRAGONWAVE,6LHC28EFC144V01,Unknown, +DRAGONWAVE,6LHC28EFC193V01,Unknown, +DRAGONWAVE,6LHC28EFC200,Unknown, +DRAGONWAVE,6LHC28HFC143V01,Unknown, +DRAGONWAVE,6LHC28HFC144,Unknown, +DRAGONWAVE,6LHC28HFC144V01,Unknown, +DRAGONWAVE,6LHC28HFC190,Unknown, +DRAGONWAVE,6LHC28HFC190V01,Unknown, +DRAGONWAVE,6LHC28HFC190V01HAAM,Unknown, +DRAGONWAVE,6LHC28HFC190V02,Unknown, +DRAGONWAVE,6LHC28HFC190V02AMR,Unknown, +DRAGONWAVE,6LHC28HFC193V01,Unknown, +DRAGONWAVE,6LHC30EFC050V00HAAM,Unknown, +DRAGONWAVE,6LHC30EFC050V01,Unknown, +DRAGONWAVE,6LHC30EFC087V00HAAM,Unknown, +DRAGONWAVE,6LHC30EFC087V01,Unknown, +DRAGONWAVE,6LHC30EFC112V00HAAM,Unknown, +DRAGONWAVE,6LHC30EFC112V01,Unknown, +DRAGONWAVE,6LHC30EFC121V01,Unknown, +DRAGONWAVE,6LHC30EFC138V00HAAM,Unknown, +DRAGONWAVE,6LHC30EFC138V01,Unknown, +DRAGONWAVE,6LHC30EFC143V01,Unknown, +DRAGONWAVE,6LHC30EFC163V00HAAM,Unknown, +DRAGONWAVE,6LHC30EFC163V01,Unknown, +DRAGONWAVE,6LHC30EFC193V01,Unknown, +DRAGONWAVE,6LHC30EFC200V00HAAM,Unknown, +DRAGONWAVE,6LHC30EFC200V01,Unknown, +DRAGONWAVE,6LHC30EFC200V01HAAM,Unknown, +DRAGONWAVE,6LHC30EFC212V03HAAM,Unknown, +DRAGONWAVE,6LHC30EFCHAAM,Unknown, +DRAGONWAVE,6LHC56HET169V03,Unknown, +DRAGONWAVE,6LHC56HET216V03,Unknown, +DRAGONWAVE,6LHC56HET265V03,Unknown, +DRAGONWAVE,6LHC56HET290V03,Unknown, +DRAGONWAVE,6LHC56HET385V03,Unknown, +DRAGONWAVE,6LHC56HET96V03,Unknown, +DRAGONWAVE,6LHC60EFC385V01,Unknown, +DRAGONWAVE,6LHC60EFC385V01HAAM,Unknown, +DRAGONWAVE,6LHY10EFC052V01,Unknown, +DRAGONWAVE,6LHY28EET138V01,Unknown, +DRAGONWAVE,6LHY30EFC039V01,Unknown, +DRAGONWAVE,6LHY30EFC039V02,Unknown, +DRAGONWAVE,6LHY30EFC092V01,Unknown, +DRAGONWAVE,6LHY30EFC092V02,Unknown, +DRAGONWAVE,6LHY30EFC118V01,Unknown, +DRAGONWAVE,6LHY30EFC118V02,Unknown, +DRAGONWAVE,6LHY30EFC118V02HAAM,Unknown, +DRAGONWAVE,6LHY30EFC144V01,Unknown, +DRAGONWAVE,6LHY30EFC144V02,Unknown, +DRAGONWAVE,6LHY30EFC144V02HAAM,Unknown, +DRAGONWAVE,6LHY30EFC170V01,Unknown, +DRAGONWAVE,6LHY30EFC170V02,Unknown, +DRAGONWAVE,6LHY30EFC170V02HAAM,Unknown, +DRAGONWAVE,6LHY30EFC196V01,Unknown, +DRAGONWAVE,6LHY30EFC196V02,Unknown, +DRAGONWAVE,6LHY30EFC209V01,Unknown, +DRAGONWAVE,6LHY30EFC209V01HAAM,Unknown, +DRAGONWAVE,6LHY30EFC209V02,Unknown, +DRAGONWAVE,6LHY30EFC209V02HAAM,Unknown, +DRAGONWAVE,6LHY30EFC222V02,Unknown, +DRAGONWAVE,6LHY30EFC222V02HAAM,Unknown, +DRAGONWAVE,6LHY30EFC248V02,Unknown, +DRAGONWAVE,6LHY30EFC248V02HAAM,Unknown, +DRAGONWAVE,6LHY30EFC270V02,Unknown, +DRAGONWAVE,6LHY30EFC270V02HAAM,Unknown, +DRAGONWAVE,6LHY30EFC39V02HAAM,Unknown, +DRAGONWAVE,6LHY30EFC92V02HAAM,Unknown, +DRAGONWAVE,6LHY56EET451V03HAAM,Unknown, +DRAGONWAVE,6LHY56EET490V02HAAM,Unknown, +DRAGONWAVE,6LHY56EET490V03HAAM,Unknown, +DRAGONWAVE,6LHY56EFC490V02HAAM,Unknown, +DRAGONWAVE,6LHY60EFC071V03HAAM,Unknown, +DRAGONWAVE,6LHY60EFC166V03HAAM,Unknown, +DRAGONWAVE,6LHY60EFC214V03HAAM,Unknown, +DRAGONWAVE,6LHY60EFC261V03HAAM,Unknown, +DRAGONWAVE,6LHY60EFC308V02HAAM,Unknown, +DRAGONWAVE,6LHY60EFC308V03HAAM,Unknown, +DRAGONWAVE,6LHY60EFC380V01HAAM,Unknown, +DRAGONWAVE,6LHY60EFC380V03HAAM,Unknown, +DRAGONWAVE,6LHY60EFC38V01HAAM,Unknown, +DRAGONWAVE,6LHY60EFC403V03HAAM,Unknown, +DRAGONWAVE,6LHY60EFC451V02HAAM,Unknown, +DRAGONWAVE,6LHY60EFC451V03HAAM,Unknown, +DRAGONWAVE,6LHY60EFC490V03HAAM,Unknown, +DRAGONWAVE,6LHY60EFC490V03HHAAM,Unknown, +DRAGONWAVE,6LHY60RFC308V02HAAM,Unknown, +DRAGONWAVE,6LQD28HFC190,Unknown, +DRAGONWAVE,6LQD28HFC190V01,Unknown, +DRAGONWAVE,6LQD28HFC190V03,Unknown, +DRAGONWAVE,6LQD30HFC037V04HAAM,Unknown, +DRAGONWAVE,6LQD30HFC037V05HAAM,Unknown, +DRAGONWAVE,6LQD30HFC039V02HAAM,Unknown, +DRAGONWAVE,6LQD30HFC039V04,Unknown, +DRAGONWAVE,6LQD30HFC050V01,Unknown, +DRAGONWAVE,6LQD30HFC086V05HAAM,Unknown, +DRAGONWAVE,6LQD30HFC087V01,Unknown, +DRAGONWAVE,6LQD30HFC091V02HAAM,Unknown, +DRAGONWAVE,6LQD30HFC091V04,Unknown, +DRAGONWAVE,6LQD30HFC111V05HAAM,Unknown, +DRAGONWAVE,6LQD30HFC112V01,Unknown, +DRAGONWAVE,6LQD30HFC118V02HAAM,Unknown, +DRAGONWAVE,6LQD30HFC118V04,Unknown, +DRAGONWAVE,6LQD30HFC136V05HAAM,Unknown, +DRAGONWAVE,6LQD30HFC138V01,Unknown, +DRAGONWAVE,6LQD30HFC142V05,Unknown, +DRAGONWAVE,6LQD30HFC143,Unknown, +DRAGONWAVE,6LQD30HFC143V02,Unknown, +DRAGONWAVE,6LQD30HFC144V02HAAM,Unknown, +DRAGONWAVE,6LQD30HFC144V04,Unknown, +DRAGONWAVE,6LQD30HFC160V05HAAM,Unknown, +DRAGONWAVE,6LQD30HFC163V00,Unknown, +DRAGONWAVE,6LQD30HFC170V02HAAM,Unknown, +DRAGONWAVE,6LQD30HFC170V04,Unknown, +DRAGONWAVE,6LQD30HFC193V02,Unknown, +DRAGONWAVE,6LQD30HFC197V04HAAM,Unknown, +DRAGONWAVE,6LQD30HFC197V05HAAM,Unknown, +DRAGONWAVE,6LQD30HFC200HAAM,Unknown, +DRAGONWAVE,6LQD30HFC200V01HAAM,Unknown, +DRAGONWAVE,6LQD30HFC200V02,Unknown, +DRAGONWAVE,6LQD30HFC200V03,Unknown, +DRAGONWAVE,6LQD30HFC209V02HAAM,Unknown, +DRAGONWAVE,6LQD30HFC209V04,Unknown, +DRAGONWAVE,6LQD30HFC209V05,Unknown, +DRAGONWAVE,6LQD30HFC209V05AMR,Unknown, +DRAGONWAVE,6LQD30HFC209V05HAAM,Unknown, +DRAGONWAVE,6LQD30HFC210V05HAAM,Unknown, +DRAGONWAVE,6LQD30HFC222V04,Unknown, +DRAGONWAVE,6LQD30HFC234V05HAAM,Unknown, +DRAGONWAVE,6LQD30HFC248V04,Unknown, +DRAGONWAVE,6LQD30HFC255V05HAAM,Unknown, +DRAGONWAVE,6LQD30HFC270V04HAAM,Unknown, +DRAGONWAVE,6LQD56HET403V03HAAM,Unknown, +DRAGONWAVE,6LQD60HFC071V02,Unknown, +DRAGONWAVE,6LQD60HFC071V02HAAM,Unknown, +DRAGONWAVE,6LQD60HFC166V02,Unknown, +DRAGONWAVE,6LQD60HFC166V02HAAM,Unknown, +DRAGONWAVE,6LQD60HFC213V02,Unknown, +DRAGONWAVE,6LQD60HFC213V02HAAM,Unknown, +DRAGONWAVE,6LQD60HFC261V02,Unknown, +DRAGONWAVE,6LQD60HFC261V02HAAM,Unknown, +DRAGONWAVE,6LQD60HFC308V02,Unknown, +DRAGONWAVE,6LQD60HFC308V02HAAM,Unknown, +DRAGONWAVE,6LQD60HFC379V01,Unknown, +DRAGONWAVE,6LQD60HFC379V02,Unknown, +DRAGONWAVE,6LQD60HFC379V02HAAM,Unknown, +DRAGONWAVE,6LQD60HFC403V02,Unknown, +DRAGONWAVE,6LQD60HFC403V02HAAM,Unknown, +DRAGONWAVE,6LQD60HFC403V03HAAM,Unknown, +DRAGONWAVE,6LQS10HFC049V00,Unknown, +DRAGONWAVE,6LQS10HFC064V04,Unknown, +DRAGONWAVE,6LQS10HFC066,Unknown, +DRAGONWAVE,6LQS10HFC066V02,Unknown, +DRAGONWAVE,6LQS28HFC109V01,Unknown, +DRAGONWAVE,6LQS28HFC133V01,Unknown, +DRAGONWAVE,6LQS28HFC157V01,Unknown, +DRAGONWAVE,6LQS28HFC190V01,Unknown, +DRAGONWAVE,6LQS30HFC039V05,Unknown, +DRAGONWAVE,6LQS30HFC050V01,Unknown, +DRAGONWAVE,6LQS30HFC087V01,Unknown, +DRAGONWAVE,6LQS30HFC091V05,Unknown, +DRAGONWAVE,6LQS30HFC112V01,Unknown, +DRAGONWAVE,6LQS30HFC118V05,Unknown, +DRAGONWAVE,6LQS30HFC138V01,Unknown, +DRAGONWAVE,6LQS30HFC143V02,Unknown, +DRAGONWAVE,6LQS30HFC143V03,Unknown, +DRAGONWAVE,6LQS30HFC143V04,Unknown, +DRAGONWAVE,6LQS30HFC144V05,Unknown, +DRAGONWAVE,6LQS30HFC163V01,Unknown, +DRAGONWAVE,6LQS30HFC170V05,Unknown, +DRAGONWAVE,6LQS30HFC190V05,Unknown, +DRAGONWAVE,6LQS30HFC193V02,Unknown, +DRAGONWAVE,6LQS30HFC200,Unknown, +DRAGONWAVE,6LQS30HFC200V01,Unknown, +DRAGONWAVE,6LQS30HFC200V01HAAM,Unknown, +DRAGONWAVE,6LQS30HFC200V02,Unknown, +DRAGONWAVE,6LQS30HFC209V05,Unknown, +DRAGONWAVE,6LQS30HFC209V05HAAM,Unknown, +DRAGONWAVE,6LQS30HFC222V04HAAM,Unknown, +DRAGONWAVE,6LQS30HFC222V05,Unknown, +DRAGONWAVE,6LQS30HFC248V04HAAM,Unknown, +DRAGONWAVE,6LQS30HFC248V05,Unknown, +DRAGONWAVE,6LQS30HFC270V04HAAM,Unknown, +DRAGONWAVE,6LQS30HFC270V05,Unknown, +DRAGONWAVE,6LQS30HFC270V05HAAM,Unknown, +DRAGONWAVE,6LQS56HET403V03HAAM,Unknown, +DRAGONWAVE,6LQS60HFC379V01HAAM,Unknown, +DRAGONWAVE,6LQS60HFC403V02HAAM,Unknown, +DRAGONWAVE,6UHC28HFC143V01,Unknown, +DRAGONWAVE,6UHC28HFC193V01,Unknown, +DRAGONWAVE,6UQD30EFC193,Unknown, +DRAGONWAVE,6UQD30HFC039V03HAAM,Unknown, +DRAGONWAVE,6UQD30HFC091V03HAAM,Unknown, +DRAGONWAVE,6UQD30HFC118V03HAAM,Unknown, +DRAGONWAVE,6UQD30HFC144V03HAAM,Unknown, +DRAGONWAVE,6UQD30HFC170V03HAAM,Unknown, +DRAGONWAVE,6UQD30HFC200,Unknown, +DRAGONWAVE,6UQD30HFC200V01HAAM,Unknown, +DRAGONWAVE,6UQD30HFC200V03,Unknown, +DRAGONWAVE,6UQD30HFC209V03HAAM,Unknown, +DRAGONWAVE,6UQD30HFC222V03HAAM,Unknown, +DRAGONWAVE,6UQS28HFC109V01,Unknown, +DRAGONWAVE,6UQS28HFC157V01,Unknown, +DRAGONWAVE,6UQS28HFC190V01,Unknown, +DRAGONWAVE,6UQS30HFC050V01,Unknown, +DRAGONWAVE,6UQS30HFC050V01HAAM,Unknown, +DRAGONWAVE,6UQS30HFC087V01,Unknown, +DRAGONWAVE,6UQS30HFC087V01HAAM,Unknown, +DRAGONWAVE,6UQS30HFC112V01,Unknown, +DRAGONWAVE,6UQS30HFC112V01HAAM,Unknown, +DRAGONWAVE,6UQS30HFC138V01,Unknown, +DRAGONWAVE,6UQS30HFC138V01HAAM,Unknown, +DRAGONWAVE,6UQS30HFC143V01,Unknown, +DRAGONWAVE,6UQS30HFC163V01,Unknown, +DRAGONWAVE,6UQS30HFC163V01HAAM,Unknown, +DRAGONWAVE,6UQS30HFC200V01,Unknown, +DRAGONWAVE,6UQS30HFC200V01HAAM,Unknown, +DRAGONWAVE,6UQS30HFC222V03HAAM,Unknown, +AVIAT,6V4SL630M128Q155R70,Unknown, +NORTEL,6X30A1,Unknown, +NUCOMM,70FT6,Unknown, +NUCOMM,70HDLT,Unknown, +LENKURT,775A1,Unknown, +LENKURT,778A1,Unknown, +LENKURT,778A2,IDU, +LENKURT,778A3,Unknown, +LENKURT,778B3,IDU, +NORTEL,7UJ6X30A1,Unknown, +NORTEL,7UJRD6C3,Unknown, +ALCATEL,8000SERIES,Unknown, +ALCATEL,8000SONET,Unknown, +ALCATEL,85MPR61H256F30160,IDU, +ALCATEL,8606MDR135,Unknown, +ALCATEL,94MPR61,Unknown, +ALCATEL,94MPR61H128F30160,IDU, +ALCATEL,9500MPR61H128F30160,IDU, +ALCATEL,9500MPTHLCSP,IDU, +ALCATEL,9506MXC100DS1128QAM,Unknown, +ALCATEL,9506MXC1OC3128QAM,Unknown, +ALCATEL,958MPR67L128A525S,IDU, +ALCATEL,95MP0R61C128F30163,ODU, +ALCATEL,95MP561H128F30160,IDU, +ALCATEL,95MP61,Unknown, +ALCATEL,95MPR11,Unknown, +ALCATEL,95MPR11C128F1052,ODU, +ALCATEL,95MPR11H,IDU, +ALCATEL,95MPR11L,IDU, +ALCATEL,95MPR16H64A30137H,IDU, +ALCATEL,95MPR6,Unknown, +ALCATEL,95MPR61,Unknown, +ALCATEL,95MPR611H128F30160,IDU, +ALCATEL,95MPR6195MPR61,Unknown, +ALCATEL,95MPR61A30137H,Unknown, +ALCATEL,95MPR61C,ODU, +ALCATEL,95MPR61D128F30160H,Unknown, +ALCATEL,95MPR61F128F30160,Unknown, +ALCATEL,95MPR61H,IDU, +ALCATEL,95MPR61J256F30191H,Unknown, +ALCATEL,95MPR61L,IDU, +ALCATEL,95MPR61O,ODU,Comsearch Frequency Coordination Database +ALCATEL,95MPR61Q,ODU, +ALCATEL,95MPR61W2048A60461,Unknown, +ALCATEL,95MPR61YH128F30160,Unknown, +ALCATEL,95MPR67,Unknown, +ALCATEL,95MPR67A,ODU, +ALCATEL,95MPR67C,ODU, +ALCATEL,95MPR67H,IDU, +ALCATEL,95MPR67L,IDU, +ALCATEL,95MPR67O,ODU,Comsearch Frequency Coordination Database +ALCATEL,95MPR67O,ODU, +ALCATEL,95MPR67Q,ODU, +ALCATEL,95MPR6H,IDU, +ALCATEL,95MPR6Q2048A60461,ODU, +ALCATEL,95MPR91H4A3045H,IDU, +ALCATEL,95MPR97,Unknown, +ALCATEL,95MPRH,IDU, +ALCATEL,95MPRL,IDU, +ALCATEL,95MPRR61,Unknown, +ALCATEL,95MPRT61H,IDU, +ALCATEL,95MPS61H,IDU, +ALCATEL,95MR61H,IDU, +ALCATEL,95MRP61H,IDU, +ALCATEL,95MRP61L,IDU, +ALCATEL,95MRP67,Unknown, +ALCATEL,95MRP67H128F1053,IDU, +ALCATEL,95NPR61L1024A30227HPA,IDU, +ALCATEL,95PMPR67L128A1052S,IDU, +ALCATEL,95PMR61L512A30206H,IDU, +ALCATEL,95PMR61O128F30160,ODU, +ALCATEL,95PMR67L128A1052S,IDU, +ALCATEL,95PR61,Unknown, +ALCATEL,95PR61H128F30160,IDU, +ALCATEL,95PR61H4A3045H,IDU, +ALCATEL,96MPR61H128F1053,IDU, +ALCATEL,96MPR61H256A30183,IDU, +ALCATEL,96MPR61H4A3045H,IDU, +ALCATEL,995MPR61L64A30136H,IDU, +ALCATEL,9MPR61H16A3091,IDU, +ITT TELECOMMUNICATIONS,A33604,Unknown, +ITT TELECOMMUNICATIONS,A33607,Unknown, +ALCATEL,A67716,Unknown, +DRAGONWAVE,AANT6G6,Unknown, +MOTOROLA,ABZ89F6601,Unknown, +MOTOROLA,ABZ89FAC6601,Unknown, +MOTOROLA,ABZ89FC6601,IDU, +MOTOROLA,ABZ89FC6610,Unknown, +"Ubiquiti Networks, Inc.",AF11FX,Unknown, +SAF TEHNIKA,AFTEHNIKAMODELPHOENIXU,Unknown, +ROCKWELL,AJN0UO8814,Unknown, +ROCKWELL,AJN8UO8201,Unknown, +COLLINS,AJN979MAR6C,Unknown, +ROCKWELL,AJN9O8201,Unknown, +ROCKWELL,AJN9OUMAR6C,Unknown, +ROCKWELL,AJN9U08803,Unknown, +ROCKWELL,AJN9U0MAR6C,Unknown, +ROCKWELL,AJN9U0MIR61,Unknown, +ROCKWELL,AJN9UO8201,Unknown, +ROCKWELL,AJN9UO8608,Unknown, +ROCKWELL,AJN9UO8609,IDU, +ROCKWELL,AJN9UO8803,Unknown, +ROCKWELL,AJN9UO8814,IDU, +ROCKWELL,AJN9UO9201,Unknown, +COLLINS,AJN9UOMAR6,IDU, +ROCKWELL,AJN9UOMAR6,IDU, +COLLINS,AJN9UOMARY6C,Unknown, +ROCKWELL,AJN9UOMIR6,IDU, +COLLINS,AJN9UOMIR6,IDU, +ROCKWELL,AJN9V08803,Unknown, +ALCATEL,AL0H06064028T110,Unknown, +ALCATEL,AL0H0606403DS330,Unknown, +ALCATEL,AL0H06064084T130,Unknown, +ALCATEL,AL0H06128016T105,Unknown, +ALCATEL,AL0H0612801OC330,Unknown, +ALCATEL,AL0H06128100T130,Unknown, +ALCATEL,AL0H0612901OC330,Unknown, +ALCATEL,AL0H606403DS330,Unknown, +ALCOMA,AL6FMP400,ODU,Comsearch Frequency Coordination Database +ALCOMA,AL6FMP400660MAMR,Unknown, +ALCOMA,AL6LFMP400,ODU,Comsearch Frequency Coordination Database +ALCATEL,ALCATELLUCENTUSAINC,Unknown, +ALCATEL,ALCMDR8606135,Unknown, +SIAE,ALFO6LPLUS,ODU,Comsearch Frequency Coordination Database +SIAE,ALFO6LPLUS2,Unknown, +SIAE,ALFO6LPLUS2255M,Unknown, +SIAE,ALFO6LPLUS2255M4096QA,Unknown, +SIAE,ALFO6LPLUS230MHZ,Unknown, +SIAE,ALFO6LPLUS2533M,Unknown, +SIAE,ALFO6LPLUS2533M4096QA,Unknown, +SIAE,ALFO6LPLUS260MHZ,Unknown, +SIAE,ALFO6UPLUS130M1024Q,Unknown, +SIAE,ALFO6UPLUS130M128Q,Unknown, +SIAE,ALFO6UPLUS130M16Q,Unknown, +SIAE,ALFO6UPLUS130M2048Q,Unknown, +SIAE,ALFO6UPLUS130M256Q,Unknown, +SIAE,ALFO6UPLUS130M32Q,Unknown, +SIAE,ALFO6UPLUS130M4096Q,Unknown, +SIAE,ALFO6UPLUS130M4Q,Unknown, +SIAE,ALFO6UPLUS130M512Q,Unknown, +SIAE,ALFO6UPLUS130M64Q,Unknown, +SIAE,ALFO6UPLUS2255M,Unknown, +SIAE,ALFO6UPLUS,ODU,Comsearch Frequency Coordination Database +ALCATEL,ALOH06064084T130,Unknown, +ALCATEL,ALOH06128010C330,Unknown, +ALCATEL,ALOH06128016T105,Unknown, +ALCATEL,ALOH0612801OC330,Unknown, +ALCATEL,ALOH06128O16T105,Unknown, +ALCATEL,ALOH606408R5130,Unknown, +STRATEX,ALTIUMMX155,Unknown, +DMC,ALTIUMOC3,Unknown, +DMC,ALTIUMSDHSONET,Unknown, +DMC,ALTIUMSDHSONET0C3,Unknown, +DMC,ALTIUMSDHSONETOC3,Unknown, +ALLGON MICROWAVE,AMRTRANSCEND,Unknown, +ALLGON MICROWAVE,AMRTRANSCEND100,Unknown, +MNI,AMTM611045,Unknown, +MNI,AMTM61519MBPS,Unknown, +MNI,AMTM637512MBPSHP,Unknown, +MNI,AMTM6519MBPS,Unknown, +MNI,AMTM6I,IDU, +COLLINS,ANJ9UOMAR6C,Unknown, +ROCKWELL,ANJ9UOMAR6C,Unknown, +APEX9,APEX9LL1000,Unknown, +APEX9,APEX9LL1000630M256Q,Unknown, +APEX9,APEX9LL10006AMR,Unknown, +APEX9,APEX9LL1337,Unknown, +TRANGO,APEXL6L,ODU,Comsearch Frequency Coordination Database +TRANGO,APEXLYNX,Unknown, +TRANGO,APEXLYNX630M1024QAMR,Unknown, +TRANGO,APEXLYNX656M1024QAMR,Unknown, +TRANGO,APEXO67,ODU,Comsearch Frequency Coordination Database +TRANGO,APEXO6L,ODU,Comsearch Frequency Coordination Database +TRANGO,APEXORION,Unknown, +TRANGO,APEXORION6,Unknown, +TRANGO,APEXORION630M1024QAM,Unknown, +TRANGO,APEXORION656M1024QAM,Unknown, +TRANGO,APEXORION6730M1024Q,Unknown, +TRANGO,APEXPLUS6,Unknown, +TRANGO,APEXPLUS630MHZAMR,Unknown, +AT&T TECHNOLOGIES,AS5933MDR630135E,IDU, +AT&T TECHNOLOGIES,AS593MD4630135B,IDU, +AT&T TECHNOLOGIES,AS593MDR30135E,IDU, +AT&T TECHNOLOGIES,AS593MDR6,IDU, +AT&T TECHNOLOGIES,AS593MR630135E,IDU, +AT&T TECHNOLOGIES,AS595MDR630135A,IDU, +AT&T TECHNOLOGIES,AS953MDR630135E,IDU, +SIAE,ASNK6LAGS201024Q446MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNK6LAGS20128Q311MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNK6LAGS2016Q166MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNK6LAGS202048Q472MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNK6LAGS20256Q359MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNK6LAGS2032Q201MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNK6LAGS20472M2048QAM,Unknown, +SIAE,ASNK6LAGS204Q86MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNK6LAGS204QS72MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNK6LAGS20512Q400MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNK6LAGS2064Q262MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNKHP6L234M2048QAM,Unknown, +SIAE,ASNKHP6L4722048QAM,Unknown, +SIAE,ASNKHP6L472M2048QAM,Unknown, +SIAE,ASNKHP6LAGS20105MB,Unknown, +SIAE,ASNKHP6LAGS20132MB,Unknown, +SIAE,ASNKHP6LAGS20144MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNKHP6LAGS20157MB,Unknown, +SIAE,ASNKHP6LAGS20166MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNKHP6LAGS20180MB,Unknown, +SIAE,ASNKHP6LAGS20200MB,Unknown, +SIAE,ASNKHP6LAGS20207MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNKHP6LAGS20224MB,Unknown, +SIAE,ASNKHP6LAGS20234M2048Q,Unknown, +SIAE,ASNKHP6LAGS20234MB,Unknown, +SIAE,ASNKHP6LAGS20262MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNKHP6LAGS20311MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNKHP6LAGS20359MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNKHP6LAGS2036MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNKHP6LAGS20400MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNKHP6LAGS2043MB,Unknown, +SIAE,ASNKHP6LAGS20446MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNKHP6LAGS20472M2048Q,Unknown, +SIAE,ASNKHP6LAGS20472MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNKHP6LAGS2052M1024QAM,Unknown, +SIAE,ASNKHP6LAGS2052M1024QAMR,Unknown, +SIAE,ASNKHP6LAGS2072MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNKHP6LAGS2073MB,Unknown, +SIAE,ASNKHP6LAGS2084MB,Unknown, +SIAE,ASNKHP6LAGS2086MB,ODU,Comsearch Frequency Coordination Database +SIAE,ASNKHP6U234M2048QAM,Unknown, +SIAE,ASNKHPWB6LAGS2028M,Unknown, +SIAE,ASNKHPWB6LAGS204096QAM,Unknown, +SIAE,ASNKHPWB6LAGS2056M,Unknown, +AVIAT,AVIATNETWORKSINC,Unknown, +Fujitsu Limited,B258QMSS6G100HA,Unknown, +Fujitsu Limited,B25QMSS6G100HA,Unknown, +RF Technology,B8H8KURF704D,Unknown, +RF Technology,B8H8KURF705C,Unknown, +HARRIS,BC,Unknown, +HARRIS,BCK8GKDVM645641,Unknown, +HARRIS,BCK96KDVM612T1,Unknown, +HARRIS,BCK9GDVM6456402,Unknown, +HARRIS,BCK9GDVM645641,Unknown, +AVIAT,BCK9GFE830301,Unknown, +AVIAT,BCK9GHDVM645641,Unknown, +HARRIS,BCK9GKDM6162501,Unknown, +AVIAT,BCK9GKDM645642,Unknown, +AVIAT,BCK9GKDVM12T1,Unknown, +HARRIS,BCK9GKDVM45641,Unknown, +HARRIS,BCK9GKDVM6,IDU, +AVIAT,BCK9GKDVM6,IDU, +FARINON,BCK9GKDVM6,IDU, +HARRIS,BCK9GKDVM8T5,Unknown, +HARRIS,BCK9GKDVMG12T1,Unknown, +HARRIS,BCK9GKDVMG45641,Unknown, +HARRIS,BCK9GKE820101,Unknown, +AVIAT,BCK9GKFE820101,Unknown, +HARRIS,BCK9GKFE820101,Unknown, +HARRIS,BCK9GKFE8303,IDU, +AVIAT,BCK9GKFE8303,IDU, +HARRIS,BCK9GKMDM6162501,Unknown, +HARRIS,BCK9GKMKSTR06301,Unknown, +HARRIS,BCK9GKMSST06301,Unknown, +HARRIS,BCK9GKMST406301,Unknown, +HARRIS,BCK9GKMSTR06031,Unknown, +HARRIS,BCK9GKMSTR061,Unknown, +HARRIS,BCK9GKMSTR0630,IDU, +HARRIS,BCK9GKMSTR063O1,IDU, +HARRIS,BCK9GKMSTR6301,IDU, +HARRIS,BCK9GKMSTRO06301,IDU, +HARRIS,BCK9GKMSTRO1,IDU, +HARRIS,BCK9GKMSTRO6301,IDU, +HARRIS,BCK9GKMXTR06301,IDU, +HARRIS,BCK9GMSTR06301,IDU, +HARRIS,BCKGKDVM645641,IDU, +HARRIS,BCKGKMSTR06301,IDU, +HARRIS,BDK9GKDVM645641,IDU, +HARRIS,BKC9GKMSTR06301,IDU, +TADIRAN,BLWCM6HC1DS3,Unknown, +Broadcast Microwave Systems,BMT759P,Unknown, +ADVANTECH,BNB866DR6,IDU, +NEC,BSF6155S02A,Unknown, +NEC,BSF69155S02A,Unknown, +NEC,BSF6O155S02A,Unknown, +NEC,BSF6P150S02A,Unknown, +NEC,BSF6P155,IDU, +NEC,BSF6P15S02A,Unknown, +NEC,BSF6PI55S02A,Unknown, +NEC,BSF80N6P26S01A,Unknown, +NEC,BSF896P135MT01,Unknown, +NEC,BSF896P135SO9,Unknown, +NEC,BSF89N6026S01A,Unknown, +NEC,BSF89N6P,IDU, +NEC,BSF89NP135S02A,Unknown, +NEC,BSF89NP135T02,Unknown, +M/A COM,BV88U4MA6,IDU, +M/A COM,BV88U4MA7J3K,Unknown, +ERICSSON,C36L63AAA030A2A,Unknown, +MOTOROLA,C6601,Unknown, +MOTOROLA,CC6001,Unknown, +MOTOROLA,CC6002,IDU, +NORTEL,CDP7UJRD6C3,Unknown, +CERAGON,CERAGONNETWORKS,Unknown, +SAF TEHNIKA,CFIP6LUMINA,ODU,Comsearch Frequency Coordination Database +SAF TEHNIKA,CFIP6LUMINAHPAMR,Unknown, +SAF TEHNIKA,CFIP6LUMINAWEAKFEC,Unknown, +SAF TEHNIKA,CFIP6PHOENIX,ODU,Comsearch Frequency Coordination Database +SAF TEHNIKA,CFIP6PHOENIXLR,ODU,Comsearch Frequency Coordination Database +SAF TEHNIKA,CFIP7PHOENIXG2,Unknown,Comsearch Frequency Coordination Database +SAF TEHNIKA,CFIPINTEGRA,ODU, +CAMBIUM,CFIPINTEGRA,ODU, +SAF TEHNIKA,CFIPLIMINA6,Unknown, +SAF TEHNIKA,CFIPLUMINA,Unknown, +SAF TEHNIKA,CFIPLUMINA3,Unknown, +SAF TEHNIKA,CFIPLUMINA6,ODU,https://saftehnika.com/en/lumina +SAF TEHNIKA,CFIPLUMINA6AMR,Unknown, +SAF TEHNIKA,CFIPLUMINA6HPAMR,Unknown, +SAF TEHNIKA,CFIPLUMINA7,Unknown, +SAF TEHNIKA,CFIPLUMINAHP,Unknown, +SAF TEHNIKA,CFIPLUMINAUPPER6,Unknown, +SAF TEHNIKA,CFIPLUMINAW6HP,Unknown, +SAF TEHNIKA,CFIPLUMINAW6HP56MAM,Unknown, +SAF TEHNIKA,CFIPLUMINAW6HPAMR,Unknown, +SAF TEHNIKA,CFIPLUMINAWU6HP,Unknown, +SAF TEHNIKA,CFIPLUMINAWU6HPAMR,Unknown, +SAF TEHNIKA,CFIPPHOENIX6,Unknown, +SAF TEHNIKA,CFIPPHOENIX6ODU,ODU,ODU is in the model +SAF TEHNIKA,CFIPPHOENIXC11,Unknown, +SAF TEHNIKA,CFIPPHOENIXC6,Unknown, +SAF TEHNIKA,CFIPPHOENIXC6HPAMR,Unknown, +SAF TEHNIKA,CFIPPHOENIXG2SPLIT,ODU,Split is in the model +SAF TEHNIKA,CFIPPHOENIXL660MVHPA,Unknown, +SAF TEHNIKA,CFIPPHOENIXM6,Unknown, +SAF TEHNIKA,CFIPPHOENIXM6HP56MA,Unknown, +SAF TEHNIKA,CFIPPHOENIXODU,ODU,ODU is in the model +SAF TEHNIKA,CFIPPHOENIXU6ODU,ODU,ODU is in the model +SAF TEHNIKA,CFIPPHOENIXW6,Unknown, +SAF TEHNIKA,CFIPPHOENIXW6HPAMR,Unknown, +SAF TEHNIKA,CFIPPHOENIXXVHPU630M,Unknown, +SAF TEHNIKA,CFLSPRINTMXMREPEATER,IDU,Comsearch Frequency Coordination Database +CIELO,CG2X630115MB,Unknown, +CIELO,CG2X630138MB,Unknown, +CIELO,CG2X630161MB,Unknown, +CIELO,CG2X63046MB,Unknown, +CIELO,CG2X63092MB,Unknown, +CIELO,CG2X630HP,Unknown, +CIELO,CG2X656197MB,Unknown, +CIELO,CG2X656247MB,Unknown, +CIELO,CG2X656296MB,Unknown, +CIELO,CG2X656445MB,Unknown, +CIELO,CG2X65698MB,Unknown, +CIELO,CG630,ODU,Comsearch Frequency Coordination Database +NUBEAM,CG630,ODU,Comsearch Frequency Coordination Database +CIELO,CG656299MB,Unknown, +CIELO,CG656335MB,Unknown, +CIELO,CG6UC3064Q,Unknown, +CIELO,CGAIL6,IDU, +CIELO,CGX630157MBHPLL,Unknown, +CIELO,CGX630157MBLL,Unknown, +CIELO,CGX630183MBLL,Unknown, +CIELO,CGX630183MBLLVHP,Unknown, +CIELO,CGX630184MBLL,Unknown, +CIELO,CIELONETWORKS,Unknown, +STAR MICROWAVE,CIRIUSHM,ODU,Comsearch Frequency Coordination Database +STAR MICROWAVE,CIRIUSHMU610M256QAMR,Unknown, +STAR MICROWAVE,CIRIUSLM,ODU,Comsearch Frequency Coordination Database +STAR MICROWAVE,CIRIUSLM630M128QAMR,Unknown, +STAR MICROWAVE,CIRIUSLM630M256QAMR,Unknown, +STAR MICROWAVE,CIRIUSLMETHERNET,Unknown, +STAR MICROWAVE,CIRIUSSHC,Unknown, +STAR MICROWAVE,CIRIUSSHCAMR,Unknown, +NERA,CITYLINK,Unknown, +ERICSSON,CITYLINKCL06A,Unknown, +NERA,CITYLINKU6,Unknown, +NERA,CL06A,Unknown, +TADIRAN,CM6,Unknown, +MNI,CM6100BTDS3,Unknown, +TADIRAN,CM612DS1,Unknown, +TADIRAN,CM612DS1HSBDHP,Unknown, +MNI,CM612DS1STD,Unknown, +TADIRAN,CM61DS3,Unknown, +MNI,CM628D1D3SP,Unknown, +MNI,CM628DS1,Unknown, +TADIRAN,CM628DS1,Unknown, +MNI,CM628DS1HP,Unknown, +TADIRAN,CM64DS1N,Unknown, +MNI,CM68DS1DHP,Unknown, +MNI,CM68DS1STD,Unknown, +TADIRAN,CM6CH28DS1,Unknown, +TADIRAN,CM6HC12DS1,Unknown, +TADIRAN,CM6HC1DS3,Unknown, +TADIRAN,CM6HC28DS1,Unknown, +TADIRAN,CM6HC4DS1N,Unknown, +TADIRAN,CM6HC8DS1,Unknown, +TADIRAN,CM6HC8DSI1,Unknown, +TADIRAN,CM6HCMDS112DS1,Unknown, +MNI,CM6HCMDS18DS1,Unknown, +TADIRAN,CM6HCMDS1N,Unknown, +MNI,CM6OC3DHP,Unknown, +MNI,CM6OC3HP,Unknown, +NERA,COMPACTLINK,Unknown, +HARRIS,CONSTELLATION,Unknown, +HARRIS,CONSTELLATION1556,Unknown, +HARRIS,CONSTELLATION3DS3,Unknown, +HARRIS,CONSTELLATION6155,Unknown, +HARRIS,CONSTELLATION6155MB,Unknown, +HARRIS,CONSTELLATION616D1,Unknown, +HARRIS,CONSTELLATION616DS1,Unknown, +HARRIS,CONSTELLATION616T,Unknown, +HARRIS,CONSTELLATION628DS1,Unknown, +HARRIS,CONSTELLATION628DS1H,Unknown, +HARRIS,CONSTELLATION628DSI,Unknown, +HARRIS,CONSTELLATION628T,Unknown, +HARRIS,CONSTELLATION68DS1,Unknown, +HARRIS,CONSTELLATION68SD1,Unknown, +HARRIS,CONSTELLATION68T,Unknown, +HARRIS,CONSTELLATION68T1,Unknown, +HARRIS,CONSTELLATION6GHZ,Unknown, +HARRIS,CONSTELLATION6GHZ8DS1,Unknown, +AVIAT,CONSTELLATIONHIGHGAIN,Unknown, +HARRIS,CONSTELLATIONHIGHGAIN,Unknown, +HARRIS,CONSTELLATIONHRSCX06G2,Unknown, +HARRIS,CONSTELLATIONOC3,Unknown, +HARRIS,CONSTELLTION626DS1,Unknown, +HARRIS,CONSTEOATION6155MB,Unknown, +SAF TEHNIKA,CPIPPHOENIX6ODU,ODU,ODU is in the model +NORTEL,CSP7UJRD6C3,Unknown, +MOSELEY,CSU9WKDTVLINK101,Unknown, +AVIAT,CTI6V3,IDU, +AVIAT,CTI6V4,IDU, +AVIAT,CTRE6HPL6,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6HPU630M1024Q230,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6HPU630M1024Q230MAMR,Unknown, +AVIAT,CTRE6HPU630M128Q155,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6HPU630M16Q90,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6HPU630M256Q180,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6HPU630M32Q108,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6HPU630M512Q210,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6HPU630M64Q135,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6HPU630MACM,Unknown, +AVIAT,CTRE6HPU630MQPSK39,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6HPV2U630MACM,Unknown, +AVIAT,CTRE6SPL610M128Q50,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6SPL610M16Q30,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6SPL610M256Q57,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6SPL610M32Q36,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6SPL610M512Q66,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6SPL610M64Q45,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6SPL630M1024Q230,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6SPL630M128Q155,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6SPL630M16Q90,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6SPL630M256Q180,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6SPL630M256Q180MA,Unknown, +AVIAT,CTRE6SPL630M32Q108,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6SPL630M512Q210,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6SPL630M64Q135,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6SPL630MQPSK39,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL610MACM,Unknown, +NEC,CTRE6V2HL610MACM,Unknown, +AVIAT,CTRE6V2HL630M,Unknown, +AVIAT,CTRE6V2HL630M101024Q230,Unknown, +AVIAT,CTRE6V2HL630M1024Q230,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL630M1024Q30M,Unknown, +AVIAT,CTRE6V2HL630M128Q130M,Unknown, +AVIAT,CTRE6V2HL630M128Q155,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL630M16Q90,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL630M256Q180,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL630M32Q108,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL630M32Q108VVV,Unknown, +AVIAT,CTRE6V2HL630M512Q210,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL630M64Q1330M,Unknown, +AVIAT,CTRE6V2HL630M64Q135,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL630MACM,Unknown, +AVIAT,CTRE6V2HL630MQPSK,Unknown, +AVIAT,CTRE6V2HL630MQPSK39,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL660M1024Q436,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL660M128Q323,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL660M16Q150,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL660M256Q373,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL660M32Q228,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL660M512Q417,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL660M64Q250,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HL660MQPSK74,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HPL630M1024Q230,Unknown, +AVIAT,CTRE6V2HPL630M1024QAMA,Unknown, +AVIAT,CTRE6V2HPL630MACM,Unknown, +AVIAT,CTRE6V2HPU610MACM,Unknown, +AVIAT,CTRE6V2HPU630M1024Q230,Unknown, +AVIAT,CTRE6V2HPU630MACM,Unknown, +AVIAT,CTRE6V2HU610M1024Q,Unknown, +AVIAT,CTRE6V2HU610M1024Q71,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU610M128Q,Unknown, +AVIAT,CTRE6V2HU610M128Q50,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU610M16Q,Unknown, +AVIAT,CTRE6V2HU610M16Q30,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU610M256Q,Unknown, +AVIAT,CTRE6V2HU610M256Q57,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU610M32Q,Unknown, +AVIAT,CTRE6V2HU610M32Q36,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU610M512Q,Unknown, +AVIAT,CTRE6V2HU610M512Q66,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU610M64Q,Unknown, +AVIAT,CTRE6V2HU610M64Q45,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU610MQPSK,Unknown, +AVIAT,CTRE6V2HU610MQPSK12,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU63030MQPSK39,Unknown, +AVIAT,CTRE6V2HU630M,Unknown, +AVIAT,CTRE6V2HU630M1024Q230,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU630M1024Q237,Unknown, +AVIAT,CTRE6V2HU630M128Q155,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU630M1630MACM,Unknown, +AVIAT,CTRE6V2HU630M16Q90,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU630M256Q180,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU630M3230MACM,Unknown, +AVIAT,CTRE6V2HU630M32Q108,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU630M512Q210,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU630M512Q2130M,Unknown, +AVIAT,CTRE6V2HU630M64Q135,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2HU630MACM,Unknown, +AVIAT,CTRE6V2HU630MQPSK39,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2SL630M1024Q230,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2SL630M128Q155,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2SL630M16Q90,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2SL630M256Q180,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2SL630M32Q108,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2SL630M512Q210,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2SL630M64Q135,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2SL630MQPSK39,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2SU610M1024Q71O,Unknown, +AVIAT,CTRE6V2SU610M128Q50OD,Unknown, +AVIAT,CTRE6V2SU610M16Q30ODU,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2SU610M256Q57OD,Unknown, +AVIAT,CTRE6V2SU610M32Q36ODU,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2SU610M64Q45ODU,ODU,Comsearch Frequency Coordination Database +AVIAT,CTRE6V2SU610MQPSKQ12O,Unknown, +AVIAT,CTRI6,IDU, +NERA,CX06AXA1,Unknown, +HARRIS,CX06G16D1,Unknown, +CALIFORNIA MICROWAVE,CX64B,Unknown, +CALIFORNIA MICROWAVE,CX64C,Unknown, +NORTEL,CX7UJRD6C3,Unknown, +NORTEL,CXP6UJRD6C3,Unknown, +NORTEL,CXP7U6X30A1,Unknown, +NORTEL,CXP7UDRD6C3,Unknown, +NORTEL,CXP7UHRD6A1,Unknown, +NORTEL,CXP7UJ6X,IDU, +NORTEL,CXP7UJR6C3,Unknown, +NORTEL,CXP7UJRD6,IDU, +NORTEL,CXP7UJRDA2,Unknown, +NORTEL,CXP7UJRDC2,Unknown, +NORTEL,CXP7UJRDU6C2,Unknown, +NORTEL,CXP7UJRUD6C3,Unknown, +NORTEL,CXP7UJX30A1,Unknown, +NORTEL,CXP7URD6C3,Unknown, +NORTEL,CXPUJ6X30A1,Unknown, +NORTEL,CXPUJRD6A2,Unknown, +NORTEL,CXPUJRD6C3,Unknown, +Cablewave Systems,DA659,Unknown, +MRC,DAR068T,Unknown, +MRC,DAR068T10A,Unknown, +MRC,DAR6,Unknown, +VISLINK,DAR6,Unknown, +MRC,DAR68T,Unknown, +MRC,DAR6HP,Unknown, +MRC,DAR6PLUS,Unknown, +MRC,DAR6PLUSHP,Unknown, +VISLINK,DARPLUS64QAM,Unknown, +MRC,DARPLUSANALOG,Unknown, +TADIRAN,DF17CUOMEGA6,Unknown, +WATERLEAF INTERNATIONAL LLC,DFN6500,IDU,Comsearch Frequency Coordination Database +DMC,DMC6QLU45,Unknown, +AT&T TECHNOLOGIES,DR630,Unknown, +AT&T TECHNOLOGIES,DR630135,IDU, +AT&T TECHNOLOGIES,DR630135E,IDU, +Gabriel Electronics,DRFB8W5971SE,Unknown, +MRC,DRP059T10A,Unknown, +VISLINK,DRP059T10A,Unknown, +MRC,DRP068,IDU, +MOSELEY,DTVLINK,Unknown, +MOSELEY,DTVLINKA,Unknown, +MICROWAVE SOURCES CO,DUM6VJMT5000C,Unknown, +HARRIS,DVM1145,Unknown, +HARRIS,DVM45,Unknown, +HARRIS,DVM4564,Unknown, +HARRIS,DVM612,Unknown, +HARRIS,DVM612T,Unknown, +HARRIS,DVM612T1,Unknown, +FARINON,DVM612T2,Unknown, +HARRIS,DVM616T,Unknown, +HARRIS,DVM616T1,Unknown, +HARRIS,DVM616T2,Unknown, +HARRIS,DVM645,Unknown, +FARINON,DVM645,Unknown, +HARRIS,DVM64564,Unknown, +HARRIS,DVM645641,Unknown, +HARRIS,DVM68T,Unknown, +HARRIS,DVM68T5,Unknown, +HARRIS,DVM6XT,Unknown, +VISLINK,DXL068T1,Unknown, +MRC,DXL5000,Unknown, +VISLINK,DXL5000,Unknown, +MRC,DXL5000AMR,Unknown, +DMC,DXR762,Unknown, +DMC,DXR7624DS1,Unknown, +DMC,DXR762DS3,Unknown, +DMC,DXR768,IDU, +DMC,DYH6RMDMC10S04,Unknown, +DMC,DYHDMC6,IDU, +AVIAT,DYHDMC6,IDU, +EXALT,E00061F128Q30MHZ,Unknown, +EXALT,E00061F256Q30MHZ,Unknown, +EXALT,E00061F64Q30MHZ,Unknown, +EXALT,E00061F64Q5MHZ,Unknown, +EXALT,E00081F128Q30MHZ,Unknown, +EXALT,E00081F16Q30MHZ,Unknown, +EXALT,E00081F256Q30MHZ,Unknown, +EXALT,E00081F32Q30MHZ,Unknown, +EXALT,E00081F64Q30MHZ,Unknown, +EXALT,E00081F64Q5MHZ,Unknown, +EXALT,E00081FQPSK30MHZ,Unknown, +EXALT,E00091FE604FXY64Q30MHZ,Unknown, +AVIAT,E300HP656M256Q366MB,Unknown, +AVIAT,E300HPL61012832T1,Unknown, +AVIAT,E300HPL630M128Q100DS1,Unknown, +AVIAT,E300HPL630M16Q87MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E300HPL630M16Q88MB,Unknown, +AVIAT,E300HPL630M189Q256MB,Unknown, +AVIAT,E300HPL630M256Q,Unknown, +AVIAT,E300HPL630M256Q179MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E300HPL630M256Q189MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E300HPL630M64Q135MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E300HPL630M64Q138MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E300HPL630M64Q139MB,Unknown, +AVIAT,E300HPL630MQPSK43MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E300HPL630MQPSK44MB,Unknown, +AVIAT,E300HPL660M,Unknown, +AVIAT,E300HPL660M256Q365MB,Unknown, +AVIAT,E300HPU61012832T1,Unknown, +AVIAT,E300HPU61012832TI,Unknown, +AVIAT,E300HPU61025636T1,Unknown, +AVIAT,E300HPU6106429T1,Unknown, +AVIAT,E300HPU610M64QAM45MB,Unknown, +AVIAT,E300HPU610M64QDS345MB,Unknown, +AVIAT,E300HPU6139MB64QAM30,Unknown, +AVIAT,E300HPU6189MB256QAM30,Unknown, +AVIAT,E300HPU630M128Q100DS1,Unknown, +AVIAT,E300HPU6375M32Q8DS1,Unknown, +AVIAT,E3HL630M128Q100T1,Unknown, +AVIAT,E3HL630M128Q100TMG,Unknown, +AVIAT,E3HL630M16Q78MB,Unknown, +AVIAT,E3HL630M16Q87MB,Unknown, +AVIAT,E3HL630M256Q115T,Unknown, +AVIAT,E3HL630M256Q179MB,Unknown, +AVIAT,E3HL630M256Q189MB,Unknown, +AVIAT,E3HL630M64Q135MB,Unknown, +AVIAT,E3HL630M64Q138MB,Unknown, +AVIAT,E3HL630M64Q87T1,Unknown, +AVIAT,E3HL630MQPSK38MB,Unknown, +AVIAT,E3HL630MQPSK43MB,Unknown, +AVIAT,E3HL664Q10M29T1,Unknown, +AVIAT,E3HPL664Q10M29T1,Unknown, +AVIAT,E3HU630M256Q179MB,Unknown, +AVIAT,E3HU630M256Q189MB,Unknown, +AVIAT,E3HU630M64Q135MB,Unknown, +AVIAT,E3HU630M64Q138MB,Unknown, +AVIAT,E3HU63M7532Q8T1,Unknown, +EXALT,E600511,IDU, +EXALT,E6005511256Q30MHZ,Unknown, +AVIAT,E600HLP630M256Q179MB,Unknown, +AVIAT,E600HPL610M64Q45MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL630M,Unknown, +AVIAT,E600HPL630M128Q154MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL630M128QOC3,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL630M128QOC3157,Unknown, +AVIAT,E600HPL630M16Q,Unknown, +AVIAT,E600HPL630M16Q78MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL630M16Q87MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL630M256Q,Unknown, +AVIAT,E600HPL630M256Q179MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL630M256Q179MBA,Unknown, +AVIAT,E600HPL630M256Q189MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL630M256Q189MBA,Unknown, +AVIAT,E600HPL630M32QLL,Unknown, +AVIAT,E600HPL630M64Q,Unknown, +AVIAT,E600HPL630M64Q135MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL630M64Q135MBAM,Unknown, +AVIAT,E600HPL630M64Q138MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL630M64QLL,Unknown, +AVIAT,E600HPL630MACMT,Unknown, +AVIAT,E600HPL630MQPSK,Unknown, +AVIAT,E600HPL630MQPSK38MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL630MQPSK43MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL650M16Q126MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL650M16Q147MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL650M256Q296MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL650M256Q317MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL650M64Q211MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL650M64Q232MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL650MQPSK62MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL650MQPSK73MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL660M16Q148MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL660M16Q183MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL660M256Q345MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL660M256Q366MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL660M64Q246MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL660M64Q270MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPL660MQPSK73MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU610M128Q50MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU610M16Q24MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU610M16Q29MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU610M256Q,Unknown, +AVIAT,E600HPU610M256Q56MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU610M256Q56MBAM,Unknown, +AVIAT,E600HPU610M256Q60MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU610M256Q60MBAM,Unknown, +AVIAT,E600HPU610M64Q29T,Unknown, +AVIAT,E600HPU610M64Q40MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU610M64Q42MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU610M64Q45MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU610MQPSK14MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU6135MB64QAM135,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU630M,Unknown, +AVIAT,E600HPU630M128Q154MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU630M128QOC3,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU630M154MB,Unknown, +AVIAT,E600HPU630M16Q78MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU630M16Q87MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU630M256Q178MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU630M256Q179MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU630M256Q189MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU630M256Q189MBA,Unknown, +AVIAT,E600HPU630M32QLL,Unknown, +AVIAT,E600HPU630M64Q135MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU630M64Q138MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU630M64QLL,Unknown, +AVIAT,E600HPU630MQPSK38MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU630MQPSK43MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU63M7532Q12MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600HPU65M128Q16T24MB,Unknown, +AVIAT,E600HPUP10M64Q45MB,Unknown, +AVIAT,E600HU630M256QAM189MB,Unknown, +AVIAT,E600HUP610M64Q45MB,Unknown, +AVIAT,E600SPL610M128Q50MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL610M16Q29MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL610M256Q60MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL610M64Q42MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL610M64Q45MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL610MQPSK11MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL610MQPSK14MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL630M128Q154MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL630M128QOC3,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL630M16Q78MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL630M16Q87MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL630M256Q179MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL630M256Q189MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL6,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL630M64Q135MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL630M64Q138MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL630MQPSK38MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL630MQPSK43MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPL660M256Q345MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU610M,Unknown, +AVIAT,E600SPU610M128Q50MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU610M16Q29MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU610M256Q36T55MB,Unknown, +AVIAT,E600SPU610M256Q56MBAM,Unknown, +AVIAT,E600SPU610M256Q60MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU6,ODU, +AVIAT,E600SPU610M60MBAMR,Unknown, +AVIAT,E600SPU610M64Q42MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU610M64Q45MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU610M64QDS3,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU610MQPSK14MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU630M128QOC3,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU630M154MB,Unknown, +AVIAT,E600SPU630M16Q87MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU630M256Q179MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU630M256Q189MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU630M64Q135MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU630M64Q138MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU630MQPSK38MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600SPU63M7532Q12MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600V2HPL630M,Unknown, +AVIAT,E600V2HPL630M128Q154MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600V2HPL630M16Q87MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600V2HPL630M256Q189MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600V2HPL630M64Q135MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600V2HPL630M64Q138MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600V2HPL630MQPSK38MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600V2HPU630M64Q135MB,ODU,Comsearch Frequency Coordination Database +AVIAT,E600V2SPL630M128Q154MB,ODU,Comsearch Frequency Coordination Database +EXALT,E60561164Q10MHZ,Unknown, +EXALT,E606511,IDU, +EXALT,E6500511,IDU, +EXALT,E650511,IDU, +EXALT,E65055164QQAM,Unknown, +EXALT,E65061164Q10M,Unknown, +EXALT,E6551164Q10M,Unknown, +EXALT,E656511128Q10MHZ,Unknown, +EXALT,E656511256Q5MHZ,Unknown, +EXALT,E65651164Q10MHZ,Unknown, +AVIAT,E6HL6135MB64Q30M,Unknown, +AVIAT,E6HL6135MB64Q30MH,Unknown, +AVIAT,E6HL6135MB64QAM30MH,Unknown, +AVIAT,E6HL6153MB064Q30M,Unknown, +AVIAT,E6HL6155MB128QAM30M,ODU,Comsearch Frequency Coordination Database +AVIAT,E6HL630M032Q132MB,Unknown, +AVIAT,E6HL630M227R70AMR,Unknown, +AVIAT,E6HL630M256Q179MB,Unknown, +AVIAT,E6HL630M27R70AMR,Unknown, +AVIAT,E6HL630M64Q135MB,Unknown, +AVIAT,E6HL630M64Q87T1,Unknown, +AVIAT,E6HL660M64Q246MB,Unknown, +AVIAT,E6HL6LL135MB64Q30M,Unknown, +AVIAT,E6HP11,Unknown, +AVIAT,E6HPL6,ODU,Comsearch Frequency Coordination Database +AVIAT,E6HPU610M128Q50MBR70,Unknown, +AVIAT,E6HPU610M16Q28MBR70,Unknown, +AVIAT,E6HPU610M256Q56MBR70,Unknown, +AVIAT,E6HPU610M32Q35MBR70,Unknown, +AVIAT,E6HPU610M64Q45MBR70,Unknown, +AVIAT,E6HPU610MQPSK12MBR70,Unknown, +AVIAT,E6HPU630M1024Q227R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6HPU630M128Q155R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6HPU630M16Q90R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6HPU630M256Q180R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6HPU630M32Q108R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6HPU630M512Q200R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6HPU630M64Q132R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6HPU630MACM,Unknown, +AVIAT,E6HPU630MQPSK39R,Unknown, +AVIAT,E6HPU630MQPSK39R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6HU30M256Q179MB,Unknown, +AVIAT,E6HU30M256Q189MB,Unknown, +AVIAT,E6HU610M64QAM29T,Unknown, +AVIAT,E6HU6135MB64QAM30MH,ODU,Comsearch Frequency Coordination Database +AVIAT,E6HU6153MB064Q30M,Unknown, +AVIAT,E6HU6155MB128QAM30M,ODU,Comsearch Frequency Coordination Database +AVIAT,E6HU630M032Q132MB,Unknown, +AVIAT,E6HU630M180R70,Unknown, +AVIAT,E6HU630M180R70AMR,Unknown, +AVIAT,E6HU630M200R70,Unknown, +AVIAT,E6HU630M64QOC3L,Unknown, +EXALT,E6LF84H0252CY64Q30MHZ,Unknown, +EXALT,E6LF84H0252XY12860,Unknown, +EXALT,E6LF84H0252XY128Q30MHZ,Unknown, +EXALT,E6LF84H0252XY128Q40MHZ,Unknown, +EXALT,E6LF84H0252XY128Q60MHZ,Unknown, +EXALT,E6LF84H0252XY1660MHZ,Unknown, +EXALT,E6LF84H0252XY16Q30MHZ,Unknown, +EXALT,E6LF84H0252XY16Q40MHZ,Unknown, +EXALT,E6LF84H0252XY16Q60MHZ,Unknown, +EXALT,E6LF84H0252XY25660,Unknown, +EXALT,E6LF84H0252XY256Q30MHZ,Unknown, +EXALT,E6LF84H0252XY256Q40MHZ,Unknown, +EXALT,E6LF84H0252XY256Q60MHZ,Unknown, +EXALT,E6LF84H0252XY3260MHZ,Unknown, +EXALT,E6LF84H0252XY32Q30MHZ,Unknown, +EXALT,E6LF84H0252XY32Q40MHZ,Unknown, +EXALT,E6LF84H0252XY32Q60MHZ,Unknown, +EXALT,E6LF84H0252XY4Q30MHZ,Unknown, +EXALT,E6LF84H0252XY4Q40MHZ,Unknown, +EXALT,E6LF84H0252XY4Q60MHZ,Unknown, +EXALT,E6LF84H0252XY512Q30MHZ,Unknown, +EXALT,E6LF84H0252XY512Q40MHZ,Unknown, +EXALT,E6LF84H0252XY512Q60MHZ,Unknown, +EXALT,E6LF84H0252XY6460,Unknown, +EXALT,E6LF84H0252XY64Q30MHZ,Unknown, +EXALT,E6LF84H0252XY64Q40MHZ,Unknown, +EXALT,E6LF84H0252XY64Q60MHZ,Unknown, +EXALT,E6LF84H0252XY64QAM10M,Unknown, +EXALT,E6LF84H0252XYQPSK30MHZ,Unknown, +EXALT,E6LF84H0252XYQPSK60MHZ,Unknown, +EXALT,E6LF84H252XY12830,Unknown, +EXALT,E6LF84H252XY12860,Unknown, +EXALT,E6LF84H252XY1630,Unknown, +EXALT,E6LF84H252XY1660,Unknown, +EXALT,E6LF84H252XY25630,Unknown, +EXALT,E6LF84H252XY25660,Unknown, +EXALT,E6LF84H252XY3230,Unknown, +EXALT,E6LF84H252XY3260,Unknown, +EXALT,E6LF84H252XY430,Unknown, +EXALT,E6LF84H252XY51230,Unknown, +EXALT,E6LF84H252XY51260,Unknown, +EXALT,E6LF84H252XY6430,Unknown, +EXALT,E6LF84H252XY6460,Unknown, +EXALT,E6LF84H252XY64QAM10M,Unknown, +EXALT,E6LF84H262XY12830,Unknown, +AVIAT,E6SL630M64QOC3L,ODU,Comsearch Frequency Coordination Database +AVIAT,E6SPL610M56M256QAMR,Unknown, +AVIAT,E6SPL630M1024Q227R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6SPL630M128Q155R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6SPL630M16Q90R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6SPL630M256Q180R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6SPL630M32Q108R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6SPL630M512Q200R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6SPL630M64Q132R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6SPL630MQPSK39R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6SPU610M128Q50MBR70,Unknown, +AVIAT,E6SU610M56R70,Unknown, +EXALT,E6UF84H0160XY128Q30MHZ,Unknown, +EXALT,E6UF84H0160XY16Q30MHZ,Unknown, +EXALT,E6UF84H0160XY256Q30MHZ,Unknown, +EXALT,E6UF84H0160XY32Q30MHZ,Unknown, +EXALT,E6UF84H0160XY4Q30MHZ,Unknown, +EXALT,E6UF84H0160XY512Q30MHZ,Unknown, +EXALT,E6UF84H0160XY64Q30MHZ,Unknown, +AVIAT,E6V2EHU630M,Unknown, +AVIAT,E6V2HL610M1024Q73R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL610M1024QRAC707,Unknown, +AVIAT,E6V2HL610M128Q51R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL610M128QRAC7051,Unknown, +AVIAT,E6V2HL610M16QRAC7029M,Unknown, +AVIAT,E6V2HL610M256Q56R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL610M256QRAC7056,Unknown, +AVIAT,E6V2HL610M32QRAC7036M,Unknown, +AVIAT,E6V2HL610M512Q67R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL610M512QRAC7067,Unknown, +AVIAT,E6V2HL610M64Q45R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL610M64QRAC7045M,Unknown, +AVIAT,E6V2HL610MQPSKRAC7013,Unknown, +AVIAT,E6V2HL630M,Unknown, +AVIAT,E6V2HL630M1024Q227R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL630M128Q155R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL630M16Q90R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL630M180R7,Unknown, +AVIAT,E6V2HL630M2048Q245R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL630M245R7ACM,Unknown, +AVIAT,E6V2HL630M245R7AMR,Unknown, +AVIAT,E6V2HL630M256Q180R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL630M266R7,Unknown, +AVIAT,E6V2HL630M266R7AMR,Unknown, +AVIAT,E6V2HL630M267R7ACM,Unknown, +AVIAT,E6V2HL630M32Q108R70,Unknown, +AVIAT,E6V2HL630M32Q109R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL630M4096Q267R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL630M512Q200R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL630M64Q132R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL630MQPSK39R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL660M,Unknown, +AVIAT,E6V2HL660M1024Q454R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL660M128Q300R70,Unknown, +AVIAT,E6V2HL660M128Q301R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL660M16Q167R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL660M2048Q491R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL660M256Q343R70,Unknown, +AVIAT,E6V2HL660M256Q344R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL660M32Q216R70,Unknown, +AVIAT,E6V2HL660M32Q217R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL660M4096Q534R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL660M512Q403R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL660M534R7ACM,Unknown, +AVIAT,E6V2HL660M64Q267R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HL660MQPSK78R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HPL630MACMR70,Unknown, +AVIAT,E6V2HU610M1024Q73R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU610M128Q51R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU610M16Q29R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU610M256Q56R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU610M32Q36R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU610M512Q67R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU610M56R70ACM,Unknown, +AVIAT,E6V2HU610M64Q45R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU610M73R70ACM,Unknown, +AVIAT,E6V2HU610MQPSK13R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU630M1024Q227R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU630M128Q155R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU630M16Q90R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU630M2048Q245R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU630M245R7,Unknown, +AVIAT,E6V2HU630M256Q180R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU630M32Q108R70,Unknown, +AVIAT,E6V2HU630M32Q109R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU630M4096Q266R70,Unknown, +AVIAT,E6V2HU630M4096Q267R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU630M512Q200R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU630M64Q132R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU630MQPSK39R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU63M7532Q11R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU65M128Q22R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU65M16Q12R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2HU65M256Q26R70,Unknown, +AVIAT,E6V2HU65M32Q14R70,Unknown, +AVIAT,E6V2HU65M64Q18R70,Unknown, +AVIAT,E6V2SL610M64Q45R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SL630M1024Q227R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SL630M128Q155R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SL630M16Q90R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SL630M2048Q245R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SL630M227R7ACM,Unknown, +AVIAT,E6V2SL630M256Q180R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SL630M266R7ACM,Unknown, +AVIAT,E6V2SL630M267R7ACM,Unknown, +AVIAT,E6V2SL630M32Q108R70,Unknown, +AVIAT,E6V2SL630M32Q109R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SL630M4096Q266R70,Unknown, +AVIAT,E6V2SL630M4096Q267R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SL630M512Q200R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SL630M64Q132R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SL630MQPSK39R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SL660M256Q344R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU610M1024Q73R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU610M128Q50R70,Unknown, +AVIAT,E6V2SU610M128Q51R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU610M16Q28R70,Unknown, +AVIAT,E6V2SU610M16Q29R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU610M256Q56R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU610M32Q35R70,Unknown, +AVIAT,E6V2SU610M32Q36R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU610M512Q67R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU610M56R70,Unknown, +AVIAT,E6V2SU610M64Q45R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU610MQPSK12R70,Unknown, +AVIAT,E6V2SU610MQPSK13R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU630M1024Q227R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU630M128Q155R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU630M16Q90R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU630M2048Q245R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU630M256Q180R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU630M267R7ACM,Unknown, +AVIAT,E6V2SU630M32Q109R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU630M4096Q267R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU630M512Q200R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU630M64Q132R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU630MQPSK39R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU65M128Q22R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU65M16Q12R70,ODU,Comsearch Frequency Coordination Database +AVIAT,E6V2SU65M256Q26R70,Unknown, +AVIAT,E6V2SU65M32Q14R70,Unknown, +AVIAT,E6V2SU65M64Q18R70,Unknown, +PENINSULA,EAK2A201,Unknown, +DMC,ECLIPSE,Unknown, +STRATEX,ECLIPSE,Unknown, +DMC,ECLIPSECONNECT,IDU, +STRATEX,ECLIPSECONNECT,IDU, +AVIAT,ECLIPSECONNECT,IDU, +HARRIS,ECLIPSECONNECT,IDU, +AVIAT,ECLIPSEIRU,IDU, +HARRIS,ECLIPSEODU,ODU,ODU is in the model +STRATEX,ECLIPSEODU,ODU,ODU is in the model +BRIDGEWAVE,EFA06016030M256Q,Unknown, +BRIDGEWAVE,EFA0625230M128Q,Unknown, +BRIDGEWAVE,EFA0625230M16Q,Unknown, +BRIDGEWAVE,EFA0625230M256Q,Unknown, +BRIDGEWAVE,EFA0625230M32Q,Unknown, +BRIDGEWAVE,EFA0625230M4Q,Unknown, +BRIDGEWAVE,EFA0625230M64Q,Unknown, +AVIAT,EH300HPL630M256Q,Unknown, +AVIAT,EH6100DS1128QAM30,Unknown, +HARRIS,EH6100DS1128QAM30,Unknown, +HARRIS,EH684DS164QAM30,Unknown, +AVIAT,EHL610C3128QAM30,Unknown, +AVIAT,EHL61DS364QAM10,Unknown, +HARRIS,EHL61DS364QAM10,Unknown, +AVIAT,EHL61OC3128QAM30,Unknown, +AVIAT,EHL630M128QAMOC3,Unknown, +AVIAT,EHL684DS164QAM30,Unknown, +AVIAT,EHL684DS164QAM30MHZ,ODU,Comsearch Frequency Coordination Database +AVIAT,EHL68DS132QAM375,Unknown, +AVIAT,EHU616DS1128QAM5,Unknown, +AVIAT,EHU628DS164QAM10,Unknown, +AVIAT,EHU684DS164QAM30,Unknown, +PENINSULA,EK2A201,ODU,"Active repeater, tower mounted, low line loss" +REPEATER TECHNOLOGIES,EK2A201,ODU,"Active repeater, tower mounted, low line loss" +PENINSULA,EK2A201RF6000EEW,Unknown, +EXALT,EPLL60GADAP60MF,Unknown, +BRIDGEWAVE,ETHERFLEX630M128QAMR,Unknown, +NERA,ETHERLINK,Unknown, +NERA,ETHERLINK6GHZ,Unknown, +CERAGON,EVLOUTIONXPANDIPLNGHA,Unknown, +CERAGON,EVOI6160128HP,Unknown, +CERAGON,EVOLHL6,Unknown,Can be split or indoor +NERA,EVOLUTION,Unknown, +CERAGON,EVOLUTIONMETRO,Unknown, +NERA,EVOLUTIONMETRO,Unknown, +NERA,EVOLUTIONMETROHP,Unknown, +NERA,EVOLUTIONSERIESMETRO,Unknown, +NERA,EVOLUTIONSERIESXPAND,Unknown, +CERAGON,EVOLUTIONXPAND,Unknown, +NERA,EVOLUTIONXPAND,Unknown, +CERAGON,EVOLUTIONXPAND67G1024,Unknown, +CERAGON,EVOLUTIONXPAND67G128Q,Unknown, +CERAGON,EVOLUTIONXPAND67G16Q,Unknown, +CERAGON,EVOLUTIONXPAND67G256Q,Unknown, +CERAGON,EVOLUTIONXPAND67G32Q,Unknown, +CERAGON,EVOLUTIONXPAND67G4Q,Unknown, +CERAGON,EVOLUTIONXPAND67G512Q,Unknown, +CERAGON,EVOLUTIONXPAND67G64Q,Unknown, +NERA,EVOLUTIONXPANDETH8T1,Unknown, +NERA,EVOLUTIONXPANDHP,Unknown, +CERAGON,EVOLUTIONXPANDIP,Unknown, +CERAGON,EVOLUTIONXPANDIPLHHP,Unknown, +CERAGON,EVOLUTIONXPANDIPLNG,Unknown, +CERAGON,EVOLUTIONXPANDIPLNGHA,Unknown,Can be split or indoor +CERAGON,EVOLUTIONXPANDIPLNGHP,Unknown, +CERAGON,EVOLUTIONXPANDIPLONGH,Unknown, +NERA,EVOLXPANDIPAMR,Unknown, +NERA,EVOLXPANDIPHPAMR,Unknown, +NERA,EVOLXPANDIPLONGHAUL,Unknown, +NERA,EVOLXPANDIPLONGHAULA,Unknown, +NERA,EVOULTIONXPANDHP,Unknown, +NERA,EVOULUTIONXPANDHP,Unknown, +EXALT,EX11SGIGEHP,Unknown, +EXALT,EX6I,Unknown, +EXALT,EX6IDS3GIGE,Unknown, +EXALT,EX6IHP,Unknown, +EXALT,EX6IHPAMR,Unknown, +EXALT,EX6IL6GHZ30MHZ,Unknown, +EXALT,EX6IOC330GIGE,Unknown, +EXALT,EX6LI,Unknown, +EXALT,EX6LIHP,Unknown, +EXALT,EX6S,Unknown, +EXALT,EX6UIHP,Unknown, +EXALT,EXI6,Unknown, +EXALT,EXI6HP,Unknown, +EXALT,EXI6LHP,Unknown, +EXALT,EXIGIGE,Unknown, +EXALT,EXL61HP30M256QAMR,Unknown, +EXALT,EXL6I4DS3GIGE,Unknown, +EXALT,EXL6IDS3GIGEHP,Unknown, +EXALT,EXL6IDS3GIGESP,Unknown, +EXALT,EXL6IGIGE,Unknown, +EXALT,EXL6IHP,Unknown, +EXALT,EXL6IHP30M256QAMR,Unknown, +EXALT,EXL6IHPAMR,Unknown, +EXALT,EXL6IOC3GIGE,Unknown, +EXALT,EXL6ISPACM,Unknown, +EXALT,EXL6SGIGE,Unknown, +EXALT,EXL6SGIGEAMR,Unknown, +EXALT,EXL6SGIGEHP,Unknown, +EXALT,EXL6SGIGEHPAMR,Unknown, +EXALT,EXLSGIGEHP,Unknown, +EXALT,EXLXIHPAMR,Unknown, +EXALT,EXPAIRLRL660M256QAMR,Unknown, +EXALT,EXPAIRLRL660MHZ512QA,Unknown, +EXALT,EXPLOREAIRLR,Unknown, +EXALT,EXPLOREAIRLR0G,Unknown, +EXALT,EXPLOREAIRLR0GMAXTPUT,Unknown, +EXALT,EXPLOREAIRLR6G30M0G,Unknown, +EXALT,EXPLOREAIRLR6G60M0G,Unknown, +EXALT,EXPLOREAIRLR6GHZ,Unknown, +EXALT,EXPLOREAIRLRAMR,Unknown, +EXALT,EXPLOREAIRLRBG,Unknown, +EXALT,EXPLOREAIRLRBGBALANCED,Unknown, +EXALT,EXPLOREAIRLRMAXT,Unknown, +EXALT,EXPLOREAIRLRRC61150,Unknown, +EXALT,EXPLOREAIRU6GHZ,Unknown, +EXALT,EXRC611506LAMR,Unknown, +EXALT,EXRC611506U,Unknown, +EXALT,EXRC611506UAMR,Unknown, +EXALT,EXRC61150LR,Unknown, +EXALT,EXRC61150LRAMR,Unknown, +EXALT,EXS6G30M0G,Unknown, +EXALT,EXSGIGE128QAM,Unknown, +EXALT,EXTENDAIRLR6GHZ,Unknown, +EXALT,EXU6IDS3GIGEHP,Unknown, +EXALT,EXU6IGIGE,Unknown, +EXALT,EXU6IHP,Unknown, +EXALT,EXU6SGIGE,Unknown, +EXALT,EXU6SGIGEHPAMR,Unknown, +SIEMENS,F6P8BHRT664L,Unknown, +HARRIS,FAS6000,Unknown, +HARRIS,FAS600001,Unknown, +MRC,FC35DMZMRCFLR6HP,Unknown, +MRC,FC35DZMR6KG,Unknown, +MRC,FC35DZMR6KJ1,Unknown, +MRC,FC35DZMRCDAR451,Unknown, +MRC,FC35DZMRCFLH6,Unknown, +MRC,FC35DZMRCFLH627CS01,Unknown, +MRC,FC35DZMRCFLH6HP,Unknown, +MRC,FC35DZMRCFLR6,IDU, +MRC,FC35DZMRCOAR451,Unknown, +MRC,FCDZMRCFLR6,Unknown, +FARINON,FE793001,Unknown, +FARINON,FE794001,Unknown, +GRANGER,FHM49ADTR6452,Unknown, +CERAGON,FIBAIR1500HP6,IDU,Comsearch Frequency Coordination Database +CERAGON,FIBEAIR1500,IDU,Comsearch Frequency Coordination Database +CERAGON,FIBEAIR15286,Unknown, +CERAGON,FIBEAIR1528HP6,Unknown, +CERAGON,FIBEAIR1528SP6,Unknown, +CERAGON,FIBEAIR3200T6,Unknown, +CERAGON,FIBEAIRIP101828HPX,Unknown, +CERAGON,FIBEAIRIP106C3064QHP,Unknown, +CERAGON,FIBEAIRIP106HP3064Q,Unknown, +CERAGON,FIBEAIRIP10A6GHZ,Unknown, +CERAGON,FIBEAIRIP10C56,Unknown, +CERAGON,FIBEAIRIP10C6GHZ,Unknown, +CERAGON,FIBEAIRIP10HP6GHZ,ODU, +CERAGON,FIBEAIRIP10T6GHZ,Unknown, +CERAGON,FIBEAIRIP15006HP,Unknown, +CERAGON,FIBEAIRIP20B,Unknown, +CERAGON,FIBEAIRIP20S,ODU,Comsearch Frequency Coordination Database +CERAGON,FIBEAIRIPMAX2HP,Unknown, +CERAGON,FIBERAIR1500HP6,Unknown, +CERAGON,FIBERAIR15286,Unknown, +CERAGON,FIBERAIRIP20S,ODU,Comsearch Frequency Coordination Database +CERAGON,FIBREAIR15286,Unknown, +ALCATEL,FJ69408,Unknown, +FARINON,FL16,IDU, +AVIAT,FL16,IDU, +HARRIS,FL16,IDU, +MRC,FLH6,Unknown, +MRC,FLH6B,Unknown, +MRC,FLHDAR6HP,Unknown, +MRC,FLR068T10A,Unknown, +MRC,FLR6,Unknown, +MRC,FLR6T,Unknown, +NUCOMM,FT6,Unknown, +FARINON,FV60600A,Unknown, +FARINON,FV6F01,Unknown, +American Television,FYU4PB01,Unknown, +HARRIS,GABRIELELECTRONICSINC,Unknown, +NEC,GABRIELELECTRONICSINC,Unknown, +SKYRIDER,GENESIS6,Unknown, +WATERLEAF INTERNATIONAL LLC,GHR6702AAA,IDU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6L56M1024QL3,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6L56M128QL3,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6L56M16QL3,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6L56M256QL3,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6L56M32QL3,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6L56M512QL3,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6L56M64QL3,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6L56M8PSKL3,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6L56MQPSKL2,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6U30M1024QL2,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6U30M128QL2,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6U30M16QL2,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6U30M256QL2,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6U30M32QL2,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6U30M512QL2,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6U30M64QL2,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6U30M8PSKL2,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAL6U30MQPSKL2,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGALONGHAUL,IDU, +TRANGO,GIGALYNX,Unknown, +TRANGO,GIGALYNX6,Unknown, +TRANGO,GIGALYNX610M1024QAMR,Unknown, +TRANGO,GIGALYNX656M1024QAMR,Unknown, +TRANGO,GIGALYNX6AMR,Unknown, +TRANGO,GIGALYNXL630M1024QHP,Unknown, +TRANGO,GIGALYNXL656M1024QAM,Unknown, +TRANGO,GIGALYNXL656MHPAMR,Unknown, +TRANGO,GIGAO6L56M256Q,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAO6L56M512Q,ODU,Comsearch Frequency Coordination Database +TRANGO,GIGAORION656M1024QAM,Unknown, +TRANGO,GIGAORION6AMR,Unknown, +TRANGO,GIGAORION6GHZ,Unknown, +TRANGO,GIGAPLUS6,Unknown, +TRANGO,GIGAPLUS630M256QAMR,Unknown, +TRANGO,GIGAPLUS630MAMR,Unknown, +TRANGO,GIGAPLUS656M256QAMR,Unknown, +TRANGO,GIGAPLUS6HP30AMR,Unknown, +TRANGO,GIGAPLUS6HP30M,Unknown, +TRANGO,GIGAPLUS6HP30MAMR,Unknown, +TRANGO,GIGAPLUS8HP30MAMR,Unknown, +TRANGO,GIGAPLUSHP30MAMR,Unknown, +TRANGO,GIGAPRO6,Unknown, +TRANGO,GIGPLUS6HP30AMR,Unknown, +RF CENTRAL LLC,GL59H,Unknown, +GRANGER,GT684DS1,Unknown, +PROXIM,GX8006LNK,Unknown, +PROXIM,GX800U6LINK,Unknown, +PROXIM,GX8106LINK30M256QAMR,Unknown, +WESTERN MULTIPLEX,HBZ645,Unknown, +ALCATEL,HDV865MAIN,Unknown, +MOTOROLA,HEL3ABDR6C2,Unknown, +HARRIS,HESCX06G16D1,Unknown, +DRAGONWAVE,HORIZONCOMPACTL6GHZAMR,Unknown, +DRAGONWAVE,HORIZONCOMPACTL6GHZEP,Unknown, +CERAGON,HP106HP30128QAM5,Unknown, +CERAGON,HP106HP30256QAM6,Unknown, +CERAGON,HP106HP30256QAM7,Unknown, +COMMSCOPE,HP6107G,Unknown, +COMMSCOPE,HP659J,Unknown, +COMMSCOPE,HP859E,Unknown, +CERAGON,HPINDOOL6128Q,Unknown, +DRAGONWAVE,HPL6HC14430,Unknown, +DRAGONWAVE,HPL6HC19030,Unknown, +COMMSCOPE,HPX865D,Unknown, +HARRIS,HRCCX06G155M,IDU,Typo on the HRSCX06 +HARRIS,HRS06G28D1,IDU, +HARRIS,HRS0CX06G08D1,IDU, +HARRIS,HRS52L6,IDU, +HARRIS,HRS52LG03T33T1128QS,IDU, +HARRIS,HRS52U6,IDU, +HARRIS,HRS53L6,IDU, +HARRIS,HRS53U6,IDU, +HARRIS,HRSCS0628D1H,Unknown, +HARRIS,HRSCS06G08D1,Unknown, +HARRIS,HRSCS06G155M,IDU,Typo on the HRSCX06 +HARRIS,HRSCS06G16D1,Unknown, +HARRIS,HRSCS06G28D1,Unknown, +HARRIS,HRSCS06G4DS3,Unknown, +HARRIS,HRSCS06G4DS3H,Unknown, +HARRIS,HRSCX05G08D1,Unknown, +HARRIS,HRSCX05G4DS3,Unknown, +HARRIS,HRSCX06,IDU, +HARRIS,HRSCX07G155MH,Unknown, +HARRIS,HRSCX07G28D1,Unknown, +HARRIS,HRSCX0G28D1,Unknown, +HARRIS,HRSCX106G2801,Unknown, +HARRIS,HRSCX10G08D1,Unknown, +HARRIS,HRSCX29G4DS3NP,Unknown, +HARRIS,HRSCX6G155MH,Unknown, +HARRIS,HRSCX6G4DS3,Unknown, +HARRIS,HRSCXD6G28D1,Unknown, +HARRIS,HRSCXG06155M,Unknown, +HARRIS,HRSCXO6G08D1,Unknown, +HARRIS,HRSCXO6G155M,Unknown, +HARRIS,HRSCXO6G28D1,Unknown, +HARRIS,HRSCXO6G4DS3,Unknown, +HARRIS,HRSCXO6G4DS3HHS,Unknown, +HARRIS,HRSCZX06,IDU, +HARRIS,HRSMG06G3S1,Unknown, +HARRIS,HRSMG06G3S11,Unknown, +HARRIS,HRSSCX06G28D1,Unknown, +HARRIS,HRSTR52L6G155M,Unknown, +HARRIS,HRSTR52U6G12T1,Unknown, +HARRIS,HRSTR52U6G28T1,Unknown, +HARRIS,HRSTR52U6G8T1,Unknown, +HARRIS,HRWSCX06G28D1H,Unknown, +HARRIS,HRXCX06G08D1,Unknown, +HARRIS,HRXCX06,IDU, +HARRIS,HRXCX06G16D1,Unknown, +HARRIS,HRZCX06G08D1,Unknown, +HARRIS,HS64E06,IDU, +HARRIS,HSRCX06,IDU, +HARRIS,HSX659ARF,Unknown, +HARRIS,HT6I06G01S3H,Unknown, +HARRIS,HT6I06G01S3S,Unknown, +HARRIS,HT6I06G29D1H,Unknown, +AVIAT,HTSCS06,IDU, +WESTERN MULTIPLEX,HZB135,Unknown, +PROXIM,HZB26000,Unknown, +WESTERN MULTIPLEX,HZB26000,Unknown, +WESTERN MULTIPLEX,HZB4T6,Unknown, +PROXIM,HZB4T6,Unknown, +WESTERN MULTIPLEX,HZB4T625,Unknown, +WESTERN MULTIPLEX,HZB4T62M5,Unknown, +WESTERN MULTIPLEX,HZB6135,Unknown, +WESTERN MULTIPLEX,HZB616T,Unknown, +WESTERN MULTIPLEX,HZB6180,Unknown, +WESTERN MULTIPLEX,HZB645,Unknown, +WESTERN MULTIPLEX,HZB6455,Unknown, +WESTERN MULTIPLEX,HZB645WM645,Unknown, +AVIAT,I500HL630MAMR,Unknown, +AVIAT,I500SL630M128QOC3157M,Unknown, +AVIAT,I600,IDU, +AVIAT,I60HL30M128Q100T154M,Unknown, +AVIAT,I60HL630M16Q87MB,Unknown, +AVIAT,I61EL630M227R70ACM,Unknown, +AVIAT,I63EL610MACMR70,Unknown, +AVIAT,I63EL630M155R70,Unknown, +AVIAT,I63EL630MACMR70,Unknown, +AVIAT,I63EL630MACMR70ACM,Unknown, +AVIAT,I63EL660MACMR70,Unknown, +AVIAT,I63EU610MACMR70,Unknown, +AVIAT,I63EU630MACMR70,Unknown, +AVIAT,I63HL6,IDU, +AVIAT,I63HU6,IDU, +AVIAT,I63SL6155MB128Q30M,IDU,Comsearch Frequency Coordination Database +AVIAT,I63SL630MACMR70,Unknown, +AVIAT,I63SU610MACMR70,Unknown, +AVIAT,I63SU630M180R70,Unknown, +AVIAT,I63SU630MACMR70,Unknown, +AVIAT,I64630M267R70ACM,Unknown, +AVIAT,I64E630M227R70ACM,Unknown, +AVIAT,I64E630M267R70AMR,Unknown, +AVIAT,I64EK630M267R70AMR,Unknown, +AVIAT,I64EL603M267R70ACM,Unknown, +AVIAT,I64EL610M045R70,Unknown, +AVIAT,I64EL610M058R70,Unknown, +AVIAT,I64EL610M073R70ACM,Unknown, +AVIAT,I64EL610M073R70AMR,Unknown, +AVIAT,I64EL630M007R70ACM,Unknown, +AVIAT,I64EL630M132R70,Unknown, +AVIAT,I64EL630M180R70,Unknown, +AVIAT,I64EL630M180R709,Unknown, +AVIAT,I64EL630M180R70ACM,Unknown, +AVIAT,I64EL630M180R70ACR,Unknown, +AVIAT,I64EL630M180R70AMR,Unknown, +AVIAT,I64EL630M200R70,Unknown, +AVIAT,I64EL630M2267R70AMR,Unknown, +AVIAT,I64EL630M227R0ACM,Unknown, +AVIAT,I64EL630M227R70ACM,Unknown, +AVIAT,I64EL630M227R70AMR,Unknown, +AVIAT,I64EL630M22R70ACM,Unknown, +AVIAT,I64EL630M257R70ACM,Unknown, +AVIAT,I64EL630M26770AMR,Unknown, +AVIAT,I64EL630M267R70,Unknown, +AVIAT,I64EL6,ODU, +AVIAT,I64EL630M267R70ACM,Unknown, +AVIAT,I64EL630M267R70AMR,Unknown, +AVIAT,I64EL630M267RACM,Unknown, +AVIAT,I64EL630M27R70AMR,Unknown, +AVIAT,I64EL630MACMR70,Unknown, +AVIAT,I64EL630MACMR70ACM,Unknown, +AVIAT,I64EL63M227R70AMR,Unknown, +AVIAT,I64EL65M26R70,Unknown, +AVIAT,I64EL660M301R70,Unknown, +AVIAT,I64EL660M534R70ACM,Unknown, +AVIAT,I64EL660M537OC3ACM,Unknown, +AVIAT,I64EU610M03R70AMR,Unknown, +AVIAT,I64EU610M045R70,Unknown, +AVIAT,I64EU610M051R70,Unknown, +AVIAT,I64EU610M058R70,Unknown, +AVIAT,I64EU610M073R70ACM,Unknown, +AVIAT,I64EU610M073R70AMR,Unknown, +AVIAT,I64EU610M0R3R70AMR,Unknown, +AVIAT,I64EU630M15570,Unknown, +AVIAT,I64EU630M155R70,Unknown, +AVIAT,I64EU630M180R70,Unknown, +AVIAT,I64EU630M180R70189,Unknown, +AVIAT,I64EU630M227R70ACM,Unknown, +AVIAT,I64EU630M227R70AMR,Unknown, +AVIAT,I64EU630M267R70,Unknown, +AVIAT,I64EU630M267R70ACM,Unknown, +AVIAT,I64EU630M67R70ACM,Unknown, +AVIAT,I64HL610M058R70,Unknown, +AVIAT,I64HL610M073R70ACM,Unknown, +AVIAT,I64HL610M073R70AMR,Unknown, +AVIAT,I64HL630M155R70,Unknown, +AVIAT,I64HL630M179OC3,Unknown, +AVIAT,I64HL630M180R70,Unknown, +AVIAT,I64HL630M200R70,Unknown, +AVIAT,I64HL630M200R70ACM,Unknown, +AVIAT,I64HL630M227R70ACM,Unknown, +AVIAT,I64HL630M267R70,Unknown, +AVIAT,I64HL630M267R70ACM,Unknown, +AVIAT,I64HL630M267R770ACM,Unknown, +AVIAT,I64HL630MACMR70,Unknown, +AVIAT,I64HL630MACMR70AMR,Unknown, +AVIAT,I64HL630MACMR770AMR,Unknown, +AVIAT,I64HL660M534R70AMR,Unknown, +AVIAT,I64HL660M537OC3ACM,Unknown, +AVIAT,I64HL660MACMR70AMR,Unknown, +AVIAT,I64HU610M058R70,Unknown, +AVIAT,I64HU610M058R70AMR,Unknown, +AVIAT,I64HU610M073R70ACM,Unknown, +AVIAT,I64HU610M073R70AMR,Unknown, +AVIAT,I64HU610M73R70ACM,Unknown, +AVIAT,I64HU610MACMR70,Unknown, +AVIAT,I64HU630M180R70,Unknown, +AVIAT,I64HU630M180R70AMR,Unknown, +AVIAT,I64HU630M200R70,Unknown, +AVIAT,I64HU630M227R70ACM,Unknown, +AVIAT,I64HU630M266R70AMR,Unknown, +AVIAT,I64HU6360M266R70AMR,Unknown, +AVIAT,I64HU63M715R70,Unknown, +AVIAT,I64HU63M719R70ACM,Unknown, +AVIAT,I64HU65M27MBR70ACM,Unknown, +AVIAT,I64HU65M2HRSCX06G16D1H,Unknown, +AVIAT,I64S610M058R70,Unknown, +AVIAT,I64S630M267R70AMR,Unknown, +AVIAT,I64SL610M045R70,Unknown, +AVIAT,I64SL610M05058R70,Unknown, +AVIAT,I64SL610M051R70ACM,Unknown, +AVIAT,I64SL610M058R70,Unknown, +AVIAT,I64SL610M073R70ACM,Unknown, +AVIAT,I64SL610M073R70AMR,Unknown, +AVIAT,I64SL630M155R70,Unknown, +AVIAT,I64SL630M180R70,Unknown, +AVIAT,I64SL630M200R70,Unknown, +AVIAT,I64SL630M200R70AMR,Unknown, +AVIAT,I64SL630M267R70ACM,Unknown, +AVIAT,I64SL630M267R70AMR,Unknown, +AVIAT,I64SL630M26R70AMR,Unknown, +AVIAT,I64SL630MACMR70AMR,Unknown, +AVIAT,I64SL660MACMR70AMR,Unknown, +AVIAT,I64SU610M045R70,Unknown, +AVIAT,I64SU610M051R70,Unknown, +AVIAT,I64SU6,ODU, +AVIAT,I64SU610M073R70ACM,Unknown, +AVIAT,I64SU610M073R70AMR,Unknown, +AVIAT,I64SU610MACMR70,Unknown, +AVIAT,I64SU630M180R70,Unknown, +AVIAT,I64SU630M267R70AMR,Unknown, +AVIAT,I64SU63M715R70,Unknown, +AVIAT,I64SU63M719R70ACM,Unknown, +AVIAT,I64U610M073R70ACM,Unknown, +AVIAT,I699V3HL630M256Q189MB,Unknown, +AVIAT,I6HL630M256Q189MBAMR,Unknown, +AVIAT,I6OO30M128QOC3,Unknown, +AVIAT,I6OOHL630MAMR,Unknown, +AVIAT,I6U3SU610M56,Unknown, +AVIAT,I6V2HL6,IDU, +AVIAT,I6V2HU630M128Q155R70,IDU,Comsearch Frequency Coordination Database +AVIAT,I6V3,ODU, +AVIAT,I6V4EHL6,IDU, +AVIAT,I6V4EHU6,IDU, +AVIAT,I6V4EL6,IDU, +AVIAT,I6V4,IDU, +AVIAT,I6V4HL6,IDU, +AVIAT,I6V4HU6,IDU, +AVIAT,I6V4SL6,IDU, +AVIAT,I6V4SU6,IDU, +MNI,IDU,IDU,IDU is in the model +MOTOROLA,IEP89FC6601,IDU, +TELESCIENCES,IEP89FC6601,IDU, +AVIAT,II64EU630M267R70ACM,Unknown, +SAF TEHNIKA,INTEGRA,ODU,Comsearch Frequency Coordination Database +SAF TEHNIKA,INTEGRA,ODU, +AVIAT,INTEGRA,ODU, +NERA,INTERLINK,Unknown, +NERA,INTERLINKNL2006A,Unknown, +SAF TEHNIKA,INTG6L244M30SACM,Unknown, +SAF TEHNIKA,INTG6L491M60S2048QAMR,Unknown, +SAF TEHNIKA,INTG6U244M30SACM,Unknown, +SAF TEHNIKA,INTGS6148M30S,Unknown, +SAF TEHNIKA,INTGS6244M30SAMR,Unknown, +SAF TEHNIKA,INTGS6474M60W102460MA,Unknown, +SAF TEHNIKA,INTGS6491M60S204860M,Unknown, +SAF TEHNIKA,INTGS6491M60S204860MA,Unknown, +MDS,INTREPIDHCHPAMR,Unknown, +SAF TEHNIKA,INTS6123M30S,Unknown, +SAF TEHNIKA,INTS6148M30S,Unknown, +SAF TEHNIKA,INTS6173M30S,Unknown, +SAF TEHNIKA,INTS6197M30S,Unknown, +SAF TEHNIKA,INTS6222M30S,Unknown, +SAF TEHNIKA,INTS637M30S,Unknown, +SAF TEHNIKA,INTS6474M60W1024QAMR,Unknown, +SAF TEHNIKA,INTS673M30S,Unknown, +SAF TEHNIKA,INTS693M30S,Unknown, +SAF TEHNIKA,INTW6L481M60WAMR,Unknown, +SAF TEHNIKA,INTX61146M60ACM,Unknown, +SAF TEHNIKA,INTX6568M30ACM,Unknown, +CERAGON,IP1061828CXF,ODU,Comsearch Frequency Coordination Database +CERAGON,IP1061R10X128Q5,Unknown, +CERAGON,IP1061R30XA,Unknown, +CERAGON,IP106A,IDU,Comsearch Frequency Coordination Database +CERAGON,IP106C,ODU,Comsearch Frequency Coordination Database +CERAGON,IP106HP,ODU,Comsearch Frequency Coordination Database +CERAGON,IP106HP20256QAM,Unknown, +CERAGON,IP106HP28128QAM,Unknown, +CERAGON,IP106HP28256QAM,Unknown, +CERAGON,IP106HP2864QAM,Unknown, +CERAGON,IP106HP30128QAM,Unknown, +CERAGON,IP106HP30128QAM5,Unknown, +CERAGON,IP106HP3016QAM2,Unknown, +CERAGON,IP106HP30256Q7,Unknown, +CERAGON,IP106HP30256QAM,Unknown, +CERAGON,IP106HP,ODU, +CERAGON,IP106HP30256QAM7,Unknown, +CERAGON,IP106HP30256QAMMR,Unknown, +CERAGON,IP106HP30256QAMR,Unknown, +CERAGON,IP106HP3032QAM3,Unknown, +CERAGON,IP106HP3064QAM,Unknown, +CERAGON,IP106HP3064QAM4,Unknown, +CERAGON,IP106HP3064QAM5,Unknown, +CERAGON,IP106HP308PSK1,Unknown, +CERAGON,IP106HP30AMR,Unknown, +CERAGON,IP106HP30QPSK0,Unknown, +CERAGON,IP106HP30XA,Unknown, +CERAGON,IP106HS30256QAM,Unknown, +CERAGON,IP106T,IDU,Comsearch Frequency Coordination Database +MDS,IP106T30256AQM7,Unknown, +CERAGON,IP10C30AMR,Unknown, +CERAGON,IP10G16T1TSLTSUXPC,Unknown, +CERAGON,IP10HP6,Unknown, +CERAGON,IP10HP6GHZ,Unknown, +CERAGON,IP10HP6L,Unknown, +CERAGON,IP10HP6ODU,ODU,ODU is in the model +CERAGON,IP16HP10256QAM,Unknown, +CERAGON,IP20630,Unknown, +CERAGON,IP20660,Unknown, +CERAGON,IP206C,ODU, +CAMBIUM,IP206C,ODU, +CERAGON,IP206C,ODU,Comsearch Frequency Coordination Database +CERAGON,IP20A6C,ODU,Comsearch Frequency Coordination Database +CERAGON,IP20A6HP,ODU,Comsearch Frequency Coordination Database +CERAGON,IP20B2048QAMQPSK,Unknown, +CERAGON,IP20B61R,ODU, +CERAGON,IP20B6A,IDU, +AVIAT,IP20B6A,IDU, +CERAGON,IP20B6C,ODU,Comsearch Frequency Coordination Database +CERAGON,IP20B6HP,ODU, +CERAGON,IP20B6M60X9LQAAMR,Unknown, +CERAGON,IP20C,ODU,Comsearch Frequency Coordination Database +CERAGON,IP20D630XARFUD,Unknown, +CERAGON,IP20DD630X1024Q9,Unknown, +CERAGON,IP20DD630X1024QX,Unknown, +CERAGON,IP20DD630X128Q6,Unknown, +CERAGON,IP20DD630X16Q3,Unknown, +CERAGON,IP20DD630X2048QY,Unknown, +CERAGON,IP20DD630X256Q7,Unknown, +CERAGON,IP20DD630X32Q4,Unknown, +CERAGON,IP20DD630X4096QZ,Unknown, +CERAGON,IP20DD630X4Q1,Unknown, +CERAGON,IP20DD630X512Q8,Unknown, +CERAGON,IP20DD630X64Q5,Unknown, +CERAGON,IP20DD630X8Q2,Unknown, +CERAGON,IP20DD660X1024Q9,Unknown, +CERAGON,IP20DD660X1024QX,Unknown, +CERAGON,IP20DD660X16Q3,Unknown, +CERAGON,IP20DD660X2048QY,Unknown, +CERAGON,IP20DD660X256Q7,Unknown, +CERAGON,IP20DD660X32Q4,Unknown, +CERAGON,IP20DD660X4096QZ,Unknown, +CERAGON,IP20DD660X4Q1,Unknown, +CERAGON,IP20DD660X512Q8,Unknown, +CERAGON,IP20DD660X6128Q6,Unknown, +CERAGON,IP20DD660X64Q5,Unknown, +CERAGON,IP20DD660X8Q2,Unknown, +CERAGON,IP20DHP630X1024Q9,Unknown, +CERAGON,IP20DHP630X1024QX,Unknown, +CERAGON,IP20DHP630X128Q6,Unknown, +CERAGON,IP20DHP630X16Q3,Unknown, +CERAGON,IP20DHP630X2048QY,Unknown, +CERAGON,IP20DHP630X256Q7,Unknown, +CERAGON,IP20DHP630X32Q4,Unknown, +CERAGON,IP20DHP630X4096QZ,Unknown, +CERAGON,IP20DHP630X4Q1,Unknown, +CERAGON,IP20DHP630X512Q8,Unknown, +CERAGON,IP20DHP630X64Q5,Unknown, +CERAGON,IP20DHP630X64Q6,Unknown, +CERAGON,IP20DHP630X8Q2,Unknown, +CERAGON,IP20DHP630XACM,Unknown, +CERAGON,IP20DHP660X1024Q9,Unknown, +CERAGON,IP20DHP660X1024QX,Unknown, +CERAGON,IP20DHP660X128Q6,Unknown, +CERAGON,IP20DHP660X16Q3,Unknown, +CERAGON,IP20DHP660X2048QY,Unknown, +CERAGON,IP20DHP660X256Q7,Unknown, +CERAGON,IP20DHP660X32Q4,Unknown, +CERAGON,IP20DHP660X4096QZ,Unknown, +CERAGON,IP20DHP660X4Q1,Unknown, +CERAGON,IP20DHP660X512Q8,Unknown, +CERAGON,IP20DHP660X64Q5,Unknown, +CERAGON,IP20DHP660X8Q2,Unknown, +CERAGON,IP20DHP66X4096QZ,Unknown, +CERAGON,IP20F630XA4505RFUD,Unknown, +CERAGON,IP20F630XARFUD,Unknown, +CERAGON,IP20F660XARFUD,Unknown, +CERAGON,IP20FHP630XARFUDHP,Unknown, +CERAGON,IP20FHP630XYRFUDHP,Unknown, +CERAGON,IP20G61R,ODU,Comsearch Frequency Coordination Database +CERAGON,IP20G6A,IDU, +CERAGON,IP20G6C,ODU,Comsearch Frequency Coordination Database +CERAGON,IP20G6GHZ,Unknown, +CERAGON,IP20G6HP,ODU, +CERAGON,IP20G6RFUHP,ODU, +CERAGON,IP20GHP6,ODU,Comsearch Frequency Coordination Database +CERAGON,IP20L6C,ODU, +CERAGON,IP20N61R,ODU, +CERAGON,IP20S,ODU,Comsearch Frequency Coordination Database +CERAGON,IP50C,ODU,Comsearch Frequency Coordination Database +CERAGON,IP50C660XA4506,Unknown, +NEC,IPASOLINK650HP,Unknown, +NEC,IPASOLINK650SP,Unknown, +NEC,IPASOLINKTRP6G2E,Unknown, +NEC,IPASOLINKTRPL6GS01A,Unknown, +NEC,IPASOLINKVR10SP30M,Unknown, +VISLINK,IPL059T1AAHACM,Unknown, +VISLINK,IPL059THAAH,Unknown, +VISLINK,IPL064DP1EAH,IDU,Comsearch Frequency Coordination Database +VISLINK,IPL064DP1EAH25M256QAMR,Unknown, +VISLINK,IPL1000IDU,IDU,IDU is in the model +VISLINK,IPL2000,Unknown, +VISLINK,IPLINK,IDU,Comsearch Frequency Coordination Database +VISLINK,IPLINKHPACM,Unknown, +VISLINK,IPLINKSM,IDU,Comsearch Frequency Coordination Database +CERAGON,IPMAX2,Unknown, +CERAGON,IQ106HP30256QAM,Unknown, +AVIAT,IRU600,Unknown, +HARRIS,IRU6006SP,Unknown, +AVIAT,IRU600IDU,IDU,IDU is in the model +AVIAT,IRU600V4RAC70EHP,Unknown, +AVIAT,IV3EHPU630M189,Unknown, +AVIAT,IV3HL6,Unknown, +AVIAT,IV600V3HL630M256Q179MB,Unknown, +TELESCIENCES,IVO6G,IDU, +ALCATEL,JF109408,Unknown, +ALCATEL,JF60408,IDU, +ALCATEL,JF609408,Unknown, +ALCATEL,JF66406,IDU, +ALCATEL,JF68421,IDU, +ALCATEL,JF68609,IDU, +ALCATEL,JF6880,IDU, +ALCATEL,JF68801,IDU, +ALCATEL,JF68803,IDU, +ALCATEL,JF68814,IDU, +ALCATEL,JF68909,IDU, +ALCATEL,JF68913,IDU, +ALCATEL,JF690214,IDU, +ALCATEL,JF69101,IDU, +ALCATEL,JF6913,IDU, +ALCATEL,JF69204,IDU, +ALCATEL,JF69205,IDU, +ALCATEL,JF69206,IDU, +ALCATEL,JF69213,IDU, +ALCATEL,JF69214,IDU, +ALCATEL,JF69215,IDU, +ALCATEL,JF69216,IDU, +ALCATEL,JF69219,IDU, +ALCATEL,JF69245,IDU, +ALCATEL,JF69301,IDU, +ALCATEL,JF69401,IDU, +ALCATEL,JF69402,IDU, +ALCATEL,JF69403,IDU, +ALCATEL,JF69404,IDU, +ALCATEL,JF69405,IDU, +ALCATEL,JF69406,IDU, +ALCATEL,JF69407,IDU, +ALCATEL,JF69408,IDU, +ALCATEL,JF69409,IDU, +ALCATEL,JF69419,IDU, +ALCATEL,JF69420,IDU, +ALCATEL,JF69421,IDU, +ALCATEL,JF69425,IDU, +ALCATEL,JF69429,Unknown, +ALCATEL,JF79408MDR670616,Unknown, +ALCATEL,JF9214MDR4206EC,Unknown, +ALCATEL,JF9216MDR41U6ECN,Unknown, +ALCATEL,JF9401MDR65062,Unknown, +ALCATEL,JF99301MDR5606,Unknown, +ALCATEL,JFE9421MDR4306SD,Unknown, +ALCATEL,JFG9421,Unknown, +ALCATEL,JR69421MDR4306SD,Unknown, +AVIAT,JRSCX06G28D1,Unknown, +MOTOROLA,K17PBF2400,Unknown, +CALIFORNIA MICROWAVE,K17PBF2400B,Unknown, +MOTOROLA,K17PBF2400B,Unknown, +Western Electric,KS15676BD,Unknown, +RAYTHEON,KTR3A,Unknown, +RAYTHEON,KTR3E610,Unknown, +AVIAT,L600V3EHPU610M128Q50MB,Unknown, +DRAGONWAVE,L6HC28HFCHAAM,Unknown, +NEC,L6NLITEEOC3,Unknown, +AVIAT,L6V4EHU630M1024Q227R70,Unknown, +TADIRAN,LBSCM6HC3DS3,Unknown, +TADIRAN,LBTCM6HC28DS1,Unknown, +MNI,LBW6G,IDU, +CALIFORNIA MICROWAVE,LBW6G,IDU, +TADIRAN,LBW6G,IDU, +CALIFORNIA MICROWAVE,LBW89FC6601,IDU, +MNI,LBW89FC6601,IDU, +TADIRAN,LBW89FC6601,IDU, +TADIRAN,LBWCH6HC8DS1,Unknown, +MNI,LBWCM6,IDU, +TADIRAN,LBWCM6,IDU, +CALIFORNIA MICROWAVE,LBWCM6,IDU, +MNI,LBWCMGHC28DS11DS3,Unknown, +MNI,LBWCMH6HC28DS11DS3,Unknown, +MNI,LBWCVM6HC28DS11DS3,Unknown, +TADIRAN,LBWDM6HC1DS3,Unknown, +SIAE,LFO6LPLUS2255M,Unknown, +ERICSSON,LH06,IDU, +LIGOWAVE,LIGOPTP6NRAPIDFIRE,ODU,Comsearch Frequency Coordination Database +APEX9,LL1337,Unknown, +APEX9,LL1337660M128QAM,ODU,Comsearch Frequency Coordination Database +APEX9,LL1337660M16QAM,ODU,Comsearch Frequency Coordination Database +APEX9,LL1337660M256QAM,ODU,Comsearch Frequency Coordination Database +APEX9,LL1337660M32QAM,ODU,Comsearch Frequency Coordination Database +APEX9,LL1337660M64QAM,ODU,Comsearch Frequency Coordination Database +APEX9,LL1337660MQPSK,ODU,Comsearch Frequency Coordination Database +MOTOROLA,LTPL6800,Unknown, +TRANGO,LYNX26GHZ,Unknown, +MNI,M6137512HP,Unknown, +MNI,M6I30134HP,Unknown, +MNI,M6I30155HP,Unknown, +MNI,M6I30155SP,Unknown, +MNI,M6I37512HP,Unknown, +M/A COM,M85T06GW,Unknown, +MOTOROLA,MA295,Unknown, +MOTOROLA,MA391,IDU, +M/A COM,MA85T007,Unknown, +M/A COM,MA85T06G,IDU, +M/A COM,MA85TO6G,Unknown, +M/A COM,MA85TO6GW,Unknown, +M/A COM,MA885T007,Unknown, +ROCKWELL,MAR6C,Unknown, +COLLINS,MAR6C,Unknown, +ROCKWELL,MAR6CLARCANREGEN,Unknown, +ERICSSON,MARCONILHMDRS,IDU, +ALCATEL,MD8606135,Unknown, +ALCATEL,MD8706S155,Unknown, +ALCATEL,MDDR8606135,Unknown, +ALCATEL,MDE8706E150,Unknown, +ALCATEL,MDR085068,Unknown, +ALCATEL,MDR08706E150,Unknown, +ALCATEL,MDR2306,Unknown, +ALCATEL,MDR4000CLASSIC,Unknown, +ALCATEL,MDR4000E,Unknown, +ALCATEL,MDR4106C,Unknown, +ALCATEL,MDR4106EC,Unknown, +ALCATEL,MDR4106ECN,Unknown, +ALCATEL,MDR4106SD,Unknown, +ALCATEL,MDR4106U,Unknown, +ALCATEL,MDR4106W,Unknown, +ALCATEL,MDR41U6ECN,Unknown, +ALCATEL,MDR4206EA,Unknown, +ALCATEL,MDR4206EC,Unknown, +ALCATEL,MDR42U6E,Unknown, +ALCATEL,MDR4306,Unknown, +ALCATEL,MDR4306C,Unknown, +ALCATEL,MDR4306E,Unknown, +ALCATEL,MDR4306EC,Unknown, +ALCATEL,MDR4306EHP,Unknown, +ALCATEL,MDR4306SD,Unknown, +ALCATEL,MDR5106,Unknown, +ALCATEL,MDR5306,Unknown, +ALCATEL,MDR5606,Unknown, +COLLINS,MDR6,IDU, +ALCATEL,MDR6,IDU, +ALCATEL,MDR760616,Unknown, +ALCATEL,MDR780616,Unknown, +ALCATEL,MDR8000,Unknown, +ALCATEL,MDR80008,Unknown, +ALCATEL,MDR80008T1,Unknown, +ALCATEL,MDR8000ETHERNET,Unknown, +ALCATEL,MDR8000LWR6,Unknown, +ALCATEL,MDR8000SONET,Unknown, +ALCATEL,MDR8006135,Unknown, +ALCATEL,MDR8076E50,Unknown, +ALCATEL,MDR8076S155,Unknown, +ALCATEL,MDR8506,IDU, +ALCATEL,MDR85104,Unknown, +ALCATEL,MDR8600135,Unknown, +ALCATEL,MDR8601135,Unknown, +ALCATEL,MDR860116,Unknown, +ALCATEL,MDR8605135,Unknown, +ALCATEL,MDR8606,IDU, +ALCATEL,MDR86071629,Unknown, +ALCATEL,MDR8608135,Unknown, +ALCATEL,MDR860845,Unknown, +ALCATEL,MDR8686135,Unknown, +ALCATEL,MDR870316,Unknown, +ALCATEL,MDR8706,IDU, +AVIAT,MDR8706,IDU, +ALCATEL,MDR8709S155,Unknown, +ALCATEL,MDR870E150,Unknown, +ALCATEL,MDR871016,Unknown, +ALCATEL,MDR8726S155,Unknown, +ALCATEL,MDR8760E150,Unknown, +ALCATEL,MDR8760E50,Unknown, +ALCATEL,MDR87616,Unknown, +ALCATEL,MDR876S155,Unknown, +ALCATEL,MDR879616,Unknown, +ALCATEL,MDR8870616,Unknown, +ALCATEL,MDR9706S155,Unknown, +ALCATEL,MDRD860645,Unknown, +ERICSSON,MDRS155,IDU, +HARRIS,MEAH0603404525HS,Unknown, +HARRIS,MEGASTAR,Unknown, +HARRIS,MEGASTAR1556GHZ,Unknown, +HARRIS,MEGASTAR2000,Unknown, +HARRIS,MEGASTAR20006,Unknown, +HARRIS,MEGTASTAR,Unknown, +MNI,MI6G155MB30M,Unknown, +MNI,MI6G179MB30M,Unknown, +ERICSSON,MINILINK63636,Unknown, +ERICSSON,MINILINKHCRAU1N6,Unknown, +ERICSSON,MINILINKL6RAU1N,Unknown, +ERICSSON,MINILINKLH06,IDU, +ERICSSON,MINILINKLHL6,IDU, +COLLINS,MIR61,Unknown, +COLLINS,MIR62,Unknown, +COLLINS,MIR62AJN9U0,Unknown, +COLLINS,MIR62AJN9U0MIR62,Unknown, +COLLINS,MIR62AJN9UO,Unknown, +COLLINS,MIR62AJN9UOMIR62,Unknown, +SAF TEHNIKA,MK26LLMIDU,IDU,IDU is in the model +ERICSSON,ML6206LAAA030016QB8B,Unknown, +ERICSSON,ML6206LAAA030032QB8B,Unknown, +ERICSSON,ML6206LAAA03004QB8B,Unknown, +ERICSSON,ML6206LAAA030064QB8B,Unknown, +ERICSSON,ML6206LAAA0301024QB8B,Unknown, +ERICSSON,ML6206LAAA030128QB8B,Unknown, +ERICSSON,ML6206LAAA030256QB8B,Unknown, +ERICSSON,ML6206LAAA030512QB8B,Unknown, +ERICSSON,ML6206LAAA060B8A,Unknown, +ERICSSON,ML6206LAAB030167F7A,Unknown, +ERICSSON,ML6206LAAB030B7A,Unknown, +ERICSSON,ML6206LAAC03039243B7A,Unknown, +ERICSSON,ML6206LAAC030B8B,Unknown, +ERICSSON,ML6206LAAC060B8B,Unknown, +ERICSSON,ML6206UAAA030016QB8B,Unknown, +ERICSSON,ML6206UAAA030032QB8B,Unknown, +ERICSSON,ML6206UAAA03004QB8B,Unknown, +ERICSSON,ML6206UAAA030064QB8B,Unknown, +ERICSSON,ML6206UAAA0301024QB8B,Unknown, +ERICSSON,ML6206UAAA030128QB8B,Unknown, +ERICSSON,ML6206UAAA030256QB8B,Unknown, +ERICSSON,ML6206UAAA030512QB8B,Unknown, +ERICSSON,ML6206UAAC030B8B,Unknown, +ERICSSON,MLLH06,IDU, +ERICSSON,MLTN62X165T128X,ODU, +ERICSSON,MLTN62X165T128XHP,ODU, +ERICSSON,MLTN6L2X097T16XHP,ODU, +ERICSSON,MLTN6L2X143T64X,ODU, +ERICSSON,MLTN6L2X143T64XHP,ODU, +ERICSSON,MLTN6L2X147S64,ODU, +ERICSSON,MLTN6L2X165128X,ODU, +ERICSSON,MLTN6L2X165T128,ODU, +ERICSSON,MLTN6L2X165T128X,ODU, +ALCATEL,MLTN6L2X165T128X,ODU, +ERICSSON,MLTN6L2X165T128XHP,ODU, +ERICSSON,MLTN6L2X166S128,ODU, +ERICSSON,MLTN6L2X182T256X,ODU, +ERICSSON,MLTN6L2X182T256XHP,ODU, +ERICSSON,MLTN6L2X204T512X,ODU, +ERICSSON,MLTN6L2X204T512XST,ODU, +ERICSSON,MLTN6U2X043T64X,ODU, +ERICSSON,MLTN6U2X143T64X,ODU, +ERICSSON,MLTN6U2X143T64XHP,ODU, +ERICSSON,MLTN6U2X165T128X,ODU, +ERICSSON,MLTN6U2X165T128XHP,ODU, +ERICSSON,MLTN6U2X182T256X,ODU, +ERICSSON,MLTN6U2X182T256XHP,ODU, +ERICSSON,MLTNL2X165T128X,ODU, +ERICSSON,MLTNL6G8DS1RAU1N,ODU, +ERICSSON,MLTNRAU2X6L,ODU, +ERICSSON,MLTNRAUX6L,ODU, +ERICSSON,MLTNU6G8DS1RAU1N,ODU, +MOTOROLA,MOT004,Unknown, +MOTOROLA,MR600,Unknown, +ALCATEL,MR860645,Unknown, +MRC,MRCFLR6HP,Unknown, +ALCATEL,MRD8606135,Unknown, +ALCATEL,MRD8706S155,Unknown, +MNI,MSX610M,Unknown, +ERICSSON,MTLN6L2X165T128X,ODU, +COLLINS,MVR6C,Unknown, +COLLINS,MW109A3,Unknown, +COLLINS,MW109E,Unknown, +COLLINS,MW109E1R,Unknown, +COLLINS,MW109E1T,Unknown, +COLLINS,MW118,Unknown, +COLLINS,MW308D,IDU, +COLLINS,MW318A,IDU, +COLLINS,MW328,IDU, +MNI,MX6I1044MBPSHP,Unknown, +MNI,MX6I1052MBPSHP,Unknown, +MNI,MX6I1052MBPSHPAMR,Unknown, +MNI,MX6I30166MBPSHPAMR,Unknown, +MNI,MXALLIN116DS1256QAM,Unknown, +MNI,MXALLIN28DS164QAMHP,Unknown, +MNI,MXALLIN38DS1256QAMHP,Unknown, +MNI,MXALLIN84DS164QAM,Unknown, +MNI,MXALLINDOOR,Unknown, +MNI,MXALLINDOOR28DS164QAM,Unknown, +MNI,MXI10M64QAM,Unknown, +MNI,MXI610M45MBHP,Unknown, +MNI,MXI610M45MBSP,Unknown, +MNI,MXI6256MBHP,Unknown, +MNI,MXI625M6MBHP,Unknown, +MNI,MXI630HP,IDU, +MNI,MXI630M105MBHP,Unknown, +MNI,MXI630M180MBHP,Unknown, +MNI,MXI630M45MBHP,Unknown, +MNI,MXI630M64MBHP,Unknown, +MNI,MXI630M77MBHP,Unknown, +MNI,MXI630MHP128Q155M,Unknown, +MNI,MXI630MHP128Q166M,Unknown, +MDS,MXI630MHP128Q166M,Unknown, +MNI,MXI630MHP256Q191MAMR,Unknown, +MNI,MXI630SP,Unknown, +MNI,MXI6G,IDU, +SAF TEHNIKA,MXMSPRINTREPEATER,Unknown, +MNI,MXS625M6MBHP,Unknown, +MNI,MXS630128Q155MB,Unknown, +MNI,MXS630128Q166MB,Unknown, +MNI,MXS630M128Q166MB,Unknown, +MNI,MXS630M16Q76MB,Unknown, +MNI,MXS630M16Q90MB,Unknown, +MNI,MXS630M256Q191MB,Unknown, +MNI,MXS630M32Q105MB,Unknown, +MNI,MXS630M64Q134MB,Unknown, +MNI,MXS630M8QAM64MB,Unknown, +MNI,MXS630MHP128Q155M,Unknown, +MNI,MXS630MQPSK38MB,Unknown, +MNI,MXS6G10M,Unknown, +MNI,MXS6G10MHP,Unknown, +MNI,MXS6G10MHP64Q45M,Unknown, +MNI,MXS6G10MHP64QAMR,Unknown, +MNI,MXS6G10MUHP,Unknown, +MNI,MXS6G10MUHP64Q45M,Unknown, +MNI,MXS6G10MUHP64Q45MA,Unknown, +MNI,MXS6G13MB10MHP,Unknown, +MNI,MXS6G13MB375MHP,Unknown, +MNI,MXS6G157MB30MHP,Unknown, +MNI,MXS6G166MB30MHP,Unknown, +MNI,MXS6G180MB30MHP,Unknown, +MNI,MXS6G19MB10MHP,Unknown, +MNI,MXS6G19MB5MHP,Unknown, +MNI,MXS6G26MB10MHP,Unknown, +MNI,MXS6G30M,Unknown, +MNI,MXS6G30M128QAM,Unknown, +MNI,MXS6G30M16QAM,Unknown, +MNI,MXS6G30M32QAM,Unknown, +MNI,MXS6G30M64QAM,Unknown, +MNI,MXS6G30M8QAM,Unknown, +MNI,MXS6G30MHP,Unknown, +MNI,MXS6G30MHP128Q155M,Unknown, +MNI,MXS6G30MHP128Q166M,Unknown, +MNI,MXS6G30MHP256Q191M,Unknown, +MNI,MXS6G30MHP64Q134MA,Unknown, +MNI,MXS6G30MQPSK,ODU,Comsearch Frequency Coordination Database +MNI,MXS6G35MB10MHP,Unknown, +MNI,MXS6G375M,Unknown, +MNI,MXS6G45MB10MHP,Unknown, +MNI,MXS6G6MB25MHP,Unknown, +MNI,MXS6G6MB25MSP,Unknown, +MNI,MXS6G7MB375MHP,Unknown, +ERICSSON,N6L63AAA030A2A,Unknown, +NEC,N6P52S01A,Unknown, +NEC,NA,Unknown, +BRIDGEWAVE,NAV6GHZ60MHZ,Unknown, +BRIDGEWAVE,NAVIGATORDT61G60M,ODU,Comsearch Frequency Coordination Database +BRIDGEWAVE,NAVIGATORGGBALANCED,Unknown, +BRIDGEWAVE,NAVIGATORL660M2048QAM,Unknown, +NEC,NEC3000SERIES,Unknown, +NEC,NEC300SERIES,Unknown, +NEC,NECCORPOFAMERICA,Unknown, +ALCATEL,NEEDTOADD,Unknown, +NERA,NL2006A,Unknown, +NEC,NLIGHTNTRP6G5B,Unknown, +NEC,NLITE,Unknown, +NEC,NLITEE,Unknown, +NEC,NLITEL,Unknown, +NEC,NLITELINDRDS3,Unknown, +NEC,NLITELX,Unknown, +NEC,NLITEN,Unknown,Comsearch Frequency Coordination Database +NEC,NLITENINDOOR,Unknown, +NEC,NLITENTRP6G5B,Unknown, +NEC,NLITENTRPL6G101A,Unknown, +NEC,NLITENTRPU6G101A,Unknown, +ERICSSON,NLTN6L2X165T128X,ODU, +NOKIA,NOKIA,Unknown, +MOSELEY,NXGENS,IDU, +MOSELEY,NXGENT,Unknown, +MOSELEY,NXGENT6L30MHP,Unknown, +MOSELEY,NXGENTU630M,Unknown, +MOSELEY,NXGENTU66410M,Unknown, +MOSELEY,NXGENTU68410M,Unknown, +SAF TEHNIKA,ODUHP,ODU,ODU is in the model +TADIRAN,OMEGA6,Unknown, +TRANGO,ORION6,Unknown, +TRANGO,ORION6GHZ,Unknown, +COMMSCOPE,P1065C,Unknown, +MARK ANTENNA,P60A96,Unknown, +COMMSCOPE,P659,Unknown, +COMMSCOPE,P665D,Unknown, +COMMSCOPE,P865D,Unknown, +ALCATEL,PA459WAC,Unknown, +RFS,PA865A,Unknown, +RFS,PAL865,Unknown, +COMMSCOPE,PAR665A,Unknown, +COMMSCOPE,PAR665ALF,Unknown, +COMMSCOPE,PARX659WB,Unknown, +COMMSCOPE,PARX859W,Unknown, +COMMSCOPE,PARX865,Unknown, +RFS,PAX865A,Unknown, +SAF TEHNIKA,PHCL6,Unknown, +SAF TEHNIKA,PHCU615230MHP,Unknown, +SAF TEHNIKA,PHCU69930MHP,Unknown, +SAF TEHNIKA,PHG26LACMFP30MFVHP,Unknown, +SAF TEHNIKA,PHG26LACMVHP1024Q,Unknown, +SAF TEHNIKA,PHG26LACMVHP128Q,Unknown, +SAF TEHNIKA,PHG26LACMVHP16Q,Unknown, +SAF TEHNIKA,PHG26LACMVHP256Q,Unknown, +SAF TEHNIKA,PHG26LACMVHP32Q,Unknown, +SAF TEHNIKA,PHG26LACMVHP4Q,Unknown, +SAF TEHNIKA,PHG26LACMVHP512Q,Unknown, +SAF TEHNIKA,PHG26LACMVHP64Q,Unknown, +SAF TEHNIKA,PHG2L6229M30SVHPACM,Unknown, +SAF TEHNIKA,PHG2L6452M60SHPAMR,Unknown, +SAF TEHNIKA,PHG2L691M30SVHP,Unknown, +SAF TEHNIKA,PHG2U6137M30SVHP,Unknown, +SAF TEHNIKA,PHG2U6229M30SVHPACM,Unknown, +SAF TEHNIKA,PHML6147M30SHP,Unknown, +SAF TEHNIKA,PHOENIX6G2,Unknown, +SAF TEHNIKA,PHOENIXG2,Unknown, +SAF TEHNIKA,PHOENIXU6G2256Q30M183MB,ODU,Comsearch Frequency Coordination Database +SAF TEHNIKA,PHOENIXU6G264Q30M137MB,ODU,Comsearch Frequency Coordination Database +SIEMENS,PHVARIOIP10,Unknown, +AVIAT,PL1602,Unknown, +FARINON,PL1602,Unknown, +COMMSCOPE,PL659D,Unknown, +CAMBIUM,PPTPL6800,Unknown, +MNI,PROTEUMAMTMSERIES,Unknown, +MNI,PROTEUS,Unknown, +MNI,PROTEUSAMTM,Unknown, +MNI,PROTEUSAMTM12DS1,Unknown, +MNI,PROTEUSAMTM155MB,Unknown, +MNI,PROTEUSAMTM16DS1,Unknown, +MNI,PROTEUSAMTM28DS1,Unknown, +MNI,PROTEUSAMTM3DS3,Unknown, +MNI,PROTEUSAMTM3DS3IDU,IDU,IDU is in the model +MNI,PROTEUSAMTM3DS3ODU,ODU,ODU is in the model +MNI,PROTEUSAMTM4DS3,Unknown, +MNI,PROTEUSAMTM4DS38DS1,Unknown, +MNI,PROTEUSAMTM630192MB,Unknown, +MNI,PROTEUSAMTM6I1045,Unknown, +MNI,PROTEUSAMTM6I1045HP,Unknown, +MNI,PROTEUSAMTM6I1045MB,Unknown, +MNI,PROTEUSAMTM6I30134MB,Unknown, +MNI,PROTEUSAMTM6I30147MB,Unknown, +MNI,PROTEUSAMTM6I30155MB,Unknown, +MNI,PROTEUSAMTM6I30171MB,Unknown, +MNI,PROTEUSAMTM6I30192MB,Unknown, +MNI,PROTEUSAMTM6I37512,Unknown, +MNI,PROTEUSAMTM6I525MBPS,Unknown, +MNI,PROTEUSAMTM8DS1,Unknown, +MNI,PROTEUSAMTMHP,Unknown, +MNI,PROTEUSAMTMINDR,Unknown, +MNI,PROTEUSAMTMOC3,Unknown, +MNI,PROTEUSAMTMOC38DS1ODU,ODU,ODU is in the model +MNI,PROTEUSAMTMOC3IDU,IDU,IDU is in the model +MNI,PROTEUSAMTMSERIES,Unknown, +MNI,PROTEUSAMTMSPLITMOUNT,ODU,Split is in the model +MNI,PROTEUSMX,Unknown,Comsearch Frequency Coordination Database +MNI,PROTEUSMXALLINDOOR,IDU,IDU is in the model +MNI,PROTEUSMXI6G,IDU, +MNI,PROTEUSMXIDU,IDU,IDU is in the model +MNI,PROTEUSMXINDOOR,IDU,IDU is in the model +ERICSSON,PT2C06LAAA030A2A,Unknown, +ERICSSON,PT2C06LAAA040B4A,Unknown, +ERICSSON,PT2C06LAAA050A2A,Unknown, +MNI,PTOTEUSAMTM,Unknown, +CAMBIUM,PTP06800I30MHZ256QHP,Unknown, +CAMBIUM,PTP11820CWIDEV2ACM,Unknown, +CAMBIUM,PTP11820IRFUAEV2,IDU,Comsearch Frequency Coordination Database +CAMBIUM,PTP18820S,ODU,Comsearch Frequency Coordination Database +LIGOWAVE,PTP620HP,Unknown, +LIGOWAVE,PTP620HP6,ODU,Comsearch Frequency Coordination Database +CAMBIUM,PTP6800I30MHZ256QHP,Unknown, +CAMBIUM,PTP6820C,ODU, +CAMBIUM,PTP6820IRFUAEV2,IDU, +CAMBIUM,PTP6820S,ODU, +CAMBIUM,PTP6L850CV2,ODU,Comsearch Frequency Coordination Database +CAMBIUM,PTP6L850CV2XPIC,ODU,Comsearch Frequency Coordination Database +MOTOROLA,PTP800,Unknown, +MOTOROLA,PTP8006,Unknown, +CAMBIUM,PTP8006GHZ30MHZAMR,Unknown, +MOTOROLA,PTP8006GHZ30MHZAMR,Unknown, +CAMBIUM,PTP800A,Unknown, +CAMBIUM,PTP820C,Unknown, +CAMBIUM,PTP820CLIGHT,Unknown, +CAMBIUM,PTP820CLIGHTFEC,Unknown, +CAMBIUM,PTP820CSTRONG,Unknown, +CAMBIUM,PTP820CSTRONGFEC,Unknown, +CAMBIUM,PTP820IIDU,IDU,IDU is in the model +CAMBIUM,PTP820S,Unknown, +CAMBIUM,PTP820SLIGHT,Unknown, +CAMBIUM,PTP820SODU,ODU,ODU is in the model +CAMBIUM,PTP820SSTRONG,Unknown, +MOTOROLA,PTPL680,Unknown, +CAMBIUM,PTPL6800,ODU,Comsearch Frequency Coordination Database +CAMBIUM,PTPL680030MHZ256QAMR,Unknown, +CAMBIUM,PTPL6800A,Unknown, +CAMBIUM,PTPL6800A10MHZ128QAMR,Unknown, +CAMBIUM,PTPL6800A30MHZ256Q,Unknown, +CAMBIUM,PTPL6800A30MHZ256QAM,Unknown, +CAMBIUM,PTPL6800A30MHZ256QAMR,Unknown, +CAMBIUM,PTPL6800ACM,Unknown, +CAMBIUM,PTPL6800I,Unknown, +CAMBIUM,PTPL6800I10MHZ128QHP,Unknown, +CAMBIUM,PTPL6800IIRFUHP,Unknown, +CAMBIUM,PTPL6800IRFU,Unknown, +CAMBIUM,PTPL6810A,ODU,Comsearch Frequency Coordination Database +CAMBIUM,PTPL6810A30MHZ256QAM,Unknown, +CAMBIUM,PTPL6810A60MHZ256QAM,Unknown, +CAMBIUM,PTPL6810I,IDU,Comsearch Frequency Coordination Database +CAMBIUM,PTPL6820C,ODU,Comsearch Frequency Coordination Database +DRAGONWAVE,PTPL6820C,ODU,Comsearch Frequency Coordination Database +CAMBIUM,PTPL6820C,ODU, +CERAGON,PTPL6820C,ODU,Comsearch Frequency Coordination Database +CAMBIUM,PTPL6820FWITHRFUDHP,Unknown,Comsearch Frequency Coordination Database +CAMBIUM,PTPL6820FWITHRFUDV2A,Unknown, +CAMBIUM,PTPL6820G,Unknown,Comsearch Frequency Coordination Database +CAMBIUM,PTPL6820G,Unknown, +CAMBIUM,PTPL6820IRFUA,IDU, +CAMBIUM,PTPL6820S,ODU,Comsearch Frequency Coordination Database +CAMBIUM,PTPL6820S,ODU, +CAMBIUM,PTPL6850C,Unknown, +CAMBIUM,PTPL6850CV2,ODU,Comsearch Frequency Coordination Database +CAMBIUM,PTPL6850CV2ACM,Unknown, +CAMBIUM,PTPL68820S,Unknown, +CAMBIUM,PTPL820C,Unknown, +CAMBIUM,PTPTU6820IRFUAEPV2,IDU, +CAMBIUM,PTPU6800,ODU,Comsearch Frequency Coordination Database +MOTOROLA,PTPU6800,ODU,Comsearch Frequency Coordination Database +CAMBIUM,PTPU6800A30MHZ256QAM,Unknown, +CAMBIUM,PTPU6800A30MHZ256QAMR,Unknown, +CAMBIUM,PTPU6800A30MHZ64Q,Unknown, +CAMBIUM,PTPU6800I,Unknown, +CAMBIUM,PTPU6800I10MHZ128QH,Unknown, +CAMBIUM,PTPU6800I10MHZ128QHPAMR,Unknown, +CAMBIUM,PTPU6800I30MHZ256QHP,Unknown, +CAMBIUM,PTPU6800IRFU,Unknown, +CAMBIUM,PTPU680S,Unknown, +CAMBIUM,PTPU6810,Unknown, +CAMBIUM,PTPU6810A,ODU,Comsearch Frequency Coordination Database +CAMBIUM,PTPU68200SV2AMR,ODU, +CAMBIUM,PTPU6820C,ODU,Comsearch Frequency Coordination Database +CAMBIUM,PTPU6820C,ODU, +CAMBIUM,PTPU6820FWITHRFUDV2A,Unknown, +CAMBIUM,PTPU6820G,Unknown,Comsearch Frequency Coordination Database +CAMBIUM,PTPU6820GAE,IDU,Comsearch Frequency Coordination Database +CAMBIUM,PTPU6820G,Unknown, +CAMBIUM,PTPU6820I,IDU, +CAMBIUM,PTPU6820S,ODU,Comsearch Frequency Coordination Database +CAMBIUM,PTPU6820S,ODU, +TRANGO,PXG2NITRO60M1024Q,ODU,Comsearch Frequency Coordination Database +TRANGO,PXG2NITRO60M128Q,ODU,Comsearch Frequency Coordination Database +TRANGO,PXG2NITRO60M16Q,ODU,Comsearch Frequency Coordination Database +TRANGO,PXG2NITRO60M2048Q,ODU,Comsearch Frequency Coordination Database +TRANGO,PXG2NITRO60M256Q,ODU,Comsearch Frequency Coordination Database +TRANGO,PXG2NITRO60M32Q,ODU,Comsearch Frequency Coordination Database +TRANGO,PXG2NITRO60M4096Q,ODU,Comsearch Frequency Coordination Database +TRANGO,PXG2NITRO60M512Q,ODU,Comsearch Frequency Coordination Database +TRANGO,PXG2NITRO60M64Q,ODU,Comsearch Frequency Coordination Database +TRANGO,PXG2NITRO60MQPSK,ODU,Comsearch Frequency Coordination Database +TRANGO,PXG2NITRO6G30M1024Q2,Unknown, +TRANGO,PXG2NITRO6G30M128Q17,Unknown, +TRANGO,PXG2NITRO6G30M16Q98M,Unknown, +TRANGO,PXG2NITRO6G30M2048Q2,Unknown, +TRANGO,PXG2NITRO6G30M256Q20,Unknown, +TRANGO,PXG2NITRO6G30M32Q122,Unknown, +TRANGO,PXG2NITRO6G30M4096Q3,Unknown, +TRANGO,PXG2NITRO6G30M512Q22,Unknown, +TRANGO,PXG2NITRO6G30M64Q152,Unknown, +TRANGO,PXG2NITRO6G30MQPSK49,Unknown, +TRANGO,PXG2NITRO6GHZ,Unknown, +TRANGO,PXG2NITROODU,ODU,ODU is in the model +INFINET WIRELESS,R5000OMX,ODU,Comsearch Frequency Coordination Database +CERAGON,R61530HP,Unknown, +CERAGON,R61530T,Unknown, +CERAGON,R64410T,Unknown, +LIGOWAVE,RAPIDFIRE6256QAMAMR,Unknown, +NORTEL,RD6C,Unknown, +NORTEL,RD6C3,Unknown, +VARIOUS,REPEATER,Unknown, +REPEATER TECHNOLOGIES,RF6000,Unknown, +PENINSULA,RF600001,Unknown, +PENINSULA,RF6000E,Unknown, +REPEATER TECHNOLOGIES,RF6000E,Unknown, +PENINSULA,RF6000E01,Unknown, +PENINSULA,RF6000E21,Unknown, +PENINSULA,RF6000E41,Unknown, +PENINSULA,RF6000EEW,Unknown, +PENINSULA,RF6000EL,Unknown, +PENINSULA,RFENGINEERING,Unknown, +RF CENTRAL LLC,RFX6GL,Unknown, +RAYTHEON,RS641,Unknown, +RAYTHEON,RS642,Unknown, +HARRIS,RSCX06G28D1H,Unknown, +ZTE,S3406505,Unknown, +ZTE,S340L6307AMR,Unknown, +ZTE,S340U6307AMR,Unknown, +CABLE AML,SALINAS6G,Unknown, +CABLE AML,SALINASHPL6GACM,Unknown, +NEC,SF6P155S02A,Unknown, +STAR MICROWAVE,SIRIUSSHC,Unknown, +CIELO,SKILINKCG,Unknown, +CIELO,SKYLINK1506,Unknown, +CIELO,SKYLINK1506U,Unknown, +CIELO,SKYLINK150W6,Unknown, +CIELO,SKYLINK335W6,Unknown, +CIELO,SKYLINKAI,Unknown, +CIELO,SKYLINKAONDT61G60M,ODU,Comsearch Frequency Coordination Database +CIELO,SKYLINKCG,Unknown, +CIELO,SKYLINKCG2,Unknown, +CIELO,SKYLINKCG2XMFEC,Unknown, +CIELO,SKYLINKCG6C30128Q,Unknown, +CIELO,SKYLINKCG6LC30,Unknown, +CIELO,SKYLINKCG6LC30128Q,Unknown, +CIELO,SKYLINKCG6LC3016Q,Unknown, +CIELO,SKYLINKCG6LC30256Q,Unknown, +CIELO,SKYLINKCG6LC3032Q,Unknown, +CIELO,SKYLINKCG6LC3064Q,Unknown, +CIELO,SKYLINKCG6LC30QPSK,Unknown, +CIELO,SKYLINKCG6UC30,Unknown, +CIELO,SKYLINKCG6UC30128Q,Unknown, +CIELO,SKYLINKCG6UC3016Q,Unknown, +CIELO,SKYLINKCG6UC30256Q,Unknown, +CIELO,SKYLINKCG6UC3032Q,Unknown, +CIELO,SKYLINKCG6UC3064Q,Unknown, +CIELO,SKYLINKCG6UC30QPSK,Unknown, +CIELO,SKYLINKCGX6C30,Unknown, +CIELO,SKYLINKCGX6LC30,Unknown, +CIELO,SKYLINKCGX6LC30128Q,Unknown, +CIELO,SKYLINKCGX6LC3032Q,Unknown, +CIELO,SKYLINKCGX6LC3064Q,Unknown, +CIELO,SKYLINKCGX6UC30,Unknown, +CIELO,SKYLINKCGX6UC30128Q,Unknown, +CIELO,SKYLINKCGX6UC3016Q,Unknown, +CIELO,SKYLINKCGX6UC3032Q,Unknown, +CIELO,SKYLINKCGX6UC3064Q,Unknown, +CIELO,SKYLINKCGX6UC30QPSK,Unknown, +CIELO,SKYLINKGC,Unknown, +SOLECTEK,SKYWAYCML6GHZ128QAM,Unknown, +SOLECTEK,SKYWAYCML6GHZ256QAMAM,Unknown, +SOLECTEK,SKYWAYKML6GHZ4096QAMA,Unknown, +SOLECTEK,SKYWAYKML6GHZACM,Unknown, +STAR MICROWAVE,SMCIRIUSSHC,Unknown, +NEC,SONETSDH20006G150MBHP,Unknown, +TRANGO,SPARTAELITE6,Unknown, +FARINON,SS6000M02,Unknown, +HARRIS,SS6000M02,Unknown, +FARINON,SS6000W02,Unknown, +FARINON,SS6000X02,Unknown, +HARRIS,SS6000X02,Unknown, +FARINON,SS6000Y02,Unknown, +MOTOROLA,STARPOINT6000,Unknown, +ADVANTECH,T800L6G3032QAM6HC,Unknown, +ADVANTECH,T800L6G3032QAM7IH,Unknown, +ADVANTECH,T800L6G3064QAM6HC,Unknown, +NEC,TEC026,Unknown, +CALIFORNIA MICROWAVE,TELESTAR6GHZ8DS1,Unknown, +ALCATEL,TEX002,Unknown, +NEC,TEX026,Unknown, +ERICSSON,TN62XAHA030A2,ODU, +ERICSSON,TN6L2XA049Y4X,ODU, +ERICSSON,TN6L2XA099Y16X,ODU, +ERICSSON,TN6L2XA122Y32X,ODU, +ERICSSON,TN6L2XA147Y64X,ODU, +ERICSSON,TN6L2XA170Y128X,ODU, +ERICSSON,TN6L2XA194Y256X,ODU, +ERICSSON,TN6L2XA216Y512X,ODU, +ERICSSON,TN6L2XA239Y1024X,ODU, +ERICSSON,TN6L2XAA030A2C,ODU, +ERICSSON,TN6L2XAAA0100512A2AH,ODU, +ERICSSON,TN6L2XAAA0100512T2AH,ODU, +ERICSSON,TN6L2XAAA0300128T2A,ODU, +ERICSSON,TN6L2XAAA0300256T2A,ODU, +ERICSSON,TN6L2XAAA0301024A2AH,ODU, +ERICSSON,TN6L2XAAA030A0A,ODU, +ERICSSON,TN6L2XAAA030A2,ODU, +ERICSSON,TN6L2XAAA030A2A,ODU, +ERICSSON,TN6L2XAAA030A2AAMR,ODU, +ERICSSON,TN6L2XAAA030A2C,ODU, +ERICSSON,TN6L2XAHA030A2,ODU, +ERICSSON,TN6L63AAA0300064T2A,ODU, +ERICSSON,TN6L63AAA0300128T0A,ODU, +ERICSSON,TN6L63AAA0300128T2A,ODU, +ERICSSON,TN6L63AAA0300256T0A,ODU, +ERICSSON,TN6L63AAA0300256T2A,ODU, +ERICSSON,TN6L63AAA0300512T2A,ODU, +ERICSSON,TN6L63AAA030A0A,ODU, +ERICSSON,TN6L63AAA030A2A,ODU, +ERICSSON,TN6L666L63AAA030F7A,ODU, +ERICSSON,TN6U2XA049Y4X,ODU, +ERICSSON,TN6U2XA099Y16X,ODU, +ERICSSON,TN6U2XA122Y32X,ODU, +ERICSSON,TN6U2XA147Y64X,ODU, +ERICSSON,TN6U2XA170Y128X,ODU, +ERICSSON,TN6U2XA194Y256X,ODU, +ERICSSON,TN6U2XA216Y512X,ODU, +ERICSSON,TN6U2XA239Y1024X,ODU, +ERICSSON,TN6U2XAAA030A2C,ODU, +ERICSSON,TN6U2XAHA030A2,ODU, +ERICSSON,TN6U2XAHA030B7B,ODU, +ERICSSON,TN6U63AAA030A2A,ODU, +ERICSSON,TN6U666L63AAA030F7A,ODU, +NEC,TP6G150MB7900AB,Unknown, +NEC,TR600,Unknown, +NEC,TR6G,IDU, +TRANGO,TRAGONLINKGIGAPLUS6HP,Unknown, +TRANGO,TRANGOLINKAPEXPLUS6,Unknown, +TRANGO,TRANGOLINKAPEXPLUS6AMR,Unknown, +TRANGO,TRANGOLINKAPEXPLUSL6,Unknown, +TRANGO,TRANGOLINKAPEXPLUSL6AM,Unknown, +TRANGO,TRANGOLINKGIGA6,Unknown, +TRANGO,TRANGOLINKGIGALONGHAUL,Unknown, +TRANGO,TRANGOLINKGIGAORION6A,Unknown, +TRANGO,TRANGOLINKGIGAPLUS,ODU,Comsearch Frequency Coordination Database +TRANGO,TRANGOLINKGIGAPLUS6,Unknown, +TRANGO,TRANGOLINKGIGAPLUS6AMR,Unknown, +TRANGO,TRANGOLINKGIGAPLUS6HP,Unknown, +TRANGO,TRANGOLINKGIGAPLUS6HP30,Unknown, +TRANGO,TRANGOLINKGIGAPRO,Unknown, +TRANGO,TRANGOLINKGIGAPRO6,Unknown, +ALLGON MICROWAVE,TRANSCEND,Unknown, +ALLGON MICROWAVE,TRANSCEND100,Unknown, +AVIAT,TRE6V2HL630M1024Q230,Unknown, +NEC,TRGL6G101AHP,Unknown, +NEC,TROL6G155MB71A,Unknown, +NEC,TRP11G150MB6900AB,Unknown, +NEC,TRP150MB7900AB,Unknown, +NEC,TRP58GL6G201A,IDU, +NEC,TRP6G101A,Unknown, +NEC,TRP6G135MB,IDU, +NEC,TRP6G13MB72600A,IDU, +NEC,TRP6G150B7900AB,IDU, +NEC,TRP6G150BMB7900AB,IDU, +NEC,TRP6G150M7900AB,IDU, +NEC,TRP6G150MB,IDU, +NOKIA,TRP6G150MB,IDU, +NEC,TRP6G150MD7900AB,IDU, +NEC,TRP6G150MD7900DA,IDU, +NEC,TRP6G150MF79000AB,IDU, +NEC,TRP6G15MB7900AB,IDU, +NEC,TRP6G15OMB7900AB,IDU, +NEC,TRP6G1AA,Unknown, +NEC,TRP6G1B,Unknown, +NEC,TRP6G1D,ODU,Comsearch Frequency Coordination Database +NEC,TRP6G1E,ODU,Comsearch Frequency Coordination Database +NEC,TRP6G26MB72600A,Unknown, +NEC,TRP6G26MBY2600A,Unknown, +NEC,TRP6G2E,ODU,Comsearch Frequency Coordination Database +NEC,TRP6G52BM72600A,Unknown, +NEC,TRP6G52MB72600A,Unknown, +NEC,TRP6G5AA,ODU,Comsearch Frequency Coordination Database +NEC,TRP6G5B,ODU,Comsearch Frequency Coordination Database +NEC,TRP6G6AA,ODU,Comsearch Frequency Coordination Database +NEC,TRP6G90MB500A,Unknown, +COLLINS,TRP6G90MB500A,Unknown, +NEC,TRP6G9OMB500A,Unknown, +NEC,TRP6GP0MB600A,Unknown, +NEC,TRP6T150MB7900AB,Unknown, +NEC,TRPG150MB7900AB,Unknown, +NEC,TRPG613MB72600A,Unknown, +NEC,TRPGG150MB7900AB,Unknown, +NEC,TRPL6101AHPNLITENAMR,Unknown, +NEC,TRPL6G101A,IDU, +NEC,TRPL6G155MB71A,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E28M1024QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E28M128QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E28M16QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E28M2048QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E28M256QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E28M32QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E28M512QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E28M64QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E28MQPSKEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M1024QAV,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M1024QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M128QAV,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M128QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M16QAV,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M16QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M2038QEH,Unknown, +NEC,TRPL6G2E30M2048QAV,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M2048QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M256QAV,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M256QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M32QAV,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M32QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M4QAMEH,Unknown, +NEC,TRPL6G2E30M4QAV,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M512QAV,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M512QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M64QAV,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30M64QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2E30MQPSKEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPL6G2EIPASOVREHHP,Unknown, +NEC,TRPL6G2F,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F,Unknown, +NEC,TRPL6G900F30M1024Q,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M1024QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M128Q,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M128QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M16Q,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M16QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M2048Q,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M2048QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M256Q,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M256QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M32Q,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M32QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M4QH,Unknown, +NEC,TRPL6G900F30M512Q,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M512QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M64Q,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30M64QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30MQPSK,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900F30MQPSKH,IDU,Comsearch Frequency Coordination Database +NEC,TRPL6G900FACM,Unknown, +NEC,TRPL6G900FAMR,Unknown, +NEC,TRPL6GS01A,IDU, +NEC,TRPLG155MB71A,Unknown, +NEC,TRPLG6101A,Unknown, +NEC,TRPLG6155MB71A,Unknown, +NEC,TRPLG6900FAMR,Unknown, +HARRIS,TRPT52L63DS33DS1,Unknown, +NEC,TRPU6G101A,IDU, +NEC,TRPU6G102A,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G155MB7A1A,Unknown, +NEC,TRPU6G2AA,Unknown,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M1024QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M1024QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M128QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M128QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M16QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M16QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M256QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M256QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M32QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M32QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M512QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M512QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M64QEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10M64QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10MQPSKEA,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E10MQPSKEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E30M1024QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E30M128QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E30M16QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E30M2048QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E30M256QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E30M32QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E30M512QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E30M64QEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G2E30MQPSKEH,ODU,Comsearch Frequency Coordination Database +NEC,TRPU6G3AA,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F,Unknown, +NEC,TRPU6G900F30M1024QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M1024QS,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M128QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M128QS,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M16QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M16QS,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M2048QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M2048QS,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M256QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M256QS,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M32QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M32QS,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M512QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M512QS,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M64QH,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30M64QS,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30MQPSKH,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900F30MQPSKS,IDU,Comsearch Frequency Coordination Database +NEC,TRPU6G900FACM,Unknown, +NEC,TRPU6GS01A,IDU, +NEC,TRPUG6101A,Unknown, +NEC,TRPUG63AA,Unknown, +HARRIS,TRUEPOINT5200,Unknown, +HARRIS,TRUEPOINT52006,Unknown, +HARRIS,TRUEPOINT5200DS3,Unknown, +HARRIS,TRUEPOINT5200DS3DS1,Unknown, +HARRIS,TRUEPOINT5200L6,Unknown, +HARRIS,TRUEPOINT5200U6,Unknown, +HARRIS,TRUEPOINT6GHZ,Unknown, +AVIAT,TRUEPT520016DS1,Unknown, +HARRIS,TRUEPT520016DS1,Unknown, +HARRIS,TRUEPT52008DS1,Unknown, +HARRIS,TRUPOINT5200U6,Unknown, +HARRIS,TRUWPT52008DS1,Unknown, +ERICSSON,TRX6L,IDU, +NEC,TTRP6G6AA,Unknown, +WESTERN MULTIPLEX,TWO6000,Unknown, +WESTERN MULTIPLEX,TWO600026001HZB26001,Unknown, +WESTERN MULTIPLEX,TWO600026011,Unknown, +WESTERN MULTIPLEX,TWO600026011HZB26001,Unknown, +WESTERN MULTIPLEX,TWO600026012HZB26001,Unknown, +WESTERN MULTIPLEX,TWO6000HZB26000,Unknown, +WESTERN MULTIPLEX,TWO6000HZB26001,Unknown, +Gabriel Electronics,UCC859BSELF,Unknown, +Gabriel Electronics,UCC859BSERF,Unknown, +COMMSCOPE,UHX659HR,Unknown, +COMMSCOPE,UHX659KLF,Unknown, +COMMSCOPE,UHX659KRF,Unknown, +COMMSCOPE,UHX659LRF,Unknown, +COMMSCOPE,UHX859H,Unknown, +COMMSCOPE,UHX859WARF,Unknown, +COMMSCOPE,UHX859WRF,Unknown, +UNKNOWN,UNKNOWN,Unknown, +AT&T TECHNOLOGIES,USR12P3J23C,Unknown, +NOKIA,USX66W,Unknown, +COMMSCOPE,USX86W,Unknown, +MDS,UT10C,Unknown, +Cablewave Systems,UXA659RF,Unknown, +ALCATEL,V95MPR61H16A3091,IDU, +ALCATEL,V95MPR67L32A1037HAC,IDU, +AVIAT,VHLP36W,Unknown, +COMMSCOPE,VHLP36W,Unknown, +COMMSCOPE,VHLP46W,Unknown, +ERICSSON,VLTN6L2X165T128X,Unknown, +NEC,VREC2L6GEH30MA1N64AT,Unknown, +NEC,VRIAPL6GEH10MAMR,Unknown, +NEC,VRIAPU6GEH10MAMR,Unknown, +NOKIA,VWCE61L2512F30S206,Unknown, +ATH SYSTEM,WAVEFORMHS,Unknown, +NOKIA,WAVENCEMPTHLC,IDU, +NOKIA,WAVENCEMPTHLCSP,IDU, +NOKIA,WCVE67L1256A10S60,IDU, +NOKIA,WCVE67L31024A30S,IDU, +NUBEAM,WLIP1130M128Q174MBPS,Unknown, +NUBEAM,WLIP1130M16Q100MBPS,Unknown, +NUBEAM,WLIP1130M32Q125MBPS,Unknown, +NUBEAM,WLIP1130M64Q150MBPS,Unknown, +NUBEAM,WLIP1130MQPSK50MBPS,Unknown, +NUBEAM,WLIP630M,Unknown, +NUBEAM,WLIP630M128Q,Unknown, +NUBEAM,WLIP630M128Q174MBPS,Unknown, +NUBEAM,WLIP630M16Q,Unknown, +NUBEAM,WLIP630M16Q100MBPS,Unknown, +NUBEAM,WLIP630M32Q,Unknown, +NUBEAM,WLIP630M32Q125MBPS,Unknown, +NUBEAM,WLIP630M64Q,Unknown, +NUBEAM,WLIP630M64Q150MBPS,Unknown, +NUBEAM,WLIP630MQPSK,Unknown, +NUBEAM,WLIP630MQPSK150MBPS,Unknown, +NUBEAM,WLIP630MQPSK50MBPS,Unknown, +NUBEAM,WLIP6730M128Q,Unknown, +NUBEAM,WLIP6730M16Q,Unknown, +NUBEAM,WLIP6730M32Q,Unknown, +NUBEAM,WLIP6730M64Q,Unknown, +NUBEAM,WLIP6730MQPSK,Unknown, +WESTERN MULTIPLEX,WM4T6,Unknown, +WESTERN MULTIPLEX,WM645,Unknown, +NOKIA,WQVC61L14A30S44,IDU, +AVIAT,WT41630MACM,Unknown, +AVIAT,WT41660MACM,Unknown, +CAMBIUM,WT41660MACM,Unknown, +AVIAT,WT41L610M64Q45MB,Unknown, +AVIAT,WT41L630M1024Q230MB,Unknown, +AVIAT,WT41L630M128Q154MB,Unknown, +AVIAT,WT41L630M16Q90MB,Unknown, +AVIAT,WT41L630M178AMR,Unknown, +AVIAT,WT41L630M2048Q254MB,Unknown, +AVIAT,WT41L630M256Q178MB,Unknown, +AVIAT,WT41L630M272ACM,Unknown, +AVIAT,WT41L630M32Q101MB,Unknown, +AVIAT,WT41L630M4096Q272MB,Unknown, +AVIAT,WT41L630M4096QAMR,Unknown, +AVIAT,WT41L630M512Q204MB,Unknown, +AVIAT,WT41L630M64Q135MB,Unknown, +AVIAT,WT41L630MACM,Unknown, +AVIAT,WT41L630MQPSK38MB,Unknown, +AVIAT,WT41L660M1024Q462MB,ODU,Comsearch Frequency Coordination Database +AVIAT,WT41L660M128Q313MB,ODU,Comsearch Frequency Coordination Database +AVIAT,WT41L660M16Q181MB,ODU,Comsearch Frequency Coordination Database +AVIAT,WT41L660M2048Q506MB,ODU,Comsearch Frequency Coordination Database +AVIAT,WT41L660M256Q357MB,ODU,Comsearch Frequency Coordination Database +AVIAT,WT41L660M32Q212MB,ODU,Comsearch Frequency Coordination Database +AVIAT,WT41L660M4096Q546MB,ODU,Comsearch Frequency Coordination Database +AVIAT,WT41L660M4096QAMR,Unknown, +AVIAT,WT41L660M512Q408MB,ODU,Comsearch Frequency Coordination Database +AVIAT,WT41L660M546A2CACM,Unknown, +AVIAT,WT41L660M546ACM,Unknown, +AVIAT,WT41L660M64Q265MB,ODU,Comsearch Frequency Coordination Database +AVIAT,WT41L660MACM,Unknown, +AVIAT,WT41L660MQPSK77MB,ODU,Comsearch Frequency Coordination Database +AVIAT,WT41L669MACM,Unknown, +AVIAT,WT41U610M081AMR,Unknown, +AVIAT,WT41U610M1024Q74MB,Unknown, +AVIAT,WT41U610M128Q50MB,Unknown, +AVIAT,WT41U610M16Q27MB,Unknown, +AVIAT,WT41U610M256Q58MB,Unknown, +AVIAT,WT41U610M32Q33MB,Unknown, +AVIAT,WT41U610M512Q66MB,Unknown, +AVIAT,WT41U610M64Q45MB,Unknown, +AVIAT,WT41U610MACM,Unknown, +AVIAT,WT41U610MQPSK13MB,Unknown, +AVIAT,WT41U630M1024Q230MB,Unknown, +AVIAT,WT41U630M128Q154MB,Unknown, +AVIAT,WT41U630M16Q90MB,Unknown, +AVIAT,WT41U630M2048Q254MB,Unknown, +AVIAT,WT41U630M256Q178MB,Unknown, +AVIAT,WT41U630M32Q101MB,Unknown, +AVIAT,WT41U630M4096Q272MB,Unknown, +AVIAT,WT41U630M512Q204MB,Unknown, +AVIAT,WT41U630M64Q135MB,Unknown, +AVIAT,WT41U630MACM,Unknown, +AVIAT,WT41U630MQPSK38MB,Unknown, +AVIAT,WT420,Unknown, +AVIAT,WT420L630M,Unknown, +AVIAT,WT420U630M,Unknown, +AVIAT,WT42660MACM,Unknown, +AVIAT,WT42C,Unknown, +AVIAT,WT42C61012MBPS2048QAM,Unknown, +AVIAT,WT42C61092MBPS4096QAM,Unknown, +AVIAT,WT42C630MACM,Unknown, +AVIAT,WT42C6530MBPS64QAM,Unknown, +AVIAT,WT42C6626MBPS128QAM,Unknown, +AVIAT,WT42C6714MBPS256QAM,Unknown, +AVIAT,WT42C6816MBPS512QAM,Unknown, +AVIAT,WT42C6924MBPS1024QAM,Unknown, +AVIAT,WT42CL630MACM,Unknown, +AVIAT,WT42CL660M1024Q462MB,Unknown, +AVIAT,WT42CL660M1024Q924MB,Unknown, +AVIAT,WT42CL660M128Q313MB,Unknown, +AVIAT,WT42CL660M128Q626MB,Unknown, +AVIAT,WT42CL660M16Q181MB,Unknown, +AVIAT,WT42CL660M16Q182MB,Unknown, +AVIAT,WT42CL660M2048Q1012MB,Unknown, +AVIAT,WT42CL660M2048Q506MB,Unknown, +AVIAT,WT42CL660M256Q357MB,Unknown, +AVIAT,WT42CL660M256Q714MB,Unknown, +AVIAT,WT42CL660M32Q212MB,Unknown, +AVIAT,WT42CL660M4096Q,Unknown, +AVIAT,WT42CL660M4096Q1092MB,Unknown, +AVIAT,WT42CL660M4096Q546MB,Unknown, +AVIAT,WT42CL660M512Q408MB,Unknown, +AVIAT,WT42CL660M512Q816MB,Unknown, +AVIAT,WT42CL660M546ACM,Unknown, +AVIAT,WT42CL660M64Q265MB,Unknown, +AVIAT,WT42CL660M64Q530MB,Unknown, +AVIAT,WT42CL660MACM,Unknown, +AVIAT,WT42CL660MQPSK77MB,Unknown, +AVIAT,WT42O630MACM,Unknown, +AVIAT,WT42O660MACM,Unknown, +SAF TEHNIKA,WT42O660MACM,Unknown, +AVIAT,WT42OL630M,Unknown, +AVIAT,WT42OL630M1024Q230MB,Unknown, +AVIAT,WT42OL630M128Q154MB,Unknown, +AVIAT,WT42OL630M16Q90MB,Unknown, +AVIAT,WT42OL630M2048Q254MB,Unknown, +AVIAT,WT42OL630M256Q178MB,Unknown, +AVIAT,WT42OL630M272ACM,Unknown, +AVIAT,WT42OL630M272AMR,Unknown, +AVIAT,WT42OL630M32Q101MB,Unknown, +AVIAT,WT42OL630M4096Q272MB,Unknown, +AVIAT,WT42OL630M512Q204MB,Unknown, +AVIAT,WT42OL630M64Q135MB,Unknown, +AVIAT,WT42OL630MQPSK38MB,Unknown, +AVIAT,WT42OL660M1024924MB,Unknown, +AVIAT,WT42OL660M1024Q462MB,Unknown, +AVIAT,WT42OL660M128Q313MB,Unknown, +AVIAT,WT42OL660M128Q626MB,Unknown, +AVIAT,WT42OL660M16Q181MB,Unknown, +AVIAT,WT42OL660M16Q362MB,Unknown, +AVIAT,WT42OL660M2048Q1012MB,Unknown, +AVIAT,WT42OL660M2048Q506MB,Unknown, +AVIAT,WT42OL660M256Q357MB,Unknown, +AVIAT,WT42OL660M256Q714MB,Unknown, +AVIAT,WT42OL660M32Q212MB,Unknown, +AVIAT,WT42OL660M32Q424MB,Unknown, +AVIAT,WT42OL660M4096Q1092MB,Unknown, +AVIAT,WT42OL660M4096Q546MB,Unknown, +AVIAT,WT42OL660M4096QAMR,Unknown, +AVIAT,WT42OL660M512Q408MB,Unknown, +AVIAT,WT42OL660M512Q816MB,Unknown, +AVIAT,WT42OL660M546AMR,Unknown, +AVIAT,WT42OL660M64Q265MB,Unknown, +AVIAT,WT42OL660M64Q530MB,Unknown, +AVIAT,WT42OL660MACM,Unknown, +AVIAT,WT42OL660MQPSK154MB,Unknown, +AVIAT,WT42OL660MQPSK77MB,Unknown, +AVIAT,WT42OL6G601012MB2048,Unknown, +AVIAT,WT42OL6G601092MB4096,Unknown, +AVIAT,WT42OL6G60154MBQPSK,Unknown, +AVIAT,WT42OL6G60362MB16,Unknown, +AVIAT,WT42OL6G60424MB32,Unknown, +AVIAT,WT42OL6G60530MB64,Unknown, +AVIAT,WT42OL6G60626MB128,Unknown, +AVIAT,WT42OL6G60714MB256,Unknown, +AVIAT,WT42OL6G60816MB512,Unknown, +AVIAT,WT42OL6G60924MB1024,Unknown, +AVIAT,WT42OU630M,Unknown, +AVIAT,WT42OU630M1024Q460MB,Unknown, +AVIAT,WT42OU630M128Q308MB,Unknown, +AVIAT,WT42OU630M16Q180MB,Unknown, +AVIAT,WT42OU630M2048Q508MB,Unknown, +AVIAT,WT42OU630M256Q356MB,Unknown, +AVIAT,WT42OU630M32Q202MB,Unknown, +AVIAT,WT42OU630M4096Q544MB,Unknown, +AVIAT,WT42OU630M512Q408MB,Unknown, +AVIAT,WT42OU630M64Q270MB,Unknown, +AVIAT,WT42OU630MQPSK76MB,Unknown, +AVIAT,WT45L660M1024Q462MB,Unknown, +AVIAT,WT45L660M128Q313MB,Unknown, +AVIAT,WT45L660M16Q181MB,Unknown, +AVIAT,WT45L660M2048Q506MB,Unknown, +AVIAT,WT45L660M256Q357MB,Unknown, +AVIAT,WT45L660M32Q212MB,Unknown, +AVIAT,WT45L660M4096Q546MB,Unknown, +AVIAT,WT45L660M512Q408MB,Unknown, +AVIAT,WT45L660M64Q265MB,Unknown, +AVIAT,WT45L660MACM,Unknown, +AVIAT,WT45L660MQPSK77MB,Unknown, +AVIAT,WT45XTL660M1024Q462MB,Unknown, +AVIAT,WT45XTL660M128Q313MB,Unknown, +AVIAT,WT45XTL660M16Q181MB,Unknown, +AVIAT,WT45XTL660M2048Q506MB,Unknown, +AVIAT,WT45XTL660M256Q357MB,Unknown, +AVIAT,WT45XTL660M32Q212MB,Unknown, +AVIAT,WT45XTL660M512Q408MB,Unknown, +AVIAT,WT45XTL660M64Q265MB,Unknown, +AVIAT,WT45XTL660MQPSK77MB,Unknown, +AVIAT,WTM4100,Unknown, +AVIAT,WTM4100A2CODU,ODU,ODU is in the model +AVIAT,WTM42OL6G601012MB2048,Unknown, +AVIAT,WTM42OL6G60154MBQPSK,Unknown, +AVIAT,WTM42OL6G60302MB16,Unknown, +AVIAT,WTM42OL6G60376MB32,Unknown, +AVIAT,WTM42OL6G60500MB64,Unknown, +AVIAT,WTM42OL6G60650MB128,Unknown, +AVIAT,WTM42OL6G60748MB256,Unknown, +AVIAT,WTM42OL6G60846MB512,Unknown, +AVIAT,WTM42OL6G60926MB1024,Unknown, +NOKIA,WVC61L4096A30H256,IDU, +NOKIA,WVCE11U2,ODU, +NOKIA,WVCE18U1,ODU, +NOKIA,WVCE61,Unknown, +NOKIA,WVCE610L1256A30S185,IDU, +NOKIA,WVCE61J21024A30S226,Unknown, +NOKIA,WVCE61J2128A30S161,Unknown, +NOKIA,WVCE61J216A30S90,Unknown, +NOKIA,WVCE61J22048A30S236,Unknown, +NOKIA,WVCE61J2256A30S182,Unknown, +NOKIA,WVCE61J232A30S114,Unknown, +NOKIA,WVCE61J24096A30S258,Unknown, +NOKIA,WVCE61J24A30S43,Unknown, +NOKIA,WVCE61J2512A30S205,Unknown, +NOKIA,WVCE61J264A30S136,Unknown, +NOKIA,WVCE61L,IDU, +AVIAT,WVCE61L,IDU, +NOKIA,WVCE61Q11024A30S232,ODU, +NOKIA,WVCE61Q1128A30S163,ODU, +NOKIA,WVCE61Q1128F30S163,ODU, +NOKIA,WVCE61Q116A30S88,ODU, +NOKIA,WVCE61Q12048A30S257,ODU, +NOKIA,WVCE61Q1256A30S186,ODU, +NOKIA,WVCE61Q1256F30S186,ODU, +NOKIA,WVCE61Q132A30S109,ODU, +NOKIA,WVCE61Q14A30S44,ODU, +NOKIA,WVCE61Q1512A30S209,ODU, +NOKIA,WVCE61Q164A30S139,ODU, +NOKIA,WVCE61Q164F30S139,ODU, +NOKIA,WVCE61Q21024A30S232,ODU, +NOKIA,WVCE61Q21024A60S416,ODU, +NOKIA,WVCE61Q2128A30S163,ODU, +NOKIA,WVCE61Q2128A60S29,ODU, +NOKIA,WVCE61Q2128F30S163,ODU, +NOKIA,WVCE61Q216A30S88,ODU, +NOKIA,WVCE61Q216A60S162,ODU, +NOKIA,WVCE61Q22048A30S257,ODU, +NOKIA,WVCE61Q22048A60S461,ODU, +NOKIA,WVCE61Q2256A30S186,ODU, +NOKIA,WVCE61Q2256A60S341,ODU, +NOKIA,WVCE61Q2256F30186,ODU, +NOKIA,WVCE61Q2256F30S186,ODU, +NOKIA,WVCE61Q232A30S109,ODU, +NOKIA,WVCE61Q232A60S201,ODU, +NOKIA,WVCE61Q24A30S44,ODU, +NOKIA,WVCE61Q24QA60S81,ODU, +NOKIA,WVCE61Q2512A30S209,ODU, +NOKIA,WVCE61Q2512A60S383,ODU, +NOKIA,WVCE61Q256F30S186,ODU, +NOKIA,WVCE61Q264A30S139,ODU, +NOKIA,WVCE61Q264A60S254,ODU, +NOKIA,WVCE61Q264F30S139,ODU, +NOKIA,WVCE61Q2QPSKA60S81,ODU, +NOKIA,WVCE61U1024F30S236,ODU, +NOKIA,WVCE61U11024A30S236,ODU, +NOKIA,WVCE61U11024A60S423,ODU, +NOKIA,WVCE61U1128A30S168,ODU, +NOKIA,WVCE61U1128A60S307,ODU, +NOKIA,WVCE61U1128F30S168,ODU, +NOKIA,WVCE61U1128F60S307,ODU, +NOKIA,WVCE61U116A30S91,ODU, +NOKIA,WVCE61U116A60S166,ODU, +NOKIA,WVCE61U12048A30S261,ODU, +NOKIA,WVCE61U12048A60S470,ODU, +NOKIA,WVCE61U1256A30S189,ODU, +NOKIA,WVCE61U1256A60S347,ODU, +NOKIA,WVCE61U128F30S168,ODU, +NOKIA,WVCE61U132A30S114,ODU, +NOKIA,WVCE61U132A60S208,ODU, +NOKIA,WVCE61U14096A30S282,ODU, +NOKIA,WVCE61U14096A30S283,ODU, +NOKIA,WVCE61U14096A60S508,ODU, +NOKIA,WVCE61U14A30S45,ODU, +NOKIA,WVCE61U1512A30S212,ODU, +NOKIA,WVCE61U1512A60S389,ODU, +NOKIA,WVCE61U164A30S142,ODU, +NOKIA,WVCE61U164A60S260,ODU, +NOKIA,WVCE61U164F30S142,ODU, +NOKIA,WVCE61U1A30S45,ODU, +NOKIA,WVCE61U1QA30S45,ODU, +NOKIA,WVCE61U1QA60S83,ODU, +NOKIA,WVCE61U2,ODU, +NOKIA,WVCE61U21024A10S75,ODU, +NOKIA,WVCE61U21024A30S236,ODU, +NOKIA,WVCE61U21024A60423,ODU, +NOKIA,WVCE61U21024A60S423,ODU, +NOKIA,WVCE61U21024A60S423OCM,ODU, +NOKIA,WVCE61U2128A10S54,ODU, +NOKIA,WVCE61U2128A30S168,ODU, +NOKIA,WVCE61U2128A60S307,ODU, +NOKIA,WVCE61U2128A60S307OCM,ODU, +NOKIA,WVCE61U2128F30S168,ODU, +NOKIA,WVCE61U216A10S29,ODU, +NOKIA,WVCE61U216A30S91,ODU, +NOKIA,WVCE61U216A60S166,ODU, +NOKIA,WVCE61U216A60S166OCM,ODU, +NOKIA,WVCE61U22048A10S82,ODU, +NOKIA,WVCE61U22048A30S261,ODU, +NOKIA,WVCE61U22048A60S470,ODU, +NOKIA,WVCE61U22048A60S470OCM,ODU, +NOKIA,WVCE61U2256A10S62,ODU, +NOKIA,WVCE61U2256A30S189,ODU, +NOKIA,WVCE61U2256A60S346,ODU, +NOKIA,WVCE61U2256A60S347,ODU, +NOKIA,WVCE61U2256A60S347OCM,ODU, +NOKIA,WVCE61U2256F30S189,ODU, +NOKIA,WVCE61U232A10S37,ODU, +NOKIA,WVCE61U232A30S114,ODU, +NOKIA,WVCE61U232A60S208,ODU, +NOKIA,WVCE61U232A60S208OCM,ODU, +NOKIA,WVCE61U24096A30S283,ODU, +NOKIA,WVCE61U24096A60S508,ODU, +NOKIA,WVCE61U24096A60S508OCM,ODU, +NOKIA,WVCE61U24A10S14,ODU, +NOKIA,WVCE61U24A30S45,ODU, +NOKIA,WVCE61U2512A10S67,ODU, +NOKIA,WVCE61U2512A30S212,ODU, +NOKIA,WVCE61U2512A60S389,ODU, +NOKIA,WVCE61U2512A60S389OCM,ODU, +NOKIA,WVCE61U2512F30S212,ODU, +NOKIA,WVCE61U256F30S189,ODU, +NOKIA,WVCE61U264A10S46,ODU, +NOKIA,WVCE61U264A30S142,ODU, +NOKIA,WVCE61U264A60S260,ODU, +NOKIA,WVCE61U264A60S260OCM,ODU, +NOKIA,WVCE61U264F30S142,ODU, +NOKIA,WVCE61U2A30S45,ODU, +NOKIA,WVCE61U2QA30S45,ODU, +NOKIA,WVCE61U2QA60S83,ODU, +NOKIA,WVCE61U2QA60S83OCM,ODU, +NOKIA,WVCE61U2QPSKA60S82,ODU, +NOKIA,WVCE61U4096A30S283HP,ODU, +NOKIA,WVCE61U4096A60S508,ODU, +NOKIA,WVCE61U4096A60S508HP,ODU, +NOKIA,WVCE61U512F30S212,ODU, +NOKIA,WVCE61U64F30S142,ODU, +NOKIA,WVCE61UBTSACM,ODU, +NOKIA,WVCE67,Unknown, +NOKIA,WVCE6762512F30S208,Unknown, +NOKIA,WVCE67L,IDU, +AVIAT,WVCE67L,IDU, +NOKIA,WVCE67Q1128F10S53,ODU, +NOKIA,WVCE67Q14A30S44,ODU, +NOKIA,WVCE67Q21024A30S232,ODU, +NOKIA,WVCE67Q2128A30S163,ODU, +NOKIA,WVCE67Q2128F30S163,ODU, +NOKIA,WVCE67Q216A30S88,ODU, +NOKIA,WVCE67Q22048A30S257,ODU, +NOKIA,WVCE67Q2256A30S186,ODU, +NOKIA,WVCE67Q2256F30S186,ODU, +NOKIA,WVCE67Q232A30S109,ODU, +NOKIA,WVCE67Q24A30S44,ODU, +NOKIA,WVCE67Q24QA30S44,ODU, +NOKIA,WVCE67Q2512A30S209,ODU, +NOKIA,WVCE67Q264A30S139,ODU, +NOKIA,WVCE67Q264F30S139,ODU, +NOKIA,WVCE67U1,ODU, +NOKIA,WVCE67U11024A30S236,ODU, +NOKIA,WVCE67U1128A30S168,ODU, +NOKIA,WVCE67U116A30S91,ODU, +NOKIA,WVCE67U12048A30S261,ODU, +NOKIA,WVCE67U1256A30S189,ODU, +NOKIA,WVCE67U132A30S114,ODU, +NOKIA,WVCE67U14096A30S283,ODU, +NOKIA,WVCE67U1512A30S212,ODU, +NOKIA,WVCE67U164A30S142,ODU, +NOKIA,WVCE67U164F10S46,ODU, +NOKIA,WVCE67U1QA30S45,ODU, +NOKIA,WVCE67U2,ODU, +NOKIA,WVCE67U21024A10S75,ODU, +NOKIA,WVCE67U21024A30S236,ODU, +NOKIA,WVCE67U2128A10S54,ODU, +NOKIA,WVCE67U2128A30S168,ODU, +NOKIA,WVCE67U216A10S29,ODU, +NOKIA,WVCE67U216A30S91,ODU, +NOKIA,WVCE67U22048A10S82,ODU, +NOKIA,WVCE67U22048A30S261,ODU, +NOKIA,WVCE67U2256A10S62,ODU, +NOKIA,WVCE67U2256A30S189,ODU, +NOKIA,WVCE67U2256F30S189,ODU, +NOKIA,WVCE67U232A10S37,ODU, +NOKIA,WVCE67U232A30S114,ODU, +NOKIA,WVCE67U24096A30S283,ODU, +NOKIA,WVCE67U24A10S14,ODU, +NOKIA,WVCE67U24A30S45,ODU, +NOKIA,WVCE67U2512A10S67,ODU, +NOKIA,WVCE67U2512A30S212,ODU, +NOKIA,WVCE67U264A10S46,ODU, +NOKIA,WVCE67U264A30S142,ODU, +NOKIA,WVCE67U264F10S46,ODU, +NOKIA,WVCE67U264F30S142,ODU, +NOKIA,WVCE67U2QA10S14,ODU, +NOKIA,WVCE67U2QA30S45,ODU, +NOKIA,WVCE67U64F30S142,ODU, +NOKIA,WVCEL11024A30S227,IDU, +NOKIA,WVCEL164F10S45,IDU, +NOKIA,WVCEL21024A10S72,IDU, +NOKIA,WVCEL21024A30S227,IDU, +NOKIA,WVCEL31024A30S227,IDU, +NOKIA,WVCEU24096A60S508,ODU, +NOKIA,WVCW61L256F30S183,IDU, +NOKIA,WVE67L1256A10S60,IDU, +NOKIA,WVE67L364A30H116,IDU, +NOKIA,WVEC67L364A30H116,IDU, +NERA,XPANDIP6C30128QHP,Unknown, +XYT,YTMWSKYNET,Unknown, +AVIAT,ZI64EL630M267R70,Unknown, +ALCATEL,,Unknown, +AT&T TECHNOLOGIES,,Unknown, +"C O SYSTEMS, INC",,Unknown, +CALIFORNIA MICROWAVE,,Unknown, +COLLINS,,Unknown, +FARINON,,Unknown, +LENKURT,,Unknown, +HARRIS,,Unknown, +Jerrold Electronics,,Unknown, +M/A COM,,Unknown, +MOTOROLA,,Unknown, +MRC,,Unknown, +NEC,,Unknown, +NERA,,Unknown, +NORTEL,,Unknown, +PENINSULA,,Unknown, +ROCKWELL,,Unknown, +SIEMENS,,Unknown, +TADIRAN,,Unknown, +TELESCIENCES,,Unknown, +WESTERN MULTIPLEX,,Unknown, diff --git a/src/ratapi/ratapi/db/sort_callsigns_addfsid.py b/src/ratapi/ratapi/db/sort_callsigns_addfsid.py new file mode 100644 index 0000000..451d71f --- /dev/null +++ b/src/ratapi/ratapi/db/sort_callsigns_addfsid.py @@ -0,0 +1,288 @@ +import csv +import sys +from os.path import exists + +csmapA = {} # Items stored in fsid table (currently FS in US) +csmapB = {} # Items not stored in fsid table (currently FS in CA) +fsidmap = {} + +remMissTxEIRPGFlag = False +filterMaxEIRPFlag = False + + +def sortCallsignsAddFSID(inputPath, fsidTableFile, outputPath, logFile): + logFile.write('Sorting callsigns and adding FSID' + '\n') + if not exists(fsidTableFile): + logFile.write( + "FSIDTable does NOT exist, creating Table at " + + fsidTableFile + + "\n") + with open(fsidTableFile, 'w') as fsidTable: + fsidTable.write( + "FSID,Region,Callsign,Path Number,Center Frequency (MHz),Bandwidth (MHz)\n") + + entriesRead = 0 + highestFSID = 0 + with open(fsidTableFile, 'r') as fsidTable: + csvreaderFSIDTable = csv.reader(fsidTable, delimiter=',') + firstRow = True + firstFSID = True + for row in csvreaderFSIDTable: + if firstRow: + firstRow = False + fsidIdx = -1 + regionIdx = -1 + callsignIdx = -1 + pathIdx = -1 + freqIdx = -1 + bandwidthIdx = -1 + for fieldIdx, field in enumerate(row): + if field == "FSID": + fsidIdx = fieldIdx + elif field == "Region": + regionIdx = fieldIdx + elif field == "Callsign": + callsignIdx = fieldIdx + elif field == "Path Number": + pathIdx = fieldIdx + elif field == "Center Frequency (MHz)": + freqIdx = fieldIdx + elif field == "Bandwidth (MHz)": + bandwidthIdx = fieldIdx + if fsidIdx == -1: + sys.exit('ERROR: Invalid FSID table file, FSID not found') + if regionIdx == -1: + sys.exit('ERROR: Invalid FSID table file, Region not found') + if callsignIdx == -1: + sys.exit('ERROR: Invalid FSID table file, callsign not found') + if pathIdx == -1: + sys.exit( + 'ERROR: Invalid FSID table file, path Number not found') + if freqIdx == -1: + sys.exit( + 'ERROR: Invalid FSID table file, Center Frequency (MHz) not found') + if bandwidthIdx == -1: + sys.exit( + 'ERROR: Invalid FSID table file, Bandwidth (MHz) not found') + else: + fsid = int(row[fsidIdx]) + region = row[regionIdx] + cs = row[callsignIdx] + path = int(row[pathIdx]) + freq = float(row[freqIdx]) + bandwidth = float(row[bandwidthIdx]) + keyv = tuple([region, cs, path, freq, bandwidth]) + fsidmap[keyv] = fsid + if firstFSID or fsid > highestFSID: + highestFSID = fsid + firstFSID = False + entriesRead += 1 + + logFile.write( + "Read " + + str(entriesRead) + + " entries from FSID table file: " + + fsidTableFile + + ", Max FSID = " + + str(highestFSID) + + "\n") + + entriesAdded = 0 + count = 0 + with open(inputPath, 'r') as f: + with open(outputPath, 'w') as fout: + with open(fsidTableFile, 'a+') as fsidTable: + csvreader = csv.reader(f, delimiter=',') + csvwriter = csv.writer(fout, delimiter=',') + nextFSID = highestFSID + 1 + firstRow = True + for row in csvreader: + if firstRow: + row.append("record") + row.append("lowest_dig_mod_rate") + row.append("highest_dig_mod_rate") + if filterMaxEIRPFlag: + row.append("highest_tx_eirp") + firstRow = False + regionIdx = -1 + callsignIdx = -1 + pathIdx = -1 + freqIdx = -1 + emdegIdx = -1 + digitalModRateIdx = -1 + txEirpIdx = -1 + txGainIdx = -1 + for fieldIdx, field in enumerate(row): + if field == "Region": + regionIdx = fieldIdx + elif field == "Callsign": + callsignIdx = fieldIdx + elif field == "Path Number": + pathIdx = fieldIdx + elif field == "Center Frequency (MHz)": + freqIdx = fieldIdx + elif field == "Bandwidth (MHz)": + bandwidthIdx = fieldIdx + elif field == "Digital Mod Rate": + digitalModRateIdx = fieldIdx + elif field == "Tx EIRP (dBm)": + txEirpIdx = fieldIdx + elif field == "Tx Gain (dBi)": + txGainIdx = fieldIdx + if regionIdx == -1: + sys.exit('ERROR: Region not found') + if callsignIdx == -1: + sys.exit('ERROR: Callsign not found') + if pathIdx == -1: + sys.exit('ERROR: Path Number not found') + if freqIdx == -1: + sys.exit('ERROR: Center Frequency (MHz) not found') + if bandwidthIdx == -1: + sys.exit('ERROR: Bandwidth (MHz) not found') + if digitalModRateIdx == -1: + sys.exit('ERROR: Digital Mod Rate not found') + if txEirpIdx == -1: + sys.exit('ERROR: Tx EIRP (dBm) not found') + if remMissTxEIRPGFlag and (txGainIdx == -1): + sys.exit('ERROR: Tx Gain (dBi) not found') + + row.insert(0, "FSID") + csvwriter.writerow(row) + else: + region = row[regionIdx] + cs = row[callsignIdx] + path = int(row[pathIdx]) + freq = float(row[freqIdx]) + bandwidth = float(row[bandwidthIdx]) + if remMissTxEIRPGFlag and ( + row[txEirpIdx].strip() == '' or row[txGainIdx].strip() == ''): + logFile.write( + "Removed entry missing Tx EIRP or Tx Gain") + else: + if region == 'US': + keyv = tuple( + [region, cs, path, freq, bandwidth]) + if keyv in csmapA: + csmapA[keyv].append(row) + else: + csmapA[keyv] = [row] + else: + # For CA, dont remove any links, make keys + # unique for each link + keyv = tuple([region, count]) + if keyv in csmapB: + sys.exit('ERROR: Invalid key') + else: + csmapB[keyv] = [row] + count += 1 + + for typeIdx in range(2): + if typeIdx == 0: + all_cs = csmapA.keys() + elif typeIdx == 1: + all_cs = csmapB.keys() + else: + sys.exit('ERROR: Invalid typeIdx') + + for keyv in sorted(all_cs): + rate_idx_list = [] + if typeIdx == 0: + for (ri, r) in enumerate(csmapA[keyv]): + if r[digitalModRateIdx].strip() == '': + rate = 0.0 + else: + rate = float(r[digitalModRateIdx]) + rate_idx = tuple([rate, ri]) + rate_idx_list.append(rate_idx) + else: + for (ri, r) in enumerate(csmapB[keyv]): + if r[digitalModRateIdx].strip() == '': + rate = 0.0 + else: + rate = float(r[digitalModRateIdx]) + rate_idx = tuple([rate, ri]) + rate_idx_list.append(rate_idx) + + rate_idx_list.sort() + + for (idx, rate_idx) in enumerate(rate_idx_list): + ri = rate_idx[1] + if typeIdx == 0: + r = csmapA[keyv][ri] + else: + r = csmapB[keyv][ri] + if filterMaxEIRPFlag: + txEirp = float(r[txEirpIdx]) + if idx == 0 or txEirp > maxEirp: + maxEirp = txEirp + maxEirpIdx = ri + initFlag = True + recordNum = 1 + for rate_idx in rate_idx_list: + rate = rate_idx[0] + ri = rate_idx[1] + if typeIdx == 0: + r = csmapA[keyv][ri] + else: + r = csmapB[keyv][ri] + if rate == 0.0: + lowRateFlag = 2 + highRateFlag = 2 + else: + if initFlag: + lowRateFlag = 1 + initFlag = False + else: + lowRateFlag = 0 + + if recordNum == len(rate_idx_list): + highRateFlag = 1 + else: + highRateFlag = 0 + + r.append(str(recordNum)) + r.append(str(lowRateFlag)) + r.append(str(highRateFlag)) + if filterMaxEIRPFlag: + if ri == maxEirpIdx: + printFlag = 1 + r.append(str(1)) + else: + printFlag = 0 + r.append(str(0)) + else: + if recordNum == len(rate_idx_list): + printFlag = 1 + else: + printFlag = 0 + + if printFlag: + if typeIdx == 0: + if keyv in fsidmap: + fsid = fsidmap[keyv] + else: + fsid = nextFSID + nextFSID += 1 + fsidTable.write(str(fsid) + + "," + + keyv[0] + + "," + + keyv[1] + + "," + + str(keyv[2]) + + "," + + str(keyv[3]) + + "," + + str(keyv[4]) + + "\n") + entriesAdded += 1 + else: + fsid = nextFSID + nextFSID += 1 + r.insert(0, str(fsid)) + csvwriter.writerow(r) + + recordNum = recordNum + 1 + + logFile.write("Added " + str(entriesAdded) + + " entries to FSID table file: " + fsidTableFile + "\n") diff --git a/src/ratapi/ratapi/manage.py b/src/ratapi/ratapi/manage.py new file mode 100644 index 0000000..882c5dd --- /dev/null +++ b/src/ratapi/ratapi/manage.py @@ -0,0 +1,1632 @@ +# +# This Python file uses the following encoding: utf-8 +# +# Portions copyright (C) 2021 Broadcom. +# All rights reserved. The term “Broadcom” refers solely +# to the Broadcom Inc. corporate affiliate that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which +# is included with this software program. +# +''' External management of this application. +''' + +import json +import logging +import os +import ratapi +import shutil +import sqlalchemy +import time +from flask_migrate import MigrateCommand +from . import create_app +from afcmodels.base import db +from .db.generators import shp_to_spatialite, spatialite_to_raster +from prettytable import PrettyTable +from flask_script import Manager, Command, Option, commands +from . import cmd_utils +import als + +LOGGER = logging.getLogger(__name__) + + +def json_lookup(key, json_obj, val): + """Loookup for key in json and change it value if required""" + keepit = [] + + def lookup(key, json_obj, val, keepit): + '''lookup for key in json + ''' + if isinstance(json_obj, dict): + for k, v in json_obj.items(): + # LOGGER.debug('%s ... %s', k, type(v)) + if k == key: + keepit.append(v) + if val: + json_obj[k] = val + elif isinstance(v, (dict, list)): + lookup(key, v, val, keepit) + elif isinstance(json_obj, list): + for node in json_obj: + lookup(key, node, val, keepit) + return keepit + + found = lookup(key, json_obj, val, keepit) + return found + + +def get_or_create(session, model, **kwargs): + ''' Ensure a specific object exists in the DB. + ''' + instance = session.query(model).filter_by(**kwargs).first() + if instance: + return instance + else: + instance = model(**kwargs) + session.add(instance) + session.commit() + return instance + + +class CleanHistory(Command): # pylint: disable=abstract-method + ''' Remove old history files + ''' + + # no extra options needed + option_list = () + + def __call__(self, flaskapp): # pylint: disable=signature-differs + + history_dir = flaskapp.config['HISTORY_DIR'] + if history_dir is None: + return + + LOGGER.debug('Removing history files from "%s"...', history_dir) + + now = time.time() + # delete file if older than 14 days + cutoff = now - (14 * 86400) + + logs = os.listdir(history_dir) + for record in logs: + t = os.stat(os.path.join(history_dir, record)) + c = t.st_ctime + + if c < cutoff: + shutil.rmtree(os.path.join(history_dir, record)) + + +class CleanTmpFiles(Command): + ''' Remove old temporary files that have been orphaned by web clients + ''' + + # no extra options needed + option_list = () + + def __call__(self, flaskapp): + + task_queue = flaskapp.config['TASK_QUEUE'] + # the [7:] removes file:// prefix + + LOGGER.debug('Removing temp files from "%s"...', task_queue) + + now = time.time() + # delete file if older than 14 days + cutoff = now - (14 * 86400) + + logs = os.listdir(task_queue) + for record in logs: + t = os.stat(os.path.join(task_queue, record)) + c = t.st_ctime + + if c < cutoff: + shutil.rmtree(os.path.join(task_queue, record)) + + +class RasterizeBuildings(Command): + ''' Convert building shape file into tiff raster + ''' + + option_list = ( + Option('--source', '-s', type=str, dest='source', required=True, + help="source shape file"), + Option('--target', '-t', type=str, dest='target', default=None, + help="target raster file"), + Option('--layer', '-l', type=str, dest='layer', required=True, + help="layer name to access polygons from"), + Option('--attribute', '-a', type=str, dest='attribute', required=True, + help="attribute name to access height data from"), + ) + + def __call__(self, flaskapp, source, target, layer, attribute): + from db.generators import shp_to_spatialite, spatialite_to_raster + + if target is None: + target = os.path.splitext(source)[0] + + if not os.path.exists(source): + raise RuntimeError( + '"{}" source file does not exist'.format(source)) + + shp_to_spatialite(target + '.sqlite3', source) + + spatialite_to_raster(target + '.tiff', target + + '.sqlite3', layer, attribute) + + +class Data(Manager): + ''' View and manage data files in RAT ''' + + def __init__(self, *args, **kwargs): + Manager.__init__(self, *args, **kwargs) + self.add_command('clean-history', CleanHistory()) + self.add_command('clean-tmp-files', CleanTmpFiles()) + self.add_command('rasterize-buildings', RasterizeBuildings()) + + +class DbCreate(Command): + ''' Create a full new database outside of alembic migrations. ''' + + def __call__(self, flaskapp): + LOGGER.debug('DbCreate.__call__()') + with flaskapp.app_context(): + from afcmodels.aaa import Role, Ruleset + from .views.ratapi import rulesets + from flask_migrate import stamp + ruleset_list = rulesets() + db.create_all() + get_or_create(db.session, Role, name='Admin') + get_or_create(db.session, Role, name='Super') + get_or_create(db.session, Role, name='Analysis') + get_or_create(db.session, Role, name='AP') + get_or_create(db.session, Role, name='Trial') + for rule in ruleset_list: + get_or_create(db.session, Ruleset, name=rule) + stamp(revision='head') + + +class DbDrop(Command): + ''' Create a full new database outside of alembic migrations. ''' + + def __call__(self, flaskapp): + LOGGER.debug('DbDrop.__call__()') + with flaskapp.app_context(): + from afcmodels.aaa import User, Role + db.drop_all() + + +class DbExport(Command): + ''' Export database in db to a file in json. ''' + + option_list = ( + Option('--dst', type=str, help='export user data file'), + ) + + def __init__(self, *args, **kwargs): + LOGGER.debug('DbExport.__init__()') + + def __call__(self, flaskapp, dst): + LOGGER.debug('DbExportPrev.__call__()') + from afcmodels.aaa import User, UserRole, Role, Limit + + filename = dst + + with flaskapp.app_context(): + limit = None + user_info = {} + user_cfg = {} + for user, _, role in db.session.query( + User, UserRole, Role).filter( + User.id == UserRole.user_id).filter( + UserRole.role_id == Role.id).all(): # pylint: disable=no-member + key = user.email + ":" + str(user.id) + if key in user_cfg: + user_cfg[key]['rolename'].append(role.name) + else: + user_cfg[key] = { + 'id': user.id, + 'email': user.email, + 'password': user.password, + 'rolename': [role.name], + 'ap': [] + } + + try: + user_cfg[key]['username'] = user.username + except BaseException: + # if old db has no username field, use email field. + user_cfg[key]['username'] = user.email + + try: + user_cfg[key]['org'] = user.org + except BaseException: + # if old db has no org field, derive from email field. + if '@' in user.email: + user_cfg[key]['org'] = user.email[user.email.index( + '@') + 1:] + else: # dummy user - dummy org + user_cfg[key]['org'] = "" + + try: + limits = db.session.query(Limit).filter(id=0).first() + limit = {'min_eirp': float( + limits.min_eirp), 'enforce': bool(limits.enforce)} + + except BaseException: + LOGGER.debug("Error exporting EIRP Limit") + + with open(filename, 'w') as fpout: + for k, v in user_cfg.items(): + fpout.write("%s\n" % json.dumps({'userConfig': v})) + if limit: + fpout.write("%s\n" % json.dumps({'Limit': limit})) + + +def setUserIdNextVal(): + # Set nextval for the sequence so that next user record + # will not reuse older id. + cmd = 'select max(id) from aaa_user' + res = db.session.execute(cmd) + val = res.fetchone()[0] + if val: + cmd = 'ALTER SEQUENCE aaa_user_id_seq RESTART WITH ' + str(val + 1) + db.session.execute(cmd) + db.session.commit() + + +class DbImport(Command): + ''' Import User Database. ''' + option_list = ( + Option('--src', type=str, help='configuration source file'), + ) + + def __init__(self, *args, **kwargs): + LOGGER.debug('DbImport.__init__()') + + def __call__(self, flaskapp, src): + LOGGER.debug('DbImport.__call__() %s', src) + from afcmodels.aaa import User, Limit + + filename = src + if not os.path.exists(filename): + raise RuntimeError( + '"{}" source file does not exist'.format(filename)) + + LOGGER.debug('Open admin cfg src file - %s', filename) + with flaskapp.app_context(): + with open(filename, 'r') as fp_src: + while True: + dataline = fp_src.readline() + if not dataline: + break + # add user, APs and server configuration + new_rcrd = json.loads(dataline) + user_rcrd = json_lookup('userConfig', new_rcrd, None) + if user_rcrd: + username = json_lookup('username', user_rcrd, None) + try: + UserCreate(flaskapp, user_rcrd[0], True) + except RuntimeError: + LOGGER.debug('User %s already exists', username[0]) + + else: + limit = json_lookup('Limit', new_rcrd, None) + try: + limits = db.session.query( + Limit).filter_by(id=0).first() + # insert case + if limits is None: + limits = Limit(limit[0]['min_eirp']) + db.session.add(limits) + elif (limit[0]['enforce'] == False): + limits.enforce = False + else: + limits.min_eirp = limit[0]['min_eirp'] + limits.enforce = True + db.session.commit() + except BaseException: + raise RuntimeError( + "Can't commit DB for EIRP limits") + + setUserIdNextVal() + + +class DbUpgrade(Command): + ''' Upgrade User Database. ''' + + def __init__(self, *args, **kwargs): + LOGGER.debug('DbUpgrade.__init__()') + + def __call__(self, flaskapp): + with flaskapp.app_context(): + import flask + from afcmodels.aaa import Ruleset + from .views.ratapi import rulesets + from flask_migrate import (upgrade, stamp) + setUserIdNextVal() + try: + from afcmodels.aaa import User + user = db.session.query( + User).first() # pylint: disable=no-member + except Exception as exception: + if 'aaa_user.username does not exist' in str(exception.args): + LOGGER.error("upgrade from preOIDC version") + stamp(revision='4c904e86218d') + elif 'aaa_user.org does not exist' in str(exception.args): + LOGGER.error("upgrade from mtls version") + stamp(revision='230b7680b81e') + db.session.commit() + upgrade() + ruleset_list = rulesets() + for rule in ruleset_list: + get_or_create(db.session, Ruleset, name=rule) + + +class UserCreate(Command): + ''' Create a new user functionality. ''' + + option_list = ( + Option('username', type=str, + help='User name'), + Option('password_in', type=str, + help='Users password\n' + 'example: rat-manage-api' + ' user create email password'), + Option('--role', type=str, default=[], action='append', + choices=['Admin', 'Super', 'Analysis', 'AP', 'Trial'], + help='role to include with the new user'), + Option('--org', type=str, help='Organization'), + Option('--email', type=str, help='User email'), + ) + + def _create_user(self, flaskapp, id, username, email, password_in, role, + hashed, org=None): + ''' Create user in database. ''' + from contextlib import closing + import datetime + from afcmodels.aaa import User, Role, Organization + LOGGER.debug('UserCreate.__create_user() %s %s %s', + email, password_in, role) + + if 'UPGRADE_REQ' in flaskapp.config and flaskapp.config['UPGRADE_REQ']: + return + try: + if not hashed: + # hash password field in non OIDC mode + if isinstance(password_in, str): + password = password_in.strip() + else: + with closing(open(password_in)) as pwfile: + password = pwfile.read().strip() + + if flaskapp.config['OIDC_LOGIN']: + # OIDC, password is never stored locally + # Still we hash it so that if we switch back + # to non OIDC, the hash still match, and can be logged in + from passlib.context import CryptContext + password_crypt_context = CryptContext(['bcrypt']) + passhash = password_crypt_context.encrypt(password_in) + else: + passhash = flaskapp.user_manager.password_manager.hash_password( + password) + else: + passhash = password_in + + with flaskapp.app_context(): + # select count(*) from aaa_user where email = ? + if User.query.filter(User.email == email).count() > 0: + raise RuntimeError( + 'Existing user found with email "{0}"'.format(email)) + + if not org: + try: + org = email[email.index('@') + 1:] + except BaseException: + org = "" + + organization = Organization.query.filter_by(name=org).first() + if not organization: + organization = Organization(name=org) + db.session.add(organization) + + if id: + user = User( + id=id, + username=username, + email=email, + org=org, + email_confirmed_at=datetime.datetime.now(), + password=passhash, + active=True, + ) + else: + user = User( + username=username, + email=email, + org=org, + email_confirmed_at=datetime.datetime.now(), + password=passhash, + active=True, + ) + for rolename in role: + user.roles.append(get_or_create( + db.session, Role, name=rolename)) + db.session.add(user) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + except IOError: + raise RuntimeError( + 'Password received was not readable.' + 'Enter as a readable pipe.\n' + 'i.e. echo "pass" | rat-manage api' + 'user create email /dev/stdin') + + def __init__(self, flaskapp=None, user_params=None, hashed=False): + if flaskapp and isinstance(user_params, dict): + if 'id' in user_params.keys(): + id = user_params['id'] + else: + id = None + + if 'email' in user_params.keys(): + email = user_params['email'] + else: + email = user_params['username'] + + if 'org' in user_params.keys(): + org = user_params['org'] + else: + org = None + + self._create_user(flaskapp, + id, + user_params['username'], + email, + user_params['password'], + user_params['rolename'], + hashed, + org) + + def __call__(self, flaskapp, username, password_in, role, hashed=False, + org=None, email=None): + # If command does not provide email. Populate email field with + # username + if not email: + email = username + self._create_user(flaskapp, None, username, email, password_in, role, + hashed, org) + + +class UserUpdate(Command): + ''' Create a new user functionality. ''' + + option_list = ( + Option('--email', type=str, + help='Email'), + Option('--role', type=str, default=[], action='append', + choices=['Admin', 'Super', 'Analysis', 'AP', 'Trial'], + help='role to include with the new user'), + Option('--org', type=str, help='Organization'), + ) + + def _update_user(self, flaskapp, email, role, org=None): + ''' Create user in database. ''' + from contextlib import closing + from afcmodels.aaa import User, Role, Organization + + if 'UPGRADE_REQ' in flaskapp.config and flaskapp.config['UPGRADE_REQ']: + return + + try: + with flaskapp.app_context(): + user = User.getemail(email) + + if user: + user.roles = [] + for rolename in role: + user.roles.append(get_or_create( + db.session, Role, name=rolename)) + user.active = True + if org: + user.org = org + org = user.org + organization = Organization.query.filter_by( + name=org).first() + if not organization: + organization = aaa.Organization(name=org) + db.session.add( + organization) # pylint: disable=no-member + + db.session.commit() # pylint: disable=no-member + else: + raise RuntimeError("User update: User not found") + except IOError: + raise RuntimeError("User update encounters unexpected error") + + def __init__(self, flaskapp=None, user_params=None): + if flaskapp and isinstance(user_params, dict): + self._update_user(flaskapp, + user_params['username'], + user_params['rolename'], + user_params['org']) + + def __call__(self, flaskapp, email, role, org): + self._update_user(flaskapp, email, role, org) + + +class UserRemove(Command): + ''' Remove a user by email. ''' + + option_list = ( + Option('email', type=str, + help="user's email address"), + ) + + def _remove_user(self, flaskapp, email): + from afcmodels.aaa import User, Role + LOGGER.debug('UserRemove._remove_user() %s', email) + + with flaskapp.app_context(): + try: + # select * from aaa_user where email = ? limit 1 + user = User.query.filter(User.email == email).one() + except sqlalchemy.orm.exc.NoResultFound: + raise RuntimeError( + 'No user found with email "{0}"'.format(email)) + db.session.delete(user) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + + def __init__(self, flaskapp=None, email=None): + if flaskapp and email: + self._remove_user(flaskapp, email) + + def __call__(self, flaskapp, email): + self._remove_user(flaskapp, email) + + +class UserList(Command): + '''Lists all users.''' + + def __call__(self, flaskapp): + LOGGER.debug('UserList.__call__()') + table = PrettyTable() + from afcmodels.aaa import User, UserRole, Role + table.field_names = ["ID", "UserName", "Email", "Org", "Roles"] + + if 'UPGRADE_REQ' in flaskapp.config and flaskapp.config['UPGRADE_REQ']: + return + + with flaskapp.app_context(): + user_info = {} + # select email, name from aaa_user as a join aaa_user_role as aur + # on au.id = aur.user_id join aaa_role as ar on ar.id = + # aur.role_id; + for user, _, role in db.session.query( + User, UserRole, Role).filter( + User.id == UserRole.user_id).filter( + UserRole.role_id == Role.id).all(): # pylint: disable=no-member + + key = user.email + ":" + user.org + ":" + \ + str(user.id) + ":" + user.username + + if key in user_info: + user_info[key] = user_info[key] + ", " + role.name + else: + user_info[key] = role.name + + # Find all users without roles and show them last + for user in db.session.query(User).filter(~User.roles.any()).all(): + key = user.email + ":" + user.org + ":" + \ + str(user.id) + ":" + user.username + user_info[key] = "" + + for k, v in user_info.items(): + email, org, _id, name = k.split(":") + table.add_row([_id, name, email, org, v]) + + print(table) + + +class User(Manager): + ''' View and manage AAA state ''' + + def __init__(self, *args, **kwargs): + LOGGER.debug('User.__init__()') + Manager.__init__(self, *args, **kwargs) + self.add_command('update', UserUpdate()) + self.add_command('create', UserCreate()) + self.add_command('remove', UserRemove()) + self.add_command('list', UserList()) + + +class AccessPointDenyCreate(Command): + ''' Create a new access point. ''' + + option_list = ( + Option('--serial', type=str, default=None, + help='serial number of the ap'), + Option('--cert_id', type=str, default=None, required=True, + help='certification id of the ap'), + Option('--ruleset', type=str, default=None, required=True, + help='ruleset of the ap'), + Option('--org', type=str, default=None, required=True, + help='org of the ap'), + ) + + def _create_ap(self, flaskapp, serial, cert_id, ruleset, org): + from contextlib import closing + import datetime + from afcmodels.aaa import AccessPointDeny, Organization, Ruleset + LOGGER.debug('AccessPointDenyCreate._create_ap() %s %s %s', + serial, cert_id, ruleset) + with flaskapp.app_context(): + if not cert_id or not org or not ruleset: + raise RuntimeError('Certification, org and ruleset required') + + ruleset = Ruleset.query.filter_by(name=ruleset).first() + if not ruleset: + raise RuntimeError('Bad ruleset') + + ap = AccessPointDeny.query.filter(AccessPointDeny. + certification_id == cert_id).\ + filter(AccessPointDeny.serial_number == serial).first() + if ap: + raise RuntimeError('Duplicate device detected') + + organization = Organization.query.filter_by(name=org).first() + if not organization: + organization = Organization(name=org) + db.session.add(organization) + ap = AccessPointDeny( + serial_number=serial, + certification_id=cert_id + ) + + organization.aps.append(ap) + ruleset.aps.append(ap) + db.session.add(ap) + db.session.commit() + + def __init__(self, flaskapp=None, serial_id=None, + cert_id=None, ruleset=None, org=None): + if flaskapp: + self._create_ap(flaskapp, str(serial_id), cert_id, ruleset, org) + + def __call__(self, flaskapp, serial=None, + cert_id=None, ruleset=None, org=None): + self._create_ap(flaskapp, serial, cert_id, ruleset, org) + + +class AccessPointDenyRemove(Command): + '''Removes an access point by serial number and or certification id''' + + option_list = ( + Option('--serial', type=str, default=None, + help='Serial number of an Access Point'), + Option('--cert_id', type=str, default=None, + help='certification id of the ap'), + ) + + def _remove_ap(self, flaskapp, serial, cert_id): + from afcmodels.aaa import AccessPointDeny + LOGGER.debug('AccessPointDenyRemove._remove_ap() %s', serial) + with flaskapp.app_context(): + try: + # select * from access_point as ap where ap.serial_number = ? + ap = AccessPointDeny.query.filter( + AccessPointDeny.serial_number == serial).\ + filter(AccessPointDeny.certification_id == cert_id).one() + + if not ap: + raise RuntimeError('No access point found') + except BaseException: + raise RuntimeError('No access point found') + + db.session.delete(ap) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + + def __init__(self, flaskapp=None, serial=None, cert_id=None): + if flaskapp: + self._remove_ap(flaskapp, serial, cert_id) + + def __call__(self, flaskapp, serial=None, cert_id=None): + self._remove_ap(flaskapp, serial, cert_id) + + +class AccessPointDenyList(Command): + '''Lists all access points''' + + def __call__(self, flaskapp): + table = PrettyTable() + from afcmodels.aaa import AccessPointDeny + + table.field_names = ["Serial Number", "Cert ID", "Ruleset", "Org"] + with flaskapp.app_context(): + for ap in db.session.query( + AccessPointDeny).all(): # pylint: disable=no-member + table.add_row([ap.serial_number, ap.certification_id, + ap.ruleset.name, ap.org.name]) + print(table) + + +class AccessPointsDeny(Manager): + '''View and manage Access Points Denied''' + + def __init__(self, *args, **kwargs): + Manager.__init__(self, *args, **kwargs) + self.add_command("create", AccessPointDenyCreate()) + self.add_command("remove", AccessPointDenyRemove()) + self.add_command("list", AccessPointDenyList()) + + +class CertificationId(Manager): + '''View and manage CertificationId ''' + + def __init__(self, *args, **kwargs): + Manager.__init__(self, *args, **kwargs) + self.add_command("create", CertIdCreate()) + self.add_command("remove", CertIdRemove()) + self.add_command("list", CertIdList()) + self.add_command("sweep", CertIdSweep()) + + +class CertIdList(Command): + '''Lists all access points''' + + def __call__(self, flaskapp): + table = PrettyTable() + from afcmodels.aaa import CertId, Organization + + table.field_names = ["Cert ID", "Ruleset", "Loc", "Refreshed"] + with flaskapp.app_context(): + for cert in db.session.query( + CertId).all(): # pylint: disable=no-member + table.add_row([cert.certification_id, cert.ruleset.name, + cert.location, cert.refreshed_at]) + print(table) + + +class CertIdRemove(Command): + '''Removes an Certificate Id by certificate id ''' + + option_list = ( + Option('--cert_id', type=str, + help='certificate id', + required=True), + ) + + def _remove_cert_id(self, flaskapp, cert_id): + from afcmodels.aaa import CertId + LOGGER.debug('CertIdRemove._remove_cert_id() %s', cert_id) + with flaskapp.app_context(): + try: + cert = CertId.query.filter( + CertId.certification_id == cert_id).one() + except sqlalchemy.orm.exc.NoResultFound: + raise RuntimeError( + 'No certificate found with id "{0}"'.format(cert_id)) + db.session.delete(cert) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + + def __init__(self, flaskapp=None, cert_id=None): + if flaskapp: + self._remove_cert_id(flaskapp, cert_id) + + def __call__(self, flaskapp, cert_id): + self._remove_cert_id(flaskapp, cert_id) + + +class CertIdCreate(Command): + ''' Create a new certification Id. ''' + option_list = ( + Option('--cert_id', type=str, + help='certification id', + required=True), + Option('--ruleset_id', type=str, + help='ruleset id', + required=True), + Option('--location', type=int, + help="location. 1 indoor - 2 outdoor - 3 both", + required=True) + ) + + def _create_cert_id(self, flaskapp, cert_id, ruleset_id, location=0): + from contextlib import closing + import datetime + from afcmodels.aaa import CertId, Ruleset, Organization + LOGGER.debug('CertIdCreate._create_cert_id() %s %s', + cert_id, ruleset_id) + with flaskapp.app_context(): + if not ruleset_id: + raise RuntimeError("Ruleset is required") + + # validate ruleset + ruleset = Ruleset.query.filter_by(name=ruleset_id).first() + if not ruleset: + raise RuntimeError("Invalid Ruleset") + + if CertId.query.filter( + CertId.certification_id == cert_id).count() > 0: + raise RuntimeError( + 'Existing certificate found with id "{0}"'.format(cert_id)) + + cert = CertId(certification_id=cert_id, location=location) + ruleset.cert_ids.append(cert) + + db.session.add(cert) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + + def __init__(self, flaskapp=None, cert_id=None, + ruleset_id=None, location=0): + if flaskapp and cert_id: + self._create_cert_id(flaskapp, cert_id, ruleset_id=ruleset_id, + location=location) + + def __call__(self, flaskapp, cert_id, ruleset_id=None, location=0): + self._create_cert_id(flaskapp, cert_id, ruleset_id, location) + + +class CertIdSweep(Command): + '''Lists all access points''' + option_list = ( + Option('--country', type=str, + help='country e.g. US or CA'), + ) + + def sweep_canada(self, flaskapp): + import csv + import requests + from .views.ratapi import regionStrToRulesetId + from afcmodels.aaa import CertId, Ruleset + import datetime + now = datetime.datetime.now() + + with flaskapp.app_context(): + url = \ + "https://www.ic.gc.ca/engineering/Certified_Standard_Power_Access_Points_6GHz.csv" + headers = { + 'accept': 'text/html,application/xhtml+xml,application/xml', + 'cache-control': 'max-age=0', + 'content-type': 'application/x-www-form-urlencoded', + 'user-agent': 'rat_server/1.0' + } + try: + with requests.get(url, headers, stream=True) as r: + r.raise_for_status() + + local_filename = "/tmp/SD6_list.csv" + with open(local_filename, 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + + with open(local_filename, newline='') as csvfile: + rdr = csv.reader(csvfile, delimiter=',') + for row in rdr: + try: + cert_id = row[7] + code = int(row[6]) + if code == 103: + location = CertId.OUTDOOR + elif code == 111: + location = CertId.INDOOR | CertId.OUTDOOR + else: + location = CertId.UNKNOWN + if not location == CertId.UNKNOWN: + cert = CertId.query.filter_by( + certification_id=cert_id).first() + if cert: + cert.refreshed_at = now + cert.location = location + else: + cert = CertId(certification_id=cert_id, + location=location) + ruleset_id_str = regionStrToRulesetId("CA") + ruleset = Ruleset.query.filter_by( + name=ruleset_id_str).first() + ruleset.cert_ids.append(cert) + db.session.add(cert) + except BaseException: + # ignore badly formatted rows + pass + als.als_json_log( + 'cert_db', { + 'action': 'sweep', 'country': 'CA', 'status': 'success'}) + db.session.commit() # pylint: disable=no-member + except BaseException: + als.als_json_log( + 'cert_db', { + 'action': 'sweep', 'country': 'CA', 'status': 'failed download'}) + self.mailAlert(flaskapp, 'CA', 'none') + + def sweep_fcc_id(self, flaskapp): + from afcmodels.aaa import CertId + id_data = "grantee_code=&product_code=&applicant_name=&grant_date_from=&grant_date_to=&comments=&application_purpose=&application_purpose_description=&grant_code_1=&grant_code_2=&grant_code_3=&test_firm=&application_status=&application_status_description=&equipment_class=243&equipment_class_description=6ID-15E+6+GHz+Low+Power+Indoor+Access+Point&lower_frequency=&upper_frequency=&freq_exact_match=on&bandwidth_from=&emission_designator=&tolerance_from=&tolerance_to=&tolerance_exact_match=on&power_output_from=&power_output_to=&power_exact_match=on&rule_part_1=&rule_part_2=&rule_part_3=&rule_part_exact_match=on&product_description=&modular_type_description=&tcb_code=&tcb_code_description=&tcb_scope=&tcb_scope_description=&outputformat=XML&show_records=10&fetchfrom=0&calledFromFrame=N" # noqa + self.sweep_fcc_data(flaskapp, id_data, CertId.INDOOR) + + def sweep_fcc_sd(self, flaskapp): + from afcmodels.aaa import CertId + + sd_data = "grantee_code=&product_code=&applicant_name=&grant_date_from=&grant_date_to=&comments=&application_purpose=&application_purpose_description=&grant_code_1=&grant_code_2=&grant_code_3=&test_firm=&application_status=&application_status_description=&equipment_class=250&equipment_class_description=6SD-15E+6+GHz+Standard+Power+Access+Point&lower_frequency=&upper_frequency=&freq_exact_match=on&bandwidth_from=&emission_designator=&tolerance_from=&tolerance_to=&tolerance_exact_match=on&power_output_from=&power_output_to=&power_exact_match=on&rule_part_1=&rule_part_2=&rule_part_3=&rule_part_exact_match=on&product_description=&modular_type_description=&tcb_code=&tcb_code_description=&tcb_scope=&tcb_scope_description=&outputformat=XML&show_records=10&fetchfrom=0&calledFromFrame=N" # noqa + self.sweep_fcc_data(flaskapp, sd_data, CertId.OUTDOOR) + + def sweep_fcc_data(self, flaskapp, data, location): + from afcmodels.aaa import CertId, Ruleset + from .views.ratapi import regionStrToRulesetId + import requests + import datetime + + now = datetime.datetime.now() + with flaskapp.app_context(): + url = 'https://apps.fcc.gov/oetcf/eas/reports/GenericSearchResult.cfm?RequestTimeout=500' + headers = { + 'accept': 'text/html,application/xhtml+xml,application/xml', + 'cache-control': 'max-age=0', + 'content-type': 'application/x-www-form-urlencoded', + 'user-agent': 'rat_server/1.0' + } + + try: + resp = requests.post(url, headers=headers, data=data) + if resp.status_code == 200: + try: + from xml.etree import ElementTree + tree = ElementTree.fromstring(resp.content) + for node in tree: + fcc_id = node.find('fcc_id').text + cert = CertId.query.filter_by( + certification_id=fcc_id).first() + + if cert: + cert.refreshed_at = now + cert.location = cert.location | location + elif location == CertId.OUTDOOR: + # add new entries that are in 6SD list. + cert = CertId(certification_id=fcc_id, + location=CertId.OUTDOOR) + ruleset_id_str = regionStrToRulesetId("US") + ruleset = Ruleset.query.filter_by( + name=ruleset_id_str).first() + ruleset.cert_ids.append(cert) + db.session.add(cert) + + als.als_json_log( + 'cert_db', { + 'action': 'sweep', 'country': 'US', 'status': 'success'}) + except BaseException: + raise RuntimeError("Bad XML in Cert Id download") + else: + raise RuntimeError("Bad Cert Id download") + db.session.commit() # pylint: disable=no-member + except BaseException: + als.als_json_log( + 'cert_db', { + 'action': 'sweep', 'country': 'US', 'status': 'failed download'}) + self.mailAlert(flaskapp, 'US', str(location)) + + def mailAlert(self, flaskapp, country, other): + import flask + from flask_mail import Mail, Message + + with flaskapp.app_context(): + src_email = flask.current_app.config['REGISTRATION_SRC_EMAIL'] + dest_email = flask.current_app.config['REGISTRATION_DEST_PDL_EMAIL'] + + mail = Mail(flask.current_app) + msg = Message(f"AFC CertId download failure", + sender=src_email, + recipients=[dest_email]) + msg.body = f'''Fail to download CertId for country: {country} info {other}''' + mail.send(msg) + + def __call__(self, flaskapp, country): + als.als_json_log('cert_db', {'action': 'sweep', 'country': country, + 'status': 'starting'}) + if country == "US": + # first sweep SD (outdoor) entries, then sweep ID list. + self.sweep_fcc_sd(flaskapp) + self.sweep_fcc_id(flaskapp) + else: + self.sweep_canada(flaskapp) + + +class Organization(Manager): + '''View and manage Organizations ''' + + def __init__(self, *args, **kwargs): + Manager.__init__(self, *args, **kwargs) + self.add_command("create", OrganizationCreate()) + self.add_command("remove", OrganizationRemove()) + self.add_command("list", OrganizationList()) + + +class OrganizationCreate(Command): + ''' Create a new Organization. ''' + + option_list = ( + Option('--name', type=str, default=None, + help='Name of Organization', + required=True), + ) + + def _create_org(self, flaskapp, name): + from contextlib import closing + import datetime + from afcmodels.aaa import Organization + LOGGER.debug('OrganizationCreate._create_org() %s', name) + with flaskapp.app_context(): + if name is None: + raise RuntimeError('Name required') + + org = Organization.query.filter(Organization. + name == name).first() + if org: + raise RuntimeError('Duplicate org detected') + + organization = Organization(name=name) + db.session.add(organization) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + + def __init__(self, flaskapp=None, name=None): + if flaskapp: + self._create_org(flaskapp, name) + + def __call__(self, flaskapp, name=None): + self._create_org(flaskapp, name) + + +class OrganizationRemove(Command): + '''Removes an access point by serial number and or certification id''' + option_list = ( + Option('--name', type=str, default=None, + help='Name of Organization', + required=True), + ) + + def _remove_org(self, flaskapp, name): + from afcmodels.aaa import Organization + LOGGER.debug('Organization._remove_org() %s', name) + with flaskapp.app_context(): + try: + # select * from access_point as ap where ap.serial_number = ? + org = Organization.query.filter( + Organization.name == name).one() + if not org: + raise RuntimeError('No organization found') + except BaseException: + raise RuntimeError('No organization found') + + for ap in org.aps: + db.session.delete(ap) + + db.session.delete(org) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + + def __init__(self, flaskapp=None, name=None): + if flaskapp: + self._remove_org(flaskapp, name) + + def __call__(self, flaskapp, name=None): + self._remove_org(flaskapp, name) + + +class OrganizationList(Command): + '''Lists all access points''' + + def __call__(self, flaskapp): + table = PrettyTable() + from afcmodels.aaa import Organization + table.field_names = ["Name"] + with flaskapp.app_context(): + for org in db.session.query( + Organization).all(): # pylint: disable=no-member + table.add_row([org.name]) + print(table) + + +class MTLSCreate(Command): + ''' Create a new mtls certificate. ''' + + option_list = ( + Option('--note', type=str, default=None, + help="note"), + Option('--src', type=str, default=None, + help="certificate file"), + Option('--org', type=str, default="", + help="email of user assocated with this access point.") + ) + + def _create_mtls(self, flaskapp, note="", org="", src=None): + from contextlib import closing + import datetime + from afcmodels.aaa import MTLS, User + LOGGER.debug('MTLS._create_mtls() %s %s %s', + note, org, src) + if not src: + raise RuntimeError('certificate data file required') + + cert = src + with flaskapp.app_context(): + cert_data = "" + with open(cert, 'r') as fp_cert: + while True: + dataline = fp_cert.readline() + if not dataline: + break + cert_data = cert_data + dataline + mtls = MTLS(cert_data, note, org) + db.session.add(mtls) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + + def __init__(self, flaskapp=None, note="", + org="", src=None): + if flaskapp and cert: + self._create_mtls(flaskapp, note, org, src) + + def __call__(self, flaskapp, note, org, src): + self._create_mtls(flaskapp, note, org, src) + + +class MTLSRemove(Command): + ''' Remove MTLS certificate by id. ''' + + option_list = ( + Option('--id', type=int, default=None, + help="id"), + ) + + def _remove_mtls(self, flaskapp, id=None): + from contextlib import closing + import datetime + from afcmodels.aaa import MTLS, User + LOGGER.debug('MTLS._remove_mtls() %d', id) + + if not id: + raise RuntimeError('mtls id required') + + with flaskapp.app_context(): + try: + mtls = MTLS.query.filter(MTLS.id == id).one() + except sqlalchemy.orm.exc.NoResultFound: + raise RuntimeError( + 'No mtls certificate found with id"{0}"'.format(id)) + db.session.delete(mtls) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + + def __init__(self, flaskapp=None, id=None): + if flaskapp and id: + self._remove_mtls(flaskapp, id) + + def __call__(self, flaskapp, id): + self._remove_mtls(flaskapp, id) + + +class MTLSList(Command): + '''Lists all mtls certificates''' + + def __call__(self, flaskapp): + table = PrettyTable() + from afcmodels.aaa import MTLS + + table.field_names = ["ID", "Note", "Org", "Create"] + with flaskapp.app_context(): + for mtls in db.session.query( + MTLS).all(): # pylint: disable=no-member + org = mtls.org if mtls.org else "" + note = mtls.note if mtls.note else "" + table.add_row( + [mtls.id, mtls.note, mtls.org, str(mtls.created)]) + print(table) + + +class MTLSDump(Command): + ''' Remove MTLS certificate by id. ''' + + option_list = ( + Option('--id', type=int, default=None, + help="id"), + Option('--dst', type=str, default=None, + help="output file"), + ) + + def _dump_mtls(self, flaskapp, id=None, dst=None): + from contextlib import closing + import datetime + from afcmodels.aaa import MTLS, User + LOGGER.debug('MTLS._remove_mtls() %d', id) + if not id: + raise RuntimeError('mtls id required') + if not dst: + raise RuntimeError('output filename required') + filename = dst + + with flaskapp.app_context(): + try: + mtls = MTLS.query.filter(MTLS.id == id).one() + except sqlalchemy.orm.exc.NoResultFound: + raise RuntimeError( + 'No mtls certificate found with id"{0}"'.format(id)) + + with open(filename, 'w') as fpout: + fpout.write("%s" % (mtls.cert)) + + def __init__(self, flaskapp=None, id=None, dst=None): + if flaskapp and id and filename: + self._dump_mtls(flaskapp, id, filename) + + def __call__(self, flaskapp, id, dst): + self._dump_mtls(flaskapp, id, dst) + + +class MTLS(Manager): + '''View and manage Access Points''' + + def __init__(self, *args, **kwargs): + Manager.__init__(self, *args, **kwargs) + self.add_command("create", MTLSCreate()) + self.add_command("remove", MTLSRemove()) + self.add_command("list", MTLSList()) + self.add_command("dump", MTLSDump()) + + +class CeleryStatus(Command): # pylint: disable=abstract-method + ''' Get status of celery workers ''' + + def __call__(self, flaskapp): # pylint: signature-differs + import subprocess + subprocess.call(['celery', 'status']) + + +class TestCelery(Command): # pylint: disable=abstract-method + ''' Run celery task in isolation ''' + + option_list = ( + Option('request_type', type=str, + choices=['PointAnalysis', + 'ExclusionZoneAnalysis', 'HeatmapAnalysis'], + help='analysis type'), + Option('request_file', type=str, + help='request input file path'), + Option('afc_config', type=str, help="path to afc config file"), + Option('--response-file', default=None, + help='destination for json results'), + Option('--afc-engine', default=None), + Option('--user-id', type=int, default=1), + Option('--username', default='test_user'), + Option('--response-dir', default=None), + Option('--temp-dir', default='./test-celery-tmp'), + Option('--history-dir', default=None), + Option('--debug', action='store_true', dest='debug'), + ) + + def __call__( + self, + flaskapp, + request_type, + request_file, + afc_config, + response_file, + user_id, + username, + response_dir, + temp_dir, + history_dir): + with flaskapp.app_context(): + from afc_worker import run + import flask + + if not os.path.exists(temp_dir): + os.makedirs(temp_dir) + + if response_file is None: + response_file = os.path.join( + temp_dir, request_type + '_response.json') + if response_dir is None: + response_dir = os.path.join( + flask.current_app.config['NFS_MOUNT_PATH'], 'responses') + if history_dir is None: + history_dir = flask.current_app.config['HISTORY_DIR'] + + LOGGER.info('Submitting task...') + task = run.apply_async(args=[ + user_id, + username, + flask.current_app.config['STATE_ROOT_PATH'], + temp_dir, + request_type, + request_file, + afc_config, + response_file, + history_dir, + flask.current_app.config['NFS_MOUNT_PATH'], + ]) + + if task.state == 'FAILURE': + raise Exception('Task was unable to be started') + + # wait for task to complete and get result + task.wait() + + if task.failed(): + raise Exception('Task excecution failed') + + if task.successful() and task.result['status'] == 'DONE': + if not os.path.exists(task.result['result_path']): + raise Exception('Resource already deleted') + # read temporary file generated by afc-engine + LOGGER.debug("Reading result file: %s", + task.result['result_path']) + LOGGER.info('SUCCESS: result file located at "%s"', + task.result['result_path']) + + elif task.successful() and task.result['status'] == 'ERROR': + if not os.path.exists(task.result['error_path']): + raise Exception('Resource already deleted') + # read temporary file generated by afc-engine + LOGGER.debug("Reading error file: %s", + task.result['error_path']) + LOGGER.error('AFC ENGINE ERROR: error file located at "%s"', + task.result['error_path']) + else: + raise Exception('Invalid task state') + + +class Celery(Manager): + ''' Celery commands ''' + + def __init__(self, *args, **kwargs): + Manager.__init__(self, *args, **kwargs) + self.add_command('status', CeleryStatus()) + self.add_command('test', TestCelery()) + + +class ConfigAdd(Command): + ''' Create a new admin configuration. ''' + option_list = ( + Option('src', type=str, help='configuration source file'), + ) + + def __init__(self, *args, **kwargs): + LOGGER.debug('ConfigAdd.__init__()') + + def __call__(self, flaskapp, src): + LOGGER.debug('ConfigAdd.__call__() %s', src) + from afcmodels.aaa import AFCConfig, CertId, User + from .views.ratapi import regionStrToRulesetId + import datetime + + split_items = src.split('=', 1) + filename = split_items[1].strip() + if not os.path.exists(filename): + raise RuntimeError( + '"{}" source file does not exist'.format(filename)) + + rollback = [] + LOGGER.debug('Open admin cfg src file - %s', filename) + with open(filename, 'r') as fp_src: + while True: + dataline = fp_src.readline() + if not dataline: + break + # add user, APs and server configuration + new_rcrd = json.loads(dataline) + user_rcrd = json_lookup('userConfig', new_rcrd, None) + username = json_lookup('username', user_rcrd, None) + try: + UserCreate(flaskapp, user_rcrd[0], False) + f_str = "UserRemove(flaskapp, '" + username[0] + "')" + rollback.insert(0, f_str) + except RuntimeError: + LOGGER.debug('User %s already exists', username[0]) + + try: + ap_rcrd = json_lookup('apConfig', new_rcrd, None) + serial_id = json_lookup('serialNumber', ap_rcrd, None) + location = json_lookup('location', ap_rcrd, None) + cert_id = json_lookup('certificationId', ap_rcrd, None) + for i in range(len(serial_id)): + cert_id_str = cert_id[i][0]['id'] + ruleset_id_str = cert_id[i][0]['rulesetId'] + loc = int(location[i]) + + email = username[0] + if '@' in email: + org = email[email.index('@') + 1:] + else: + org = "" + + try: + OrganizationCreate(flaskapp, org) + f_str = "OrganizationRemove(flaskapp, '" + \ + org + "')" + rollback.insert(0, f_str) + except BaseException: + pass + + try: + # Since this is test, we mark these as both indoor + # and indoor certified + CertIdCreate(flaskapp, cert_id_str, + ruleset_id_str, loc) + f_str = "CertIdRemove(flaskapp, '" + \ + cert_id_str + "')" + rollback.insert(0, f_str) + except BaseException: + LOGGER.debug( + 'CertId %s already exists', cert_id_str) + + with flaskapp.app_context(): + user = User.query.filter( + User.email == username[0]).one() + LOGGER.debug('New user id %d', user.id) + + cfg_rcrd = json_lookup('afcConfig', new_rcrd, None) + region_rcrd = json_lookup('regionStr', cfg_rcrd, None) + # validate the region string + regionStrToRulesetId(region_rcrd[0]) + + config = AFCConfig.query.filter( + AFCConfig.config['regionStr'].astext == region_rcrd[0]).first() + if not config: + config = AFCConfig(cfg_rcrd[0]) + config.config['regionStr'] = config.config['regionStr'].upper( + ) + db.session.add(config) + else: + config.config = cfg_rcrd[0] + config.created = datetime.datetime.now() + db.session.commit() + + except Exception as e: + LOGGER.error(e) + LOGGER.error('Rolling back...') + for f in rollback: + eval(f) + + +class ConfigRemove(Command): + ''' Remove a user by email. ''' + option_list = ( + Option('src', type=str, help='configuration source file'), + ) + + def __init__(self, *args, **kwargs): + LOGGER.debug('ConfigRemove.__init__()') + + def __call__(self, flaskapp, src): + LOGGER.debug('ConfigRemove.__call__() %s', src) + from afcmodels.aaa import User + + split_items = src.split('=', 1) + filename = split_items[1].strip() + if not os.path.exists(filename): + raise RuntimeError( + '"{}" source file does not exist'.format(filename)) + + LOGGER.debug('Open admin cfg src file - %s', filename) + with open(filename, 'r') as fp_src: + while True: + dataline = fp_src.readline() + if not dataline: + break + new_rcrd = json.loads(dataline) + + ap_rcrd = json_lookup('apConfig', new_rcrd, None) + user_rcrd = json_lookup('userConfig', new_rcrd, None) + username = json_lookup('username', user_rcrd, None) + with flaskapp.app_context(): + try: + user = User.query.filter( + User.email == username[0]).one() + LOGGER.debug('Found user id %d', user.id) + UserRemove(flaskapp, username[0]) + + except RuntimeError: + LOGGER.debug('Delete missing user %s', username[0]) + except Exception as e: + LOGGER.debug('Missing user %s in DB', username[0]) + + +class ConfigShow(Command): + '''Show all configurations.''' + option_list = ( + Option('src', type=str, help="user's source file"), + ) + + def __init__(self, *args, **kwargs): + LOGGER.debug('ConfigShow.__init__()') + + def __call__(self, flaskapp, src): + LOGGER.debug('ConfigShow.__call__()') + split_items = src.split('=', 1) + filename = split_items[1].strip() + if not os.path.exists(filename): + raise RuntimeError( + '"{}" source file does not exist'.format(filename)) + + LOGGER.debug('Open admin cfg src file - %s', filename) + with open(filename, 'r') as fp_src: + while True: + dataline = fp_src.readline() + if not dataline: + break + new_rcrd = json.loads(dataline) + user_rcrd = json_lookup('userConfig', new_rcrd, None) + LOGGER.info('\nRecord ...\n\tuserConfig\n') + LOGGER.info(user_rcrd) + + ap_rcrd = json_lookup('apConfig', new_rcrd, None) + LOGGER.info('\n\tapConfig\n') + for i in range(len(ap_rcrd[0])): + LOGGER.info(ap_rcrd[0][i]) + + cfg_rcrd = json_lookup('afcConfig', new_rcrd, None) + LOGGER.info('\n\tafcConfig\n') + LOGGER.info(cfg_rcrd) + + +class Config(Manager): + ''' View and manage configuration records ''' + + def __init__(self, *args, **kwargs): + LOGGER.debug('Config.__init__()') + Manager.__init__(self, *args, **kwargs) + self.add_command('add', ConfigAdd()) + self.add_command('del', ConfigRemove()) + self.add_command('list', ConfigShow()) + + +def main(): + + def appfact(log_level): + ''' Construct an application based on command parameters. ''' + # Override some parameters for console use + log_level = log_level.upper() + conf = dict( + DEBUG=(log_level == 'DEBUG'), + PROPAGATE_EXCEPTIONS=(log_level == 'DEBUG'), + # converts str log_level to int value + LOG_LEVEL=logging.getLevelName(log_level), + LOG_HANDLERS=[logging.StreamHandler()] + ) + return create_app(config_override=conf) + + version_name = cmd_utils.packageversion(__package__) + + manager = Manager(appfact) + + if version_name is not None: + dispver = '%(prog)s {0}'.format(version_name) + manager.add_option('--version', action='version', + version=dispver) + manager.add_option('--log-level', dest='log_level', default='info', + help='Console logging lowest level displayed.') + manager.add_command('showurls', commands.ShowUrls()) + manager.add_command('db', MigrateCommand) + manager.add_command('db-create', DbCreate()) + manager.add_command('db-drop', DbDrop()) + manager.add_command('db-export', DbExport()) + manager.add_command('db-import', DbImport()) + manager.add_command('db-upgrade', DbUpgrade()) + manager.add_command('user', User()) + manager.add_command('mtls', MTLS()) + manager.add_command('data', Data()) + manager.add_command('celery', Celery()) + manager.add_command('ap-deny', AccessPointsDeny()) + manager.add_command('org', Organization()) + manager.add_command('cert_id', CertificationId()) + manager.add_command('cfg', Config()) + + try: + manager.run() + finally: + als.als_flush() + + +if __name__ == '__main__': + sys.exit(main()) + +# Local Variables: +# mode: Python +# indent-tabs-mode: nil +# python-indent: 4 +# End: +# +# vim: sw=4:et:tw=80:cc=+1 diff --git a/src/ratapi/ratapi/migrations/README b/src/ratapi/ratapi/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/src/ratapi/ratapi/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/src/ratapi/ratapi/migrations/alembic.ini b/src/ratapi/ratapi/migrations/alembic.ini new file mode 100644 index 0000000..f8ed480 --- /dev/null +++ b/src/ratapi/ratapi/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/src/ratapi/ratapi/migrations/env.py b/src/ratapi/ratapi/migrations/env.py new file mode 100644 index 0000000..8519d32 --- /dev/null +++ b/src/ratapi/ratapi/migrations/env.py @@ -0,0 +1,89 @@ +# pylint: disable=no-member +from __future__ import with_statement +from flask import current_app +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig +import logging + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', + current_app.config.get('SQLALCHEMY_DATABASE_URI')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.readthedocs.org/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + engine = engine_from_config(config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + connection = engine.connect() + context.configure(connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args) + + try: + with context.begin_transaction(): + context.run_migrations() + finally: + connection.close() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/src/ratapi/ratapi/migrations/script.py.mako b/src/ratapi/ratapi/migrations/script.py.mako new file mode 100644 index 0000000..9570201 --- /dev/null +++ b/src/ratapi/ratapi/migrations/script.py.mako @@ -0,0 +1,22 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision} +Create Date: ${create_date} + +""" + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/src/ratapi/ratapi/migrations/versions/0574ac12b90c_multiple_limits.py b/src/ratapi/ratapi/migrations/versions/0574ac12b90c_multiple_limits.py new file mode 100644 index 0000000..682b3a1 --- /dev/null +++ b/src/ratapi/ratapi/migrations/versions/0574ac12b90c_multiple_limits.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 0574ac12b90c +Revises: 878ecf2c3467 +Create Date: 2023-11-08 16:52:43.406880 + +""" + +# revision identifiers, used by Alembic. +revision = '0574ac12b90c' +down_revision = '878ecf2c3467' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('limits', sa.Column('limit', sa.Numeric(precision=50), nullable=True)) + op.add_column('limits', sa.Column('name', sa.String(length=64), nullable=True)) + op.drop_constraint('limits_min_eirp_key', 'limits', type_='unique') + op.create_unique_constraint('limits_name_unique', 'limits', ['name']) + op.drop_column('limits', 'min_eirp') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('limits', sa.Column('min_eirp', sa.NUMERIC(precision=50, scale=0), autoincrement=False, nullable=True)) + op.drop_constraint('limits_name_unique', 'limits', type_='unique') + op.create_unique_constraint('limits_min_eirp_key', 'limits', ['min_eirp']) + op.drop_column('limits', 'name') + op.drop_column('limits', 'limit') + # ### end Alembic commands ### diff --git a/src/ratapi/ratapi/migrations/versions/0b9577323e85_ap_deny.py b/src/ratapi/ratapi/migrations/versions/0b9577323e85_ap_deny.py new file mode 100644 index 0000000..1c9605a --- /dev/null +++ b/src/ratapi/ratapi/migrations/versions/0b9577323e85_ap_deny.py @@ -0,0 +1,51 @@ +"""empty message + +Revision ID: 0b9577323e85 +Revises: a0977ae9d7f7 +Create Date: 2023-04-14 20:10:00.561731 + +""" + +# revision identifiers, used by Alembic. +revision = '0b9577323e85' +down_revision = 'a0977ae9d7f7' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('aaa_org', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('aaa_ruleset', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('access_point_deny', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('serial_number', sa.String(length=64), nullable=True), + sa.Column('certification_id', sa.String(length=64), nullable=True), + sa.Column('org_id', sa.Integer(), nullable=True), + sa.Column('ruleset_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['org_id'], ['aaa_org.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['ruleset_id'], ['aaa_ruleset.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_access_point_deny_serial_number'), 'access_point_deny', ['serial_number'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_access_point_deny_serial_number'), table_name='access_point_deny') + op.drop_table('access_point_deny') + op.drop_table('aaa_org') + op.drop_table('aaa_ruleset') + # ### end Alembic commands ### diff --git a/src/ratapi/ratapi/migrations/versions/20bcbbdc61_ap_org.py b/src/ratapi/ratapi/migrations/versions/20bcbbdc61_ap_org.py new file mode 100644 index 0000000..1d64fde --- /dev/null +++ b/src/ratapi/ratapi/migrations/versions/20bcbbdc61_ap_org.py @@ -0,0 +1,46 @@ +"""empty message + +Revision ID: 20bcbbdc61 +Revises: 4435e833fee6 +Create Date: 2022-11-08 23:12:44.204351 + +""" + +# revision identifiers, used by Alembic. +revision = '20bcbbdc61' +down_revision = '4435e833fee6' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('access_point', sa.Column('org', sa.String(length=64), nullable=True)) + connection = op.get_bind() + connection.execute( + "ALTER TABLE access_point DROP CONSTRAINT access_point_user_id_fkey") + for ap in connection.execute('select id, user_id from access_point'): + if ap[0]: + if ap[1]: + result = connection.execute('select org from aaa_user where id = %d limit 1' %ap[1]) + for org in result: + connection.execute( + "update access_point set org = '%s' where id = %d" % (org[0], ap[0])) + break + else: + connection.execute( + "update access_point set org = '' where id = %d" % (ap[0])) + else: + break + + op.drop_column('access_point', 'user_id') + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('access_point', sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.create_foreign_key(u'access_point_user_id_fkey', 'access_point', 'aaa_user', ['user_id'], ['id'], ondelete=u'CASCADE') + op.drop_column('access_point', 'org') + ### end Alembic commands ### diff --git a/src/ratapi/ratapi/migrations/versions/230b7680b81e_oidc.py b/src/ratapi/ratapi/migrations/versions/230b7680b81e_oidc.py new file mode 100644 index 0000000..5b588c6 --- /dev/null +++ b/src/ratapi/ratapi/migrations/versions/230b7680b81e_oidc.py @@ -0,0 +1,43 @@ +"""oidc + +Revision ID: 230b7680b81e +Revises: 4c904e86218d +Create Date: 2022-10-19 12:34:33.521092 + +""" + +# revision identifiers, used by Alembic. +revision = '230b7680b81e' +down_revision = '4c904e86218d' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('blacklist_tokens') + op.add_column('aaa_user', sa.Column('sub', sa.String(length=255), nullable=True)) + op.add_column('aaa_user', sa.Column('username', sa.String(length=50), nullable=True)) + connection = op.get_bind() + for user in connection.execute('select id, email from aaa_user'): + connection.execute( + "update aaa_user set username = '%s' where id = %d" % (user[1], user[0])) + connection.execute( + "alter table aaa_user alter column username set not null") + op.create_unique_constraint(None, 'aaa_user', ['username']) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + # op.drop_constraint(None, 'aaa_user', type_='unique') + op.drop_column('aaa_user', 'username') + op.drop_column('aaa_user', 'sub') + op.create_table('blacklist_tokens', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('token', sa.VARCHAR(length=500), autoincrement=False, nullable=False), + sa.Column('blacklisted_on', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name=u'blacklist_tokens_pkey') + ) + ### end Alembic commands ### diff --git a/src/ratapi/ratapi/migrations/versions/31b2eb54b29_user_session.py b/src/ratapi/ratapi/migrations/versions/31b2eb54b29_user_session.py new file mode 100644 index 0000000..ebbf27a --- /dev/null +++ b/src/ratapi/ratapi/migrations/versions/31b2eb54b29_user_session.py @@ -0,0 +1,45 @@ +"""user session + +Revision ID: 31b2eb54b29 +Revises: 38cf654d18c2 +Create Date: 2019-10-18 14:39:49.437802 + +""" + +# revision identifiers, used by Alembic. +revision = '31b2eb54b29' +down_revision = '38cf654d18c2' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('blacklist_tokens', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('token', sa.String(length=500), nullable=False), + sa.Column('blacklisted_on', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('token') + ) + op.create_table('access_point', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('serial_number', sa.String(length=64), nullable=False), + sa.Column('model', sa.String(length=64), nullable=True), + sa.Column('manufacturer', sa.String(length=64), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['aaa_user.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('serial_number') + ) + op.create_index(op.f('ix_access_point_serial_number'), 'access_point', ['serial_number'], unique=False) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_access_point_serial_number'), table_name='access_point') + op.drop_table('access_point') + op.drop_table('blacklist_tokens') + ### end Alembic commands ### diff --git a/src/ratapi/ratapi/migrations/versions/38cf654d18c2_initial_aaa.py b/src/ratapi/ratapi/migrations/versions/38cf654d18c2_initial_aaa.py new file mode 100644 index 0000000..c094da0 --- /dev/null +++ b/src/ratapi/ratapi/migrations/versions/38cf654d18c2_initial_aaa.py @@ -0,0 +1,53 @@ +"""Initial AAA + +Revision ID: 38cf654d18c2 +Revises: None +Create Date: 2019-10-04 12:34:33.521092 + +""" + +# revision identifiers, used by Alembic. +revision = '38cf654d18c2' +down_revision = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('aaa_user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('email_confirmed_at', sa.DateTime(), nullable=True), + sa.Column('password', sa.String(length=255), nullable=False), + sa.Column('active', sa.Boolean(), nullable=True), + sa.Column('first_name', sa.String(length=50), nullable=True), + sa.Column('last_name', sa.String(length=50), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email') + ) + op.create_table('aaa_role', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('aaa_user_role', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('role_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['role_id'], ['aaa_role.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['aaa_user.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'role_id') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('aaa_user_role') + op.drop_table('aaa_role') + op.drop_table('aaa_user') + ### end Alembic commands ### diff --git a/src/ratapi/ratapi/migrations/versions/41f24c9fbec8_afc_config.py b/src/ratapi/ratapi/migrations/versions/41f24c9fbec8_afc_config.py new file mode 100644 index 0000000..b27c664 --- /dev/null +++ b/src/ratapi/ratapi/migrations/versions/41f24c9fbec8_afc_config.py @@ -0,0 +1,31 @@ +"""empty message + +Revision ID: 41f24c9fbec8 +Revises: 20bcbbdc61 +Create Date: 2022-12-09 00:14:33.533463 + +""" + +# revision identifiers, used by Alembic. +revision = '41f24c9fbec8' +down_revision = '20bcbbdc61' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('AFCConfig', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('config', postgresql.JSON(), nullable=True), + sa.Column('created', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('AFCConfig') + ### end Alembic commands ### diff --git a/src/ratapi/ratapi/migrations/versions/4435e833fee6_mtls.py b/src/ratapi/ratapi/migrations/versions/4435e833fee6_mtls.py new file mode 100644 index 0000000..ccd8eb9 --- /dev/null +++ b/src/ratapi/ratapi/migrations/versions/4435e833fee6_mtls.py @@ -0,0 +1,45 @@ +"""Add MTLS + +Revision ID: 4435e833fee6 +Revises: 230b7680b81e +Create Date: 2022-10-21 21:10:05.341302 + +""" + +# revision identifiers, used by Alembic. +revision = '4435e833fee6' +down_revision = '230b7680b81e' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('MTLS', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('cert', sa.String(length=32768), nullable=False), + sa.Column('note', sa.String(length=128), nullable=True), + sa.Column('org', sa.String(length=64), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id'), + ) + + op.add_column('aaa_user', sa.Column('org', + sa.String(length=50), nullable=True)) + connection = op.get_bind() + for user in connection.execute('select id, email from aaa_user'): + if '@' in user[1]: + org = user[1][user[1].index('@') + 1:] + else: + org = "" + connection.execute( + "update aaa_user set org = '%s' where id = %d" % (org, user[0])) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column('aaa_user', 'org') + op.drop_table('MTLS') + ### end Alembic commands ### diff --git a/src/ratapi/ratapi/migrations/versions/4c904e86218d_add_certification_id.py b/src/ratapi/ratapi/migrations/versions/4c904e86218d_add_certification_id.py new file mode 100644 index 0000000..f70c5ac --- /dev/null +++ b/src/ratapi/ratapi/migrations/versions/4c904e86218d_add_certification_id.py @@ -0,0 +1,26 @@ +"""add certification id + +Revision ID: 4c904e86218d +Revises: 31b2eb54b29 +Create Date: 2020-09-09 14:51:54.624778 + +""" + +# revision identifiers, used by Alembic. +revision = '4c904e86218d' +down_revision = '31b2eb54b29' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('access_point', sa.Column('certification_id', sa.String(length=64), nullable=True)) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column('access_point', 'certification_id') + ### end Alembic commands ### diff --git a/src/ratapi/ratapi/migrations/versions/878ecf2c3467_drop_ap.py b/src/ratapi/ratapi/migrations/versions/878ecf2c3467_drop_ap.py new file mode 100644 index 0000000..0d0b2f1 --- /dev/null +++ b/src/ratapi/ratapi/migrations/versions/878ecf2c3467_drop_ap.py @@ -0,0 +1,37 @@ +"""empty message + +Revision ID: 878ecf2c3467 +Revises: c3d56c68042e +Create Date: 2023-05-30 21:37:53.543153 + +""" + +# revision identifiers, used by Alembic. +revision = '878ecf2c3467' +down_revision = 'c3d56c68042e' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('ix_access_point_serial_number', table_name='access_point') + op.drop_table('access_point') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('access_point', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('serial_number', sa.VARCHAR(length=64), autoincrement=False, nullable=False), + sa.Column('model', sa.VARCHAR(length=64), autoincrement=False, nullable=True), + sa.Column('manufacturer', sa.VARCHAR(length=64), autoincrement=False, nullable=True), + sa.Column('certification_id', sa.VARCHAR(length=64), autoincrement=False, nullable=True), + sa.Column('org', sa.VARCHAR(length=64), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name='access_point_pkey'), + sa.UniqueConstraint('serial_number', name='access_point_serial_number_key') + ) + op.create_index('ix_access_point_serial_number', 'access_point', ['serial_number'], unique=False) + # ### end Alembic commands ### diff --git a/src/ratapi/ratapi/migrations/versions/a0977ae9d7f7_backpopulate.py b/src/ratapi/ratapi/migrations/versions/a0977ae9d7f7_backpopulate.py new file mode 100644 index 0000000..c937820 --- /dev/null +++ b/src/ratapi/ratapi/migrations/versions/a0977ae9d7f7_backpopulate.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: a0977ae9d7f7 +Revises: 41f24c9fbec8 +Create Date: 2023-02-13 22:02:18.057538 + +""" + +# revision identifiers, used by Alembic. +revision = 'a0977ae9d7f7' +down_revision = '41f24c9fbec8' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('blacklist_tokens') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('blacklist_tokens', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('token', sa.VARCHAR(length=500), autoincrement=False, nullable=False), + sa.Column('blacklisted_on', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='blacklist_tokens_pkey'), + sa.UniqueConstraint('token', name='blacklist_tokens_token_key') + ) + # ### end Alembic commands ### diff --git a/src/ratapi/ratapi/migrations/versions/c3d56c68042e_cert_id.py b/src/ratapi/ratapi/migrations/versions/c3d56c68042e_cert_id.py new file mode 100644 index 0000000..c440fe4 --- /dev/null +++ b/src/ratapi/ratapi/migrations/versions/c3d56c68042e_cert_id.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: c3d56c68042e +Revises: 0b9577323e85 +Create Date: 2023-04-25 19:23:52.811135 + +""" + +# revision identifiers, used by Alembic. +revision = 'c3d56c68042e' +down_revision = '0b9577323e85' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('cert_id', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('certification_id', sa.String(length=64), nullable=False), + sa.Column('refreshed_at', sa.DateTime(), nullable=True), + sa.Column('ruleset_id', sa.Integer(), nullable=False), + sa.Column('location', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['ruleset_id'], ['aaa_ruleset.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('cert_id') + # ### end Alembic commands ### diff --git a/src/ratapi/ratapi/templates/about.html b/src/ratapi/ratapi/templates/about.html new file mode 100644 index 0000000..6dbb5ad --- /dev/null +++ b/src/ratapi/ratapi/templates/about.html @@ -0,0 +1,15 @@ + + + About + + + + +
+

Fill out the Request Access to the AFC Website form below

+
+ + diff --git a/src/ratapi/ratapi/templates/about_csrf.html b/src/ratapi/ratapi/templates/about_csrf.html new file mode 100644 index 0000000..bc4bfd8 --- /dev/null +++ b/src/ratapi/ratapi/templates/about_csrf.html @@ -0,0 +1,11 @@ + + + About + + + +
+ +
+ + diff --git a/src/ratapi/ratapi/templates/about_login.html b/src/ratapi/ratapi/templates/about_login.html new file mode 100644 index 0000000..5d2b964 --- /dev/null +++ b/src/ratapi/ratapi/templates/about_login.html @@ -0,0 +1,21 @@ + + + About + + + + +
+ {% set about = '\#about' %} +

Login here.

+

If you don't have an account, visit About.

+ diff --git a/src/ratapi/ratapi/templates/flask_user_layout.html b/src/ratapi/ratapi/templates/flask_user_layout.html new file mode 100644 index 0000000..2b18a97 --- /dev/null +++ b/src/ratapi/ratapi/templates/flask_user_layout.html @@ -0,0 +1,91 @@ + + + + + + + {{ user_manager.USER_APP_NAME }} + + + + + + + + + + + + {# *** Allow sub-templates to insert extra html to the head section *** #} + {% block extra_css %}{% endblock %} + + + + + {% block body %} +
+ +
+ {% if call_or_get(current_user.is_authenticated) %} + {{ current_user.username or current_user.email }} +   |   + {%trans%}Sign out{%endtrans%} + {% else %} + {%trans%}Sign in{%endtrans%} + {% endif %} +
+
+ {% block menu %} + + {% endblock %} +
+ +
+ {# One-time system messages called Flash messages #} + {% block flash_messages %} + {%- with messages = get_flashed_messages(with_categories=true) -%} + {% if messages %} + {% for category, message in messages %} + {% if category=='error' %} + {% set category='danger' %} + {% endif %} +
{{ message|safe }}
+ {% endfor %} + {% endif %} + {%- endwith %} + {% endblock %} + + {% block main %} + {% block content %}{% endblock %} + {% endblock %} +
+ +
+
+ + {% endblock %} + + + + + + + + {# *** Allow sub-templates to insert extra html to the bottom of the body *** #} + {% block extra_js %}{% endblock %} + + + diff --git a/src/ratapi/ratapi/templates/login.html b/src/ratapi/ratapi/templates/login.html new file mode 100644 index 0000000..eb7d74b --- /dev/null +++ b/src/ratapi/ratapi/templates/login.html @@ -0,0 +1,65 @@ +{% extends 'flask_user/_public_base.html' %} + +{% block content %} +{% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %} +

{%trans%}Sign in{%endtrans%}

+ +
+ {{ form.hidden_tag() }} + + {# Username or Email field #} + {% set field = form.username if user_manager.USER_ENABLE_USERNAME else form.email %} +
+ {# Label on left, "New here? Register." on right #} +
+
+ +
+
+ {% if user_manager.USER_ENABLE_REGISTER and not user_manager.USER_REQUIRE_INVITATION %} + + {%trans%}New here? Register.{%endtrans%} + {% endif %} +
+
+ {{ field(class_='form-control', tabindex=110) }} + {% if field.errors %} + {% for e in field.errors %} +

{{ e }}

+ {% endfor %} + {% endif %} +
+ + {# Password field #} + {% set field = form.password %} +
+ {# Label on left, "Forgot your Password?" on right #} +
+
+ +
+
+ {% if user_manager.USER_ENABLE_FORGOT_PASSWORD %} + + {%trans%}Forgot your Password?{%endtrans%} + {% endif %} +
+
+ {{ field(class_='form-control', tabindex=120, autocomplete="off") }} + {% if field.errors %} + {% for e in field.errors %} +

{{ e }}

+ {% endfor %} + {% endif %} +
+ + {# Remember me #} + {% if user_manager.USER_ENABLE_REMEMBER_ME %} + {{ render_checkbox_field(login_form.remember_me, tabindex=130) }} + {% endif %} + + {# Submit button #} + {{ render_submit_field(form.submit, tabindex=180) }} +
+ +{% endblock %} diff --git a/src/ratapi/ratapi/templates/register.html b/src/ratapi/ratapi/templates/register.html new file mode 100644 index 0000000..14b4f66 --- /dev/null +++ b/src/ratapi/ratapi/templates/register.html @@ -0,0 +1,46 @@ +{% extends 'flask_user/_public_base.html' %} + +{% block content %} +{% from "flask_user/_macros.html" import render_field, render_submit_field %} +

{%trans%}Register{%endtrans%}

+ +
+ {{ form.hidden_tag() }} + + {# Username or Email #} + {% set field = form.username if user_manager.USER_ENABLE_USERNAME else form.email %} +
+ {# Label on left, "Already registered? Sign in." on right #} +
+
+ +
+
+ {% if user_manager.USER_ENABLE_REGISTER %} + + {%trans%}Already registered? Sign in.{%endtrans%} + {% endif %} +
+
+ {{ field(class_='form-control', tabindex=210, autocomplete="off") }} + {% if field.errors %} + {% for e in field.errors %} +

{{ e }}

+ {% endfor %} + {% endif %} +
+ + {% if user_manager.USER_ENABLE_EMAIL and user_manager.USER_ENABLE_USERNAME %} + {{ render_field(form.email, tabindex=220, autocomplete="off") }} + {% endif %} + + {{ render_field(form.password, tabindex=230, autocomplete="off") }} + + {% if user_manager.USER_REQUIRE_RETYPE_PASSWORD %} + {{ render_field(form.retype_password, tabindex=240, autocomplete="off") }} + {% endif %} + + {{ render_submit_field(form.submit, tabindex=280) }} +
+ +{% endblock %} diff --git a/src/ratapi/ratapi/util.py b/src/ratapi/ratapi/util.py new file mode 100644 index 0000000..15deda0 --- /dev/null +++ b/src/ratapi/ratapi/util.py @@ -0,0 +1,180 @@ +''' Shared utility classes and functions for views. +''' + +import os +import logging +import flask +import tempfile +import shutil +import uuid +from celery import Celery +from werkzeug.exceptions import HTTPException +from werkzeug.http import HTTP_STATUS_CODES +from werkzeug._internal import _get_environ + +#: LOGGER for this module +LOGGER = logging.getLogger(__name__) + + +class Response(flask.Response): + ''' A derived Response object with adjusted default values. + ''' + #: HTTP 1.1 (RFC 7231) explicitly allows relative Location URIs + #: but Qt 5.9 does not accept relative URLs + autocorrect_location_header = True + + +class AFCEngineException(HTTPException): + ''' Custom exception class for AFC Engine. + Used when the AFC Engine encounters an internal error. + ''' + + code = 550 # custom error code for AFC Engine Exceptions + description = 'Your request was unable to be processed because of an unknown error.' + + def __init__(self, description=None, response=None, exit_code=None): + HTTPException.__init__(self) + if description is not None: + self.description = description + self.response = response + self.exit_code = exit_code + + @property + def name(self): + """The status name.""" + return HTTP_STATUS_CODES.get(self.code, 'AFC Engine Error') + + def get_description(self, environ=None): + """Get the description.""" + return self.description + + def get_body(self, environ=None): + """Get the HTML body.""" + return flask.json.dumps(dict( + type='AFC Engine Exception', + exitCode=self.exit_code, + description=self.description # , + # env=environ + )) + + def get_headers(self, environ=None): + """Get a list of headers.""" + return [('Content-Type', 'application/json')] + + def get_response(self, environ=None): + """Get a response object. If one was passed to the exception + it's returned directly. + + :param environ: the optional environ for the request. This + can be used to modify the response depending + on how the request looked like. + :return: a :class:`Response` object or a subclass thereof. + """ + if self.response is not None: + return self.response + if environ is not None: + environ = _get_environ(environ) + headers = self.get_headers(environ) + return Response(self.get_body(environ), self.code, headers) + + +class TemporaryDirectory(object): + """Context manager for temporary directories""" + + def __init__(self, prefix): + self.prefix = prefix + self.name = None + + def __enter__(self): + self.name = tempfile.mkdtemp(prefix=self.prefix) + return self.name + + def __exit__(self, exc_type, exc_val, trace): + shutil.rmtree(self.name) + + +def getQueueDirectory(task_queue_dir, analysis_type): + ''' creates a unique directory under the task_queue_dir + and returns the name to the caller. Caller responsible for cleanup. + ''' + + unique = str(uuid.uuid4()) + dir_name = analysis_type + '-' + unique + + full_path = os.path.join(task_queue_dir, dir_name) + os.mkdir(full_path) + return full_path + + +def redirect(location, code): + ''' Generate a response to redirect. + + :param location: The URI to redirect to. + :type location: unicode or str + :param code: The redirect status code. + :type code: int + :return: The response object. + :rtype: :py:cls:`Response` + ''' + resp = Response(status=code) + resp.location = location + return resp + + +def require_default_uls(): + ''' Copies all default ULS Database files to variable directory as symlinks. + These will then be accessible through web dav interface + ''' + # copy default uls database files to var + for uls_file in os.listdir(flask.current_app.config['DEFAULT_ULS_DIR']): + if os.path.exists( + os.path.join( + flask.current_app.config['NFS_MOUNT_PATH'], + 'rat_transfer', + 'ULS_Database', + uls_file)): + continue + os.symlink( + os.path.join( + flask.current_app.config['DEFAULT_ULS_DIR'], + uls_file), + os.path.join( + flask.current_app.config['NFS_MOUNT_PATH'], + 'rat_transfer', + 'ULS_Database', + uls_file)) + + +class PrefixMiddleware(object): + ''' Apply an application root path if necessary. + ''' + + def __init__(self, app, prefix=''): + self.app = app + self.prefix = prefix + + def __call__(self, environ, start_response): + + # we need to strip prefix from path info before flask can handle it + if environ['PATH_INFO'].startswith(self.prefix): + environ['PATH_INFO'] = environ['PATH_INFO'][len(self.prefix):] + LOGGER.debug('Path: %s', environ['PATH_INFO']) + environ['SCRIPT_NAME'] = self.prefix + LOGGER.debug('Script: %s', environ['SCRIPT_NAME']) + return self.app(environ, start_response) + else: + # here the prefix has already been stripped by apache so just set + # script and pass on + environ['SCRIPT_NAME'] = self.prefix + return self.app(environ, start_response) + + +class HeadersMiddleware(object): + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + def custom_start_response(status, headers, exc_info=None): + headers.append(('Cache-Control', "max-age=0")) + return start_response(status, headers, exc_info) + return self.app(environ, custom_start_response) diff --git a/src/ratapi/ratapi/views/__init__.py b/src/ratapi/ratapi/views/__init__.py new file mode 100644 index 0000000..72f946e --- /dev/null +++ b/src/ratapi/ratapi/views/__init__.py @@ -0,0 +1 @@ +from . import paws, ratapi, auth, admin, ratafc diff --git a/src/ratapi/ratapi/views/admin.py b/src/ratapi/ratapi/views/admin.py new file mode 100644 index 0000000..a5200a8 --- /dev/null +++ b/src/ratapi/ratapi/views/admin.py @@ -0,0 +1,770 @@ +# This Python file uses the following encoding: utf-8 +# +# Portions copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate +# affiliate that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy +# of which is included with this software program. +# + +import json +import logging +import os +import inspect +import contextlib +import shutil +import flask +import datetime +from flask.views import MethodView +from fst import DataIf +from ncli import MsgPublisher +from werkzeug import exceptions +from sqlalchemy.exc import IntegrityError +import werkzeug +import afcmodels.aaa as aaa +from .auth import auth +from afcmodels.base import db +from .ratapi import rulesetIdToRegionStr, rulesets + +#: Logger for this module +LOGGER = logging.getLogger(__name__) + +#: All views under this API blueprint +module = flask.Blueprint("admin", "admin") + +dbg_msg = "inspect.getframeinfo(inspect.currentframe().f_back)[2]" + + +class User(MethodView): + """Administration resources for managing users.""" + + methods = ["POST", "GET", "DELETE"] + + def get(self, user_id): + """Get User infor with specific user_id. If none is provided, + return list of all users. Query parameters used for params.""" + + id = auth(roles=["Admin"], is_user=(None if user_id == 0 else user_id)) + if user_id == 0: + # check if we limit to org or query all + cur_user = aaa.User.query.filter_by(id=id).first() + roles = [r.name for r in cur_user.roles] + + LOGGER.debug( + "USER got user: %s org %s roles %s", + cur_user.email, + cur_user.org, + str(cur_user.roles), + ) + if "Super" in roles: + users = aaa.User.query.all() + else: + org = cur_user.org if cur_user.org else "" + users = aaa.User.query.filter_by(org=org).all() + + return flask.jsonify( + users=[ + { + "id": u.id, + "email": u.email, + "org": u.org if u.org else "", + "firstName": u.first_name, + "lastName": u.last_name, + "active": u.active, + "roles": [r.name for r in u.roles], + } + for u in users + ] + ) + else: + user = aaa.User.query.filter_by(id=user_id).first() + if user is None: + raise exceptions.NotFound("User does not exist") + return flask.jsonify( + user={ + "id": user.id, + "email": user.email, + "org": user.org if user.org else "", + "firstName": user.first_name, + "lastName": user.last_name, + "active": user.active, + "roles": [r.name for r in user.roles], + } + ) + + def post(self, user_id): + """Update user information.""" + + content = flask.request.json + user = aaa.User.query.filter_by(id=user_id).first() + LOGGER.debug("got user: %s", user.email) + org = user.org if user.org else "" + + if "setProps" in content: + auth(roles=["Admin"], is_user=user_id) + # update all user props + user_props = content + user.email = user_props.get("email", user.email) + if "email" in user_props: + user.email_confirmed_at = datetime.datetime.now() + user.active = user_props.get("active", user.active) + if "password" in user_props: + if flask.current_app.config["OIDC_LOGIN"]: + # OIDC, password is never stored locally + # Still we hash it so that if we switch back + # to non OIDC, the hash still match, and can be logged in + from passlib.context import CryptContext + + password_crypt_context = CryptContext(["bcrypt"]) + pass_hash = password_crypt_context.encrypt(password_in) + else: + pass_hash = ( + flask.current_app.user_manager.password_manager.hash_password( + user_props["password"])) + user.password = pass_hash + db.session.commit() # pylint: disable=no-member + + elif "addRole" in content: + # just add a single role + role = content.get("addRole") + if role == "Super": + auth(roles=["Super"], org=org) + else: + auth(roles=["Admin"], org=org) + LOGGER.debug("Adding role: %s", role) + # check if user already has role + if role not in [r.name for r in user.roles]: + # add role + to_add_role = aaa.Role.query.filter_by(name=role).first() + user.roles.append(to_add_role) + # When adding Super role, add Admin role too + if role == "Super" and "Admin" not in [ + r.name for r in user.roles]: + to_add_role = aaa.Role.query.filter_by( + name="Admin").first() + user.roles.append(to_add_role) + db.session.commit() # pylint: disable=no-member + + elif "removeRole" in content: + # just remove a single role + role = content.get("removeRole") + if role == "Super": + # only super user can remove someone elses super role + auth(roles=["Super"]) + else: + auth(roles=["Admin"], org=org) + + LOGGER.debug("Removing role: %s", role) + # check if user has role + if role in [r.name for r in user.roles]: + # remove role + for r in user.roles: + if r.name == role: + link = aaa.UserRole.query.filter_by( + user_id=user.id, role_id=r.id + ).first() + db.session.delete(link) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + + else: + raise exceptions.BadRequest() + + return flask.make_response() + + def delete(self, user_id): + """Remove a user from the system.""" + + user = aaa.User.query.filter_by(id=user_id).first() + org = user.org if user.org else "" + auth(roles=["Admin"], org=org) + db.session.delete(user) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + + return flask.make_response() + + +class AccessPointDeny(MethodView): + """resources to manage access points""" + + methods = ["PUT", "GET", "DELETE", "POST"] + + def get(self, id): + """Get APs info with an org""" + id = auth(roles=["Admin", "AP"]) + + user = aaa.User.query.filter_by(id=id).first() + roles = [r.name for r in user.roles] + if "Super" in roles: + # Super user gets all access points + access_points = db.session.query(aaa.AccessPointDeny).all() + else: + # Admin only user gets all access points within own org + org = user.org if user.org else "" + organization = ( + db.session.query(aaa.Organization) + .filter(aaa.Organization.name == org) + .first() + ) + if organization: + access_points = organization.aps + + # translate ruleset.id to index into rulesets + rules = rulesets() + rule_map = {} + for idx, rule in enumerate(rules): + r = db.session.query(aaa.Ruleset).filter( + aaa.Ruleset.name == rule).first() + if r: + rule_map[r.id] = idx + + return flask.jsonify( + rulesets=rules, + access_points={ + "data": [ + "{},{},{},{}".format( + ap.serial_number, + ap.certification_id, + rule_map[ap.ruleset_id], + ap.org.name, + ) + for ap in access_points + ] + }, + ) + + def put(self, id): + """add an AP.""" + + content = flask.request.json + user = aaa.User.query.filter_by(id=id).first() + org = content.get("org") + auth(roles=["Admin"], org=org) + + serial = content.get("serialNumber") + if serial == "*" or not serial: + # match all + serial = None + + cert_id = content.get("certificationId") + if not cert_id: + raise exceptions.BadRequest("Certification Id required") + + rulesetId = content.get("rulesetId") + if not rulesetId: + raise exceptions.BadRequest("Ruleset Id required") + + ap = ( + aaa.AccessPointDeny.query.filter_by(certification_id=cert_id) + .filter_by(serial_number=serial) + .first() + ) + if ap: + # We detect existing entry + raise exceptions.BadRequest("Duplicate device detected") + + organization = aaa.Organization.query.filter_by(name=org).first() + if not organization: + raise exceptions.BadRequest("Organization does not exist") + + ruleset = aaa.Ruleset.query.filter_by(name=rulesetId).first() + if not ruleset: + raise exceptions.BadRequest("Ruleset does not exist") + + ap = aaa.AccessPointDeny(serial, cert_id) + organization.aps.append(ap) + ruleset.aps.append(ap) + db.session.add(ap) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + + return flask.jsonify(id=ap.id) + + def post(self, id): + """replace list of APs.""" + + import json + + content = flask.request.json + user = aaa.User.query.filter_by(id=id).first() + user_org = user.org + auth(roles=["Admin"]) + + # format as below. The organization (e.g. my_org) is optional, and if not listed will default + # to the org of the logged in user. + # e.g. + # {'rulesets':['US_47_CFR_PART_15_SUBPART_E', 'CA_RES_DBS-06'], + # 'access_points': { + # 'data': ['serial1, cert1, 0, my_org', + # 'serial2, cert2, 0'] + # } + # } + + roles = [r.name for r in user.roles] + if "Super" in roles: + # delete, replace whole list + aaa.AccessPointDeny.query.delete() + else: + # delete, replace list belonging to the admin's org + organization = aaa.Organization.query.filter_by( + name=user_org).first() + aaa.AccessPointDeny.query.filter_by( + org_id=organization.id).delete() + + payload = content.get("accessPoints") + rcrd = json.loads(payload) + ruleset_list = rcrd["rulesets"] + access_points = rcrd["access_points"]["data"] + rows = list(map(lambda a: a.strip("\r").split(","), access_points)) + + for row in rows: + if len(row) < 3: + raise exceptions.BadRequest( + "serial number, cert id, ruleset are required" + ) + + org = user_org + serial = row[0].strip() + if serial == "*" or serial == "None" or not serial: + # match all + serial = None + + cert_id = row[1].strip() + try: + ruleset_id = ruleset_list[int(row[2].strip())] + ruleset = aaa.Ruleset.query.filter_by(name=ruleset_id).first() + if not ruleset: + raise exceptions.BadRequest( + "ruleset {} does not exist".format(ruleset_id) + ) + except BaseException: + raise exceptions.BadRequest("ruleset does not exist") + + if len(row) > 3: # this column is to override the AP org if user is Super + if not "".join(row): # ignore empty row + continue + + org_override = row[3].strip().lower() + if "Super" in [r.name for r in user.roles]: + org = org_override + elif user_org != org_override: + # can't override org + raise exceptions.BadRequest( + "organization {} not accessible".format(org_override) + ) + ap = ( + aaa.AccessPointDeny.query.filter_by(certification_id=cert_id) + .filter_by(serial_number=serial) + .first() + ) + if not ap: + organization = aaa.Organization.query.filter_by( + name=org).first() + if not organization: + raise exceptions.BadRequest( + "organization {} does not exist".format(org) + ) + + ap = aaa.AccessPointDeny(serial, cert_id) + organization.aps.append(ap) + ruleset.aps.append(ap) + db.session.add(ap) # pylint: disable=no-member + else: + raise exceptions.BadRequest("duplicate entry") + + db.session.commit() # pylint: disable=no-member + return "Success", 200 + + def delete(self, id): + """Remove an AP from the system. Here the id is the AP id""" + + LOGGER.info("Deleting ap: %s", id) + ap = aaa.AccessPointDeny.query.filter_by(id=id).first() + if not ap: + raise exceptions.BadRequest("Bad AP") + + # check user roles + auth(roles=["Admin"], org=ap.org.name) + db.session.delete(ap) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + return flask.make_response() + + +class DeniedRegion(MethodView): + """resources to manage denied regions""" + + methods = ["PUT", "GET"] + + def _open(self, rel_path, mode, user=None): + """Open a configuration file. + + :param rel_path: The specific config name to open. + :param mode: The file open mode. + :return: The opened file. + :rtype: file-like + """ + + config_path = os.path.join( + flask.current_app.config["NFS_MOUNT_PATH"], + "rat_transfer", + "denied_regions") + if not os.path.exists(config_path): + os.makedirs(config_path) + + file_path = os.path.join(config_path, rel_path) + LOGGER.debug('Opening denied region file "%s"', file_path) + if not os.path.exists(file_path) and mode != "wb": + raise werkzeug.exceptions.NotFound() + + handle = open(file_path, mode) + + if mode == "wb": + os.chmod(file_path, 0o666) + + return handle + + def get(self, regionStr): + """GET method for denied regions""" + LOGGER.debug("getting denied regions") + filename = regionStr + "_denied_regions.csv" + + resp = flask.make_response() + with self._open(filename, "rb") as conf_file: + resp.data = conf_file.read() + resp.content_type = "text/csv" + return resp + + def put(self, regionStr): + """PUT method for denied regions""" + user_id = auth(roles=["Super"]) + LOGGER.debug("current user: %s", user_id) + filename = regionStr + "_denied_regions.csv" + + if flask.request.content_type != "text/csv": + raise werkzeug.exceptions.UnsupportedMediaType() + + with contextlib.closing(self._open(filename, "wb", user_id)) as outfile: + shutil.copyfileobj(flask.request.stream, outfile) + return flask.make_response("Denied regions updated", 204) + + +class CertId(MethodView): + """resources to manage access points""" + + methods = ["GET"] + + def get(self, id): + """Get Certification Id info with specific user_id.""" + id = auth(roles=["Super", "Admin"]) + user = aaa.User.query.filter_by(id=id).first() + roles = [r.name for r in user.roles] + # Super user gets all access points + cert_ids = db.session.query(aaa.CertId).all() + + return flask.jsonify( + certIds=[ + { + "id": cert.id, + "certificationId": cert.certification_id, + "rulesetId": cert.ruleset_id, + "org": ap.org, + } + for cert in cert_ids + ] + ) + + +class Limits(MethodView): + methods = ["PUT", "GET"] + + def put(self): + """set eirp limit""" + + content = flask.request.get_json() + auth(roles=["Super"]) + try: + LOGGER.error("content: %s ", content) + newIndoorEnforce = content.get('indoorEnforce') + newIndoorLimit = content.get('indoorLimit') + newOutdoorEnforce = content.get('outdoorEnforce') + newOutdoorLimit = content.get('outdoorLimit') + + indoorlimitRecord = aaa.Limit.query.filter_by(id=0).first() + outdoorlimitRecord = aaa.Limit.query.filter_by(id=1).first() + + if (indoorlimitRecord is None and outdoorlimitRecord is None): + # create new records + if (not newIndoorEnforce and not newOutdoorEnforce): + raise exceptions.BadRequest("No change") + limit0 = aaa.Limit(newIndoorLimit, newIndoorEnforce, False) + limit1 = aaa.Limit(newOutdoorLimit, newOutdoorEnforce, True) + db.session.add(limit0) + db.session.add(limit1) + elif (indoorlimitRecord is None and outdoorlimitRecord is not None): + limit0 = aaa.Limit(newIndoorLimit, newIndoorEnforce, False) + db.session.add(limit0) + outdoorlimitRecord.enforce = newOutdoorEnforce + outdoorlimitRecord.limit = newOutdoorLimit + elif (indoorlimitRecord is not None and outdoorlimitRecord is None): + limit1 = aaa.Limit(newOutdoorLimit, newOutdoorEnforce, True) + db.session.add(limit1) + indoorlimitRecord.enforce = newIndoorEnforce + indoorlimitRecord.limit = newIndoorLimit + else: + outdoorlimitRecord.enforce = newOutdoorEnforce + outdoorlimitRecord.limit = newOutdoorLimit + indoorlimitRecord.enforce = newIndoorEnforce + indoorlimitRecord.limit = newIndoorLimit + db.session.commit() + return flask.jsonify( + indoorLimit=float(newIndoorLimit), + outdoorLimit=float(newOutdoorLimit), + indoorEnforce=newIndoorEnforce, + outdoorEnforce=newOutdoorEnforce, + ) + except IntegrityError: + raise exceptions.BadRequest("DB Error") + + def get(self): + """get eirp limit""" + try: + # First get the indoor limit (id 0) + indoorlimits = aaa.Limit.query.filter_by(id=0).first() + # Then get the outdoor limit (id 1) + outdoorlimits = aaa.Limit.query.filter_by(id=1).first() + if indoorlimits or outdoorlimits: + return flask.jsonify( + indoorLimit=float(indoorlimits.limit), + outdoorLimit=float(outdoorlimits.limit), + indoorEnforce=indoorlimits.enforce, + outdoorEnforce=outdoorlimits.enforce, + ) + else: + return flask.make_response("Min Eirp not configured", 404) + + except IntegrityError: + raise exceptions.BadRequest("DB Error") + + +class AllowedFreqRanges(MethodView): + """Allows an admin to update the JSON containing the allowed + frequency bands and allow any user to view but not edit the file + """ + + methods = ["PUT", "GET"] + ACCEPTABLE_FILES = { + "allowed_frequencies.json": dict( + content_type="application/json", + ) + } + + def _open(self, rel_path, mode, user=None): + """Open a configuration file. + + :param rel_path: The specific config name to open. + :param mode: The file open mode. + :return: The opened file. + :rtype: file-like + """ + + config_path = os.path.join( + flask.current_app.config["NFS_MOUNT_PATH"], + "rat_transfer", + "frequency_bands", + ) + if not os.path.exists(config_path): + os.makedirs(config_path) + + file_path = os.path.join(config_path, rel_path) + LOGGER.debug('Opening frequncy file "%s"', file_path) + if not os.path.exists(file_path) and mode != "wb": + raise werkzeug.exceptions.NotFound() + + handle = open(file_path, mode) + + if mode == "wb": + os.chmod(file_path, 0o666) + + return handle + + def get(self): + """GET method for allowed frequency bands""" + LOGGER.debug("getting admin supplied frequncy bands") + filename = "allowed_frequencies.json" + if filename not in self.ACCEPTABLE_FILES: + LOGGER.debug("Could not find allowed_frequencies.json") + raise werkzeug.exceptions.NotFound() + filedesc = self.ACCEPTABLE_FILES[filename] + + resp = flask.make_response() + with self._open("allowed_frequencies.json", "rb") as conf_file: + resp.data = conf_file.read() + resp.content_type = filedesc["content_type"] + return resp + + def put(self, filename="allowed_frequencies.json"): + """PUT method for afc config""" + user_id = auth(roles=["Super"]) + LOGGER.debug("current user: %s", user_id) + if filename not in self.ACCEPTABLE_FILES: + raise werkzeug.exceptions.NotFound() + filedesc = self.ACCEPTABLE_FILES[filename] + if flask.request.content_type != filedesc["content_type"]: + raise werkzeug.exceptions.UnsupportedMediaType() + + with contextlib.closing(self._open(filename, "wb", user_id)) as outfile: + shutil.copyfileobj(flask.request.stream, outfile) + return flask.make_response("Allowed frequency ranges updated", 204) + + +class MTLS(MethodView): + """resources to manage mtls certificates""" + + methods = ["POST", "GET", "DELETE"] + + def _rebuild_cert_bundle(self) -> None: + LOGGER.debug(f"{type(self)}.{eval(dbg_msg)}() ") + bundle_data = "" + cmd = "cmd_restart" + for certs in db.session.query(aaa.MTLS).all(): + LOGGER.info(f"{certs.id}") + bundle_data += certs.cert + db.session.commit() # pylint: disable=no-member + LOGGER.debug( + f"{type(self)}.{eval(dbg_msg)}() {bundle_data} {len(bundle_data)}") + with DataIf().open("certificate/client.bundle.pem") as hfile: + if len(bundle_data) == 0: + hfile.delete() + cmd = "cmd_remove" + else: + hfile.write(bundle_data) + LOGGER.debug( + f"{type(self)}.{eval(dbg_msg)}() " + f"{flask.current_app.config['BROKER_URL']}" + ) + publisher = MsgPublisher( + flask.current_app.config["BROKER_URL"], + flask.current_app.config["BROKER_EXCH_DISPAT"], + ) + publisher.publish(cmd) + publisher.close() + + def get(self, id): + """Get MTLS info with specific user_id.""" + LOGGER.debug(f"{type(self)}.{eval(dbg_msg)}() ") + + if id == 0: + user_id = auth(roles=["Admin"]) + user = aaa.User.query.filter_by(id=user_id).first() + roles = [r.name for r in user.roles] + if "Super" in roles: + mtls_list = aaa.MTLS.query.all() + else: + # Admin user gets certificates for his/her own org + org = user.org if user.org else "" + mtls_list = db.session.query(aaa.MTLS).filter( + aaa.MTLS.org == org).all() + else: + raise werkzeug.exceptions.NotFound() + + return flask.jsonify( + mtls=[ + { + "id": mtls.id, + "cert": mtls.cert, + "note": mtls.note if mtls.note else "", + "org": mtls.org if mtls.org else "", + "created": str(mtls.created), + } + for mtls in mtls_list + ] + ) + + def post(self, id): + """Insert an mtls certificate by a user id to a database table, + fetch all certificates from the table and create a new + certificate bundle. Copy the bundle to a predefined place + and send command to correspondent clients. + """ + content = flask.request.json + org = content.get("org") + auth(roles=["Admin"], org=org) + LOGGER.debug( + f"{type(self)}.{eval(dbg_msg)}() " f"mtls: {str(id)} org: {org}") + + # check if certificate is already there. + cert = content.get("cert") + try: + import base64 + + strip_chars = "base64," + index = cert.index(strip_chars) + cert = base64.b64decode(cert[index + len(strip_chars):]) + cert = str(cert, "UTF-8").replace("\\n", "\n") + except BaseException: + LOGGER.error(f"PUT mtls: {str(id)} org: {org} exception") + raise exceptions.BadRequest("Unexpected certificate format") + + try: + mtls = aaa.MTLS(cert, content.get("note"), org) + db.session.add(mtls) + db.session.flush() # pylint: disable=no-member + except Exception as e: + LOGGER.error( + f"Failed to insert new cert into table " f"({type(e)} {e})") + raise exceptions.BadRequest("Failed to insert new cert into table") + + LOGGER.debug( + f"{type(self)}.{eval(dbg_msg)}() " + f"Added cert id: {str(mtls.id)} org: {org}" + ) + + try: + self._rebuild_cert_bundle() + except Exception as e: + LOGGER.error( + f"Failed to prepare new bundle with mtls: " + f"{str(mtls.id)}, ({type(e)} {e})" + ) + self.delete(mtls.id) + raise exceptions.BadRequest("Failed to prepare new bundle file") + + return flask.jsonify(id=mtls.id) + + def delete(self, id): + """Remove an mtls cert from the system. + Here the id is the mtls cert id instead of the user_id + """ + LOGGER.debug(f"{type(self)}.{eval(dbg_msg)}()") + + mtls = aaa.MTLS.query.filter_by(id=id).first() + user_id = auth(roles=["Admin"], org=mtls.org) + user = aaa.User.query.filter_by(id=user_id).first() + LOGGER.info("Deleting mtls: %s", str(mtls.id)) + + db.session.delete(mtls) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + + try: + self._rebuild_cert_bundle() + except Exception as e: + LOGGER.error( + f"Failed to prepare new bundle without mtls: " + f"{str(mtls.id)}, ({type(e)} {e})" + ) + raise exceptions.BadRequest("Failed to prepare new bundle file") + + return flask.make_response() + + +module.add_url_rule("/user/", view_func=User.as_view("User")) +module.add_url_rule("/user/ap_deny/", + view_func=AccessPointDeny.as_view("AccessPointDeny")) +module.add_url_rule("/user/cert/", view_func=CertId.as_view("CertId")) +module.add_url_rule("/user/eirp_min", view_func=Limits.as_view("Eirp")) +module.add_url_rule( + "/user/frequency_range", view_func=AllowedFreqRanges.as_view("Frequency") +) +module.add_url_rule("/user/mtls/", view_func=MTLS.as_view("MTLS")) +module.add_url_rule( + "/user/denied_regions/", + view_func=DeniedRegion.as_view("DeniedRegion"), +) diff --git a/src/ratapi/ratapi/views/auth.py b/src/ratapi/ratapi/views/auth.py new file mode 100644 index 0000000..49e2e20 --- /dev/null +++ b/src/ratapi/ratapi/views/auth.py @@ -0,0 +1,409 @@ +# This Python file uses the following encoding: utf-8 +# +# Portions copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate +# affiliate that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy +# of which is included with this software program. +# +from appcfg import OIDCConfigurator +import os +import logging +import datetime +import hashlib +import base64 +import secrets +import werkzeug +import flask +from flask.views import MethodView +import requests +from afcmodels.base import db +from afcmodels.aaa import User, Organization, Role +from flask_login import current_user +import als + +OIDC_LOGIN = OIDCConfigurator().OIDC_LOGIN + +if OIDC_LOGIN: + from flask_login import ( + login_user, + logout_user, + ) + +LOGGER = logging.getLogger(__name__) + +# using https://github.com/realpython/flask-jwt-auth +# LICENCE: MIT + +module = flask.Blueprint('auth', __name__) + +PY3 = False # using Python2 + + +def auth(ignore_active=False, roles=None, is_user=None, org=None): + ''' Provide application authentication for API methods. Returns the user_id if successful. + + :param ignore_active: if True, allows inactive users to access resource + + :param roles: array of strings which specify the roles that can + access the resource. User must have at least 1 of the roles + :type roles: str[] + + :param is_user: is a single user_id. checks if the user is identical. + Use for personal information. + + :returns: user Id + :rtype: int + ''' + if not current_user.is_authenticated: + raise werkzeug.exceptions.Unauthorized() + (user_id, active, user_roles, cur_org) = \ + (current_user.id, current_user.active, + [r.name for r in current_user.roles], + current_user.org if current_user.org else "") + + # Super always has admin roles + if "Super" in user_roles and "Admin" not in user_roles: + user_roles.append("Admin") + + if not isinstance(user_id, str): + if not active and not ignore_active: + raise werkzeug.exceptions.Forbidden("Inactive user") + if is_user: + if user_id == is_user: + LOGGER.debug("User id matches: %i", user_id) + elif "Super" in user_roles: + return is_user # return impersonating user. Don't check org + elif "Admin" not in user_roles: + raise werkzeug.exceptions.NotFound() + target_user = User.get(is_user) + target_org = target_user.org if target_user.org else "" + + else: + if roles: + found_role = False + for r in roles: + if r in user_roles: + LOGGER.debug( + "User %i is authenticated with role %s", user_id, r) + found_role = True + break + + if not found_role: + raise werkzeug.exceptions.Forbidden( + "You do not have access to this resource") + + is_user = user_id + target_org = cur_org if cur_org else "" + else: + raise werkzeug.exceptions.Unauthorized() + + # done checking roles/userid. now check org + if "Super" not in user_roles: + if (not cur_org == target_org) or (org and not cur_org == org): + raise werkzeug.exceptions.Forbidden( + "You do not have access to this resource") + + return is_user + + +class AboutLoginAPI(MethodView): + ''' Allow the web UI to manipulate configuration directly. + ''' + + def get(self): + ''' GET method for About + ''' + + resp = flask.make_response() + + resp.data = flask.render_template("about_login.html") + resp.content_type = 'text/html' + return resp + + +class LoginAPI(MethodView): + """ + User Login Resource + """ + + def get(self): + # store app state and code verifier in session + if not flask.current_app.config['OIDC_LOGIN']: + return flask.redirect(flask.url_for('user.login')) + + flask.session['app_state'] = secrets.token_urlsafe(64) + flask.session['code_verifier'] = secrets.token_urlsafe(64) + # calculate code challenge + hashed = hashlib.sha256( + flask.session['code_verifier'].encode('ascii')).digest() + encoded = base64.urlsafe_b64encode(hashed) + code_challenge = encoded.decode('ascii').strip('=') + redirect_uri = flask.request.base_url + redirect_uri = redirect_uri.replace("login", "callback") + fwd_proto = flask.request.headers.get('X-Forwarded-Proto') + if (fwd_proto == 'https') and (flask.request.scheme == "http"): + redirect_uri = redirect_uri.replace("http:", "https:") + + # get request params + query_params = { + 'client_id': flask.current_app.config['OIDC_CLIENT_ID'], + 'redirect_uri': redirect_uri, + 'scope': "openid email profile", + 'state': flask.session['app_state'], + 'code_challenge': code_challenge, + 'code_challenge_method': 'S256', + 'response_type': 'code', + 'response_mode': 'query'} + + # build request_uri + request_uri = "{base_url}?{query_params}".format( + base_url=flask.current_app.config['OIDC_ORG_AUTH_URL'], + query_params=requests.compat.urlencode(query_params) + ) + response = flask.redirect(request_uri) + return response + + +class CallbackAPI(MethodView): + """ + Callback Resource + """ + + def get(self): + if not flask.current_app.config['OIDC_LOGIN']: + return "Invalid Access", 403 + + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + code = flask.request.args.get("code") + app_state = flask.request.args.get("state") + + if app_state != flask.session['app_state']: + LOGGER.debug('user:%s login bad state', 'unknown') + als.als_json_log('user_access', + {'action': 'login', + 'user': 'unknown', + 'from': flask.request.remote_addr, + 'status': 'bad state'}) + return "Unexpected application state", 406 + if not code: + LOGGER.debug('user:%s login no code', 'unknown') + als.als_json_log('user_access', + {'action': 'login', + 'user': 'unknown', + 'from': flask.request.remote_addr, + 'status': 'no code'}) + return "The code was not returned or is not accessible", 406 + + fwd_proto = flask.request.headers.get('X-Forwarded-Proto') + if (fwd_proto == 'https') and (flask.request.scheme == "http"): + redirect_uri = flask.request.base_url.replace("http:", "https:") + else: + redirect_uri = flask.request.base_url + + query_params = {'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': redirect_uri, + 'code_verifier': flask.session['code_verifier'], + } + query_params = requests.compat.urlencode(query_params) + exchange = requests.post( + flask.current_app.config['OIDC_ORG_TOKEN_URL'], + headers=headers, + data=query_params, + auth=(flask.current_app.config['OIDC_CLIENT_ID'], + flask.current_app.config['OIDC_CLIENT_SECRET']), + ).json() + + # Get tokens and validate + if not exchange.get("token_type"): + return "Unsupported token type. Should be 'Bearer'.", 403 + access_token = exchange["access_token"] + + # Authorization flow successful, get userinfo and login user + userinfo_response = requests.get( + flask.current_app.config['OIDC_ORG_USER_INFO_URL'], + headers={'Authorization': 'Bearer %s' % (access_token)}).json() + + user_sub = userinfo_response["sub"] + user_email = userinfo_response["email"] + first_name = userinfo_response["given_name"] + last_name = userinfo_response["family_name"] + user = User.getsub(user_sub) + + try: + if user: + if (user.email != user_email + or user.first_name != first_name + or user.last_name != last_name): + user.email = user_email + user.first_name = first_name + user.last_name = last_name + update_user = True + else: + update_user = False + else: + # User logs in first time. If matched email, reuse that entry, + # otherwise, create new active user entry + user = User.getemail(user_email) + if user: + # update the record + user.sub = user_sub + user.username = user_email + user.first_name = first_name + user.last_name = last_name + user.active = True + user.email_confirmed_at = datetime.datetime.now() + else: + user = User(sub=user_sub, email=user_email, + username=user_email, # fake user name + first_name=first_name, + last_name=last_name, active=True, + password="", + email_confirmed_at=datetime.datetime.now()) + db.session.add(user) # pylint: disable=no-member + update_user = True + if update_user: + db.session.commit() # pylint: disable=no-member + + except Exception as e: + LOGGER.debug('user:%s login unauthorized', user_email) + als.als_json_log('user_access', + {'action': 'login', + 'user': user_email, + 'from': flask.request.remote_addr, + 'status': 'unauthorized'}) + raise werkzeug.exceptions.Unauthorized( + 'An unexpected error occured. Please try again.') + + login_user(user) + + LOGGER.debug('user:%s login success', user.username) + als.als_json_log('user_access', + {'action': 'login', + 'user': user.username, + 'from': flask.request.remote_addr, + 'status': 'success'}) + return flask.redirect(flask.url_for("root")) + + +class LogoutAPI(MethodView): + """ + Logout Resource + """ + + def get(self): + # store app state and code verifier in session + if not flask.current_app.config['OIDC_LOGIN']: + return flask.redirect(flask.url_for('user.logout')) + + try: + LOGGER.debug('user:%s logout', current_user.username) + als.als_json_log('user_access', + {'action': 'logout', + 'user': current_user.username, + 'from': flask.request.remote_addr}) + except BaseException: + LOGGER.debug('user:%s logout', 'unknown') + als.als_json_log( + 'user_access', { + 'action': 'logout', 'user': 'unknown', 'from': flask.request.remote_addr}) + + logout_user() + return flask.redirect(flask.url_for("root")) + + +class UserAPI(MethodView): + """ + User Resource + """ + + def get(self): + if not current_user.is_authenticated: + return flask.make_response("User not authenticated", 401) + + if not current_user.org: + try: + current_user.org = current_user.email[current_user.email.index( + '@') + 1:] + user = User.query.filter(User.id == current_user.id).first() + user.org = current_user.org + db.session.commit() + except BaseException: + current_user.org = "" + + if not current_user.roles: + try: + user = User.query.filter(User.id == current_user.id).first() + user.roles.append(db.session.query( + Role).filter_by(name="Trial").first()) + db.session.commit() + except BaseException: + pass + + # add organization if not exist. + org = current_user.org if current_user.org else "" + organization = Organization.query.filter( + Organization.name == org).first() + if not organization: + organization = Organization(org) + db.session.add(organization) + db.session.commit() + + data = { + 'userId': current_user.id, + 'email': current_user.email, + 'org': org, + 'roles': [r.name for r in current_user.roles], + 'email_confirmed_at': current_user.email_confirmed_at, + 'active': current_user.active, + 'firstName': current_user.first_name, + 'lastName': current_user.last_name, + } + + if flask.current_app.config['OIDC_LOGIN']: + data['editCredential'] = False + else: + data['editCredential'] = True + + responseObject = { + 'status': 'success', + 'data': data, + } + + return flask.make_response(flask.jsonify(responseObject)), 200 + + +# define the API resources +user_view = UserAPI.as_view('UserAPI') +logout_view = LogoutAPI.as_view('LogoutAPI') +login_view = LoginAPI.as_view('LoginAPI') +about_login_view = AboutLoginAPI.as_view('AboutLoginAPI') +callback_view = CallbackAPI.as_view('CallbackAPI') + +# add Rules for API Endpoints +module.add_url_rule( + '/status', + view_func=user_view, + methods=['GET'] +) +module.add_url_rule( + '/logout', + view_func=logout_view, + methods=['GET'] +) +module.add_url_rule( + '/login', + view_func=login_view, + methods=['GET'] +) +module.add_url_rule( + '/about_login', + view_func=about_login_view, + methods=['GET'] +) +module.add_url_rule( + '/callback', + view_func=callback_view, + methods=['GET'] +) diff --git a/src/ratapi/ratapi/views/paws.py b/src/ratapi/ratapi/views/paws.py new file mode 100644 index 0000000..9921412 --- /dev/null +++ b/src/ratapi/ratapi/views/paws.py @@ -0,0 +1,252 @@ +''' API related to IETF RFC 7545 "Protocol to Access White-Space Databases". +All of the API uses JSON-RPC transport on a single endpoint URI. +''' + +import logging +import datetime +import subprocess +import os +import json +import shutil +from random import gauss, random +import threading +from flask_jsonrpc import JSONRPC +from flask_jsonrpc.exceptions import ( + InvalidParamsError, ServerError) +import flask +from ..util import TemporaryDirectory, getQueueDirectory +from ..xml_utils import (datetime_to_xml) +from afcmodels.aaa import User +import gzip +from .ratapi import build_task + +RULESET = 'AFC-6GHZ-DEMO-1.1' + +#: Logger for this module +LOGGER = logging.getLogger(__name__) +LOCK = threading.Lock() + +# thread safe generation of brown noise array + + +def brownNoise(start_mu, sigma, length): + ''' Thread safe generation of a brown noise array + ''' + LOGGER.debug("Waiting for random number lock") + LOCK.acquire() + arr = [start_mu] + prev_val = start_mu + for _i in range(length - 1): + if False: # random() < 0.25: + arr.append(None) + else: + arr.append(gauss(prev_val, sigma)) + prev_val = arr[-1] + + LOGGER.debug("Releasing random number lock") + LOCK.release() + return arr + + +def genProfiles(bandwith, start_freq, num_channels, start_mu, sigma): + ''' Create properly formatted list of profiles for PAWS + returns { dbm: number, hz: number }[][] + ''' + values = brownNoise(start_mu, sigma, num_channels) + profiles = [] + current_profile = [] + begin_freq = start_freq + end_freq = begin_freq + bandwith + for dbm in values: + if not (dbm is None): + current_profile.append(dict(dbm=dbm, hz=begin_freq)) + current_profile.append(dict(dbm=dbm, hz=end_freq)) + elif len(current_profile) > 0: + profiles.append(current_profile) + current_profile = [] + + begin_freq = end_freq + end_freq = begin_freq + bandwith + + if len(current_profile) > 0: + profiles.append(current_profile) + return profiles + + +def _auth_paws(serial_number, model, manufacturer, rulesets): + ''' Authenticate an access point. If must match the serial_number, model, and manufacturer in the database to be valid ''' + if serial_number is None: + raise InvalidParamsError('serialNumber is required in the deviceDesc') + + if rulesets is None or len(rulesets) != 1 or rulesets[0] != RULESET: + raise InvalidParamsError( + 'Invalid rulesetIds: ["{}"] expected'.format(RULESET)) + + +def getSpectrum(**kwargs): + ''' Analyze spectrum availability for a single RLAN AP. + Parameters are AVAIL_SPECTRUM_REQ data and + result is AVAIL_SPECTRUM_RESP data. + ''' + + from flask_login import current_user + + REQUIRED_PARAMS = ( + 'deviceDesc', + 'location', + 'antenna', + 'capabilities', + 'type', + 'version', + ) + + missing_params = frozenset(REQUIRED_PARAMS) - \ + frozenset(list(kwargs.keys())) + if missing_params: + raise InvalidParamsError( + 'Required parameter names: {0}'.format(' '.join(REQUIRED_PARAMS))) + + now_time = datetime.datetime.utcnow() + spectrum_specs = [] + + if flask.current_app.config["PAWS_RANDOM"]: + # this is sample data generation. remove once afc-engine is working + spectrum1 = dict( + resolutionBwHz=20e6, + profiles=genProfiles( + bandwith=20e6, + start_freq=5935e6, + num_channels=59, + start_mu=40, + sigma=5), + ) + spectrum2 = dict( + resolutionBwHz=40e6, + profiles=genProfiles( + bandwith=40e6, + start_freq=5935e6, + num_channels=29, + start_mu=30, + sigma=7), + ) + spectrum3 = dict( + resolutionBwHz=80e6, + profiles=genProfiles( + bandwith=80e6, + start_freq=5935e6, + num_channels=14, + start_mu=35, + sigma=10), + ) + spectrum4 = dict( + resolutionBwHz=160e6, + profiles=genProfiles( + bandwith=160e6, + start_freq=5935e6, + num_channels=7, + start_mu=28, + sigma=12), + ) + spectrum_specs.append(dict( + rulesetInfo=dict( + authority='US', + rulesetId='AFC-6GHZ-DEMO-1.1', + ), + spectrumSchedules=[ + dict( + eventTime=dict( + startTime=datetime_to_xml(now_time), + stopTime=datetime_to_xml( + now_time + datetime.timedelta(days=1)), + ), + spectra=[ + spectrum1, + spectrum2, + spectrum3, + spectrum4, + ], + ), + ], + )) + + LOGGER.debug("Returning sample data") + return dict( + type="AVAIL_SPECTRUM_RESP", + version="1.0", + timestamp=datetime_to_xml(now_time), + deviceDesc=kwargs['deviceDesc'], + spectrumSpecs=spectrum_specs, + ) + + # authenitcate access point + description = kwargs['deviceDesc'] + auth_paws(description.get('serialNumber'), + description.get(- 'modelId'), + description.get('manufacturerId'), + description.get('rulesetIds')) + user_id = current_user.id + user = User.query.filter_by(id=user_id).first() + + request_type = "APAnalysis" + temp_dir = getQueueDirectory( + flask.current_app.config['TASK_QUEUE'], request_type) + + response_file_path = os.path.join( + temp_dir, 'pawsResponse.json') + + request_file_path = os.path.join(temp_dir, 'analysisRequest.json') + + LOGGER.debug("Writing request file: %s", request_file_path) + with open(request_file_path, "w") as f: + f.write(flask.jsonify(kwargs).get_data(as_text=True)) + LOGGER.debug("Request file written") + + task = build_task(request_file_path, response_file_path, + request_type, user_id, user, temp_dir) + + if task.state == 'FAILURE': + raise ServerError('Task was unable to be started') + + LOGGER.debug("WAITING for task to complete") + # wait for task to complete and get result + task.wait() + + if task.failed(): + raise ServerError('Task excecution failed') + + if task.successful() and task.result['status'] == 'DONE': + if not os.path.exists(task.result['result_path']): + raise ServerError('Resource already deleted') + # read temporary file generated by afc-engine + LOGGER.debug("Reading result file: %s", task.result['result_path']) + with gzip.open(task.result['result_path'], 'rb') as resp_file: + return json.loads(resp_file.read()) + + elif task.successful() and task.result['status'] == 'ERROR': + if not os.path.exists(task.result['error_path']): + raise ServerError('Resource already deleted') + # read temporary file generated by afc-engine + LOGGER.debug("Reading error file: %s", task.result['error_path']) + with open(task.result['error_path'], 'rb') as error_file: + raise ServerError(error_file.read(), task.result['exit_code']) + + else: + raise ServerError('Invalid task state') + + +def create_handler(app, path): + ''' The PAWS interface is all JSON-RPC over a single endpoint path. + + :param app: The flask application to bind to. + + :param path: The root path to bind under. + + :return: The :py:cls:`JSONRPC` object created. + ''' + rpc = JSONRPC(service_url=path, enable_web_browsable_api=True) + rpc.method( + 'spectrum.paws.getSpectrum(deviceDesc=object, location=object, antenna=object, capabilities=object, type=str, version=str) -> object', + validate=False)(getSpectrum) + rpc.init_app(app) + + return rpc diff --git a/src/ratapi/ratapi/views/ratafc.py b/src/ratapi/ratapi/views/ratafc.py new file mode 100644 index 0000000..237d457 --- /dev/null +++ b/src/ratapi/ratapi/views/ratafc.py @@ -0,0 +1,1129 @@ +# +# This Python file uses the following encoding: utf-8 +# +# Portions copyright (C) 2021 Broadcom. +# All rights reserved. The term “Broadcom” refers solely +# to the Broadcom Inc. corporate affiliate that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which +# is included with this software program. +# +''' API for AFC specification +''' + +import gzip +import contextlib +import logging +import os +import sys +import shutil +import pkg_resources +import flask +import json +import glob +import re +import datetime +import zlib +import hashlib +import uuid +import copy +import platform +from flask.views import MethodView +import werkzeug.exceptions +import threading +import inspect +from typing import NamedTuple, Optional +import six +from appcfg import RatafcMsghndCfgIface, AFC_RATAPI_LOG_LEVEL +from hchecks import RmqHealthcheck +from defs import RNTM_OPT_NODBG_NOGUI, RNTM_OPT_DBG, RNTM_OPT_GUI, \ + RNTM_OPT_AFCENGINE_HTTP_IO, RNTM_OPT_NOCACHE, RNTM_OPT_SLOW_DBG, \ + RNTM_OPT_CERT_ID +from afc_worker import run +from ..util import AFCEngineException, require_default_uls, getQueueDirectory +from afcmodels.aaa import User, AFCConfig, CertId, Ruleset, \ + Organization, AccessPointDeny +from .auth import auth +from .ratapi import build_task, rulesetIdToRegionStr +from fst import DataIf +import afctask +import als +from afcmodels.base import db +from flask_login import current_user +from .auth import auth +import traceback +from urllib.parse import urlparse +from .ratapi import rulesets +from typing import Any, Dict, NamedTuple, Optional +from rcache_models import RcacheClientSettings +from rcache_client import RcacheClient +import prometheus_client + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(AFC_RATAPI_LOG_LEVEL) + +# Metrics for autoscaling +prometheus_metric_flask_afc_waiting_reqs = \ + prometheus_client.Gauge('msghnd_flask_afc_waiting_reqs', + 'Number of requests waiting for afc engine', + ['host'], multiprocess_mode='sum') +prometheus_metric_flask_afc_waiting_reqs = \ + prometheus_metric_flask_afc_waiting_reqs.labels(host=platform.node()) + +# We want to dynamically trim this this list e.g. +# via environment variable, for the current deployment +RULESETS = rulesets() + +#: All views under this API blueprint +module = flask.Blueprint('ap-afc', 'ap-afc') + +ALLOWED_VERSIONS = ['1.4'] + + +class AP_Exception(Exception): + ''' Exception type used for RAT AFC respones + ''' + + def __init__(self, code, description, supplemental_info=None): + super(AP_Exception, self).__init__(description) + self.response_code = code + self.description = description + self.supplemental_info = supplemental_info + + +# class TempNotProcessedException(AP_Exception): +# ''' +# 101 Name: TEMPORARY_NOT_PROCESSED +# Interpretation: AFC cannot complete processing the request and respond to it. +# Required Action: AFC Device shall resend the same request later. +# Other Information: This responseCode value is returned when, for example, AFC receives a lot of requests or a large request and +# AFC cannot complete the process soon. +# ''' + +# def __init__(self, wait_time_sec=3600): +# super(TempNotProcessedException, self).__init__(101, 'The request could not be processed at this time. Try again later.', +# { 'waitTime': wait_time_sec }) + +class VersionNotSupportedException(AP_Exception): + ''' + 100 Name: VERSION_NOT_SUPPORTED + Interpretation: The requested version number is invalid + The communication can be attempted again using a different version number. In the case of an AP attempting + to communicate with an AFC, communications with the same version but a different AFC could be attempted + ''' + + def __init__(self, invalid_version=[]): + super( + VersionNotSupportedException, self).__init__( + 100, 'The requested version number is invalid', { + 'invalidVersion': invalid_version}) + + +class DeviceUnallowedException(AP_Exception): + ''' + 101 Name: DEVICE_UNALLOWED + Interpretation: The provided credentials are invalid + This specific device as identified by the combination of its FCC ID and unique manufacturer's + serial number is not allowed to operate under AFC control due to regulatory action or other action. + ''' + + def __init__(self, err_str): + super( + DeviceUnallowedException, + self).__init__( + 101, + 'This specific device is not allowed to operate under AFC control.' + + err_str) + + +class MissingParamException(AP_Exception): + ''' + 102 Name: MISSING_PARAM + Interpretation: One or more fields required to be included in the request are missing. + Required Action: Not specified. + Other Information: The supplementalInfo field may carry a list of missing parameter names. + ''' + + def __init__(self, missing_params=[]): + super( + MissingParamException, self).__init__( + 102, 'One or more fields required to be included in the request are missing.', { + 'missingParams': missing_params}) + + +class InvalidValueException(AP_Exception): + ''' + 103 Name: INVALID_VALUE + Interpretation: One or more fields have an invalid value. + Required Action: Not specified. + Other Information: The supplementalInfo field may carry a list of the names of the fields set to invalid value. + ''' + + def __init__(self, invalid_params=[]): + super( + InvalidValueException, self).__init__( + 103, 'One or more fields have an invalid value.', { + 'invalidParams': invalid_params}) + + +class UnexpectedParamException(AP_Exception): + ''' + 106 Name: UNEXPECTED_PARAM + Interpretation: Unknown parameter found, or conditional parameter found, but condition is not met. + Required Action: Not specified. + Other Information: The supplementalInfo field may carry a list of unexpected parameter names. + ''' + + def __init__(self, unexpected_params=[]): + super( + UnexpectedParamException, self).__init__( + 106, 'Unknown parameter found, or conditional parameter found, but condition is not met.', { + 'unexpectedParams': unexpected_params}) + + +class UnsupportedSpectrumException(AP_Exception): + ''' + 300 Name: UNSUPPORTED_SPECTRUM + Interpretation: The frequency range indicated in the Available Spectrum Inquiry Request is at least partially outside of the frequency band + under the management of the AFC (e.g. 5.925-6.425 GHz and 6.525-6.875 GHz bands in US). + Required Action: Not specified. + Other Information: None + ''' + + def __init__(self): + super( + UnsupportedSpectrumException, + self).__init__( + 300, + 'The frequency range indicated in the Available Spectrum Inquiry Request is at least partially ' + 'outside of the frequency band under the management of the AFC.') + + +def _translate_afc_error(error_msg): + unsupported_spectrum = error_msg.find('UNSUPPORTED_SPECTRUM') + if unsupported_spectrum != -1: + raise UnsupportedSpectrumException() + + invalid_value = error_msg.find('INVALID_VALUE') + if invalid_value != -1: + raise InvalidValueException() + + missing_param = error_msg.find('MISSING_PARAM') + if missing_param != -1: + raise MissingParamException() + + unexpected_param = error_msg.find('UNEXPECTED_PARAM') + if unexpected_param != -1: + raise UnexpectedParamException() + + invalid_version = error_msg.find('VERSION_NOT_SUPPORTED') + if invalid_version != -1: + raise VersionNotSupportedException() + + +class VendorExtensionFilter: + """ Filters Vendor Extensions from Messages and requests/responses + according to type-specific whitelists + + Private attributes: + _whitelist_dict -- Dictionary of whitelists, indexed by (nonpartial) + whitelist types + """ + class _WhitelistType(NamedTuple): + """ Whitelist type """ + # True for message, False for request/response, None for both + is_message: Optional[bool] = None + # True for input (request), false for output(response), None for both + is_input: Optional[bool] = None + # True for GUI, False for non-GUI, None for both + is_gui: Optional[bool] = None + # True for internal, False for external, None for both + is_internal: Optional[bool] = None + + def is_partial(self): + """ True for partial whitelist type that has fields set to None """ + return any(getattr(self, attr) is None for attr in self._fields) + + class _Whitelist: + """ Whitelist for a message/request/response type(s) + + Public attributes: + extensions -- Set of allowed extension IDs + + Private attributes + _partial_type -- Specifies type(s) of messages/resuests/response this + whitelist is for + """ + + def __init__(self, extensions, partial_type): + """ Constructor + + Arguments: + extensions -- Sequence of whitelisted extensions + partial_type -- Type(s) of messages/resuests/response this + whitelist is for + """ + assert isinstance(extensions, list) + self._partial_type = partial_type + self.extensions = set(extensions) + + def is_for(self, whitelist_type): + """ True if this whilelist is for messages/requests/responses + specified by nonpartial whitelist type """ + assert not whitelist_type.is_partial() + for attr in whitelist_type._fields: + value = getattr(whitelist_type, attr) + if getattr(self._partial_type, attr) not in (None, value): + return False + return True + + # List of whilelists. Only nonempty whitelists are presented + # It is OK for more than one whitelist to cover some type + # It is OK to add more whitelists (e.g. for different whitelist types) + _whitelists = [ + # Allowed vendor extensions for individual requests from AFC services + # (NOT from APs) + _Whitelist( + ["openAfc.overrideAfcConfig"], + _WhitelistType(is_input=True, is_message=False, is_internal=True)), + # Allowed vendor extensions for individual requests received from APs + _Whitelist( + ["rlanAntenna"], + _WhitelistType(is_input=True, is_message=False)), + # Allowed vendor extensions for individual responses sent to AFC Web + # GUI + _Whitelist( + ["openAfc.redBlackData", "openAfc.mapinfo"], + _WhitelistType(is_input=False, is_message=False, is_gui=True)), + _Whitelist( + ["openAfc.heatMap"], + _WhitelistType(is_input=True, is_gui=True))] + + def __init__(self): + """ Constructor """ + # Maps whitelist types to sets of allowed extensions + self._whitelist_dict = {} + for is_message in (True, False): + for is_input in (True, False): + for is_gui in (True, False): + for is_internal in (True, False): + whitelist_type = \ + self._WhitelistType( + is_input=is_input, is_message=is_message, + is_gui=is_gui, is_internal=is_internal) + # Ensure that no attributes were omitted + assert not whitelist_type.is_partial() + # Finding whitelist for key + for wl in self._whitelists: + if wl.is_for(whitelist_type): + self._whitelist_dict.setdefault( + whitelist_type, set()).\ + update(wl.extensions) + + def filter(self, json_dict, is_input, is_gui, is_internal): + """ Filtering out nonwhitelisted vendor extensions + + Arguments: + json_dict -- Dictionary made of message + is_input -- True for input (request), False for output (Response) + is_gui -- True for GUI + is_internal -- True for internal + """ + if not isinstance(json_dict, dict): + return + # First filtering message + self._filter_ext_list( + json_dict, + self._WhitelistType(is_message=True, is_input=is_input, + is_gui=is_gui, is_internal=is_internal)) + # Filtering individual requests/responses + for req_resp in \ + json_dict.get("availableSpectrumInquiryRequests" if is_input + else "availableSpectrumInquiryResponses", []): + self._filter_ext_list( + req_resp, + self._WhitelistType(is_message=False, is_input=is_input, + is_gui=is_gui, is_internal=is_internal)) + + def _filter_ext_list(self, container, whitelist_type): + """ Drops extensions that not passed whitelist + + Arguments: + container -- Dictionary that (may) contain "vendorExtensions" + list. Modified inplace + whitelist_type -- Type of whitelist to apply + """ + if not isinstance(container, dict): + return + extensions = container.get("vendorExtensions") + if extensions is None: + return + assert not whitelist_type.is_partial() + allowed_extensions = self._whitelist_dict.get(whitelist_type) + if not allowed_extensions: + del container["vendorExtensions"] + return + idx = 0 + while idx < len(extensions): + if extensions[idx]["extensionId"] not in allowed_extensions: + extensions.pop(idx) + else: + idx += 1 + if not extensions: + del container["vendorExtensions"] + + +def get_vendor_extension_filter(): + """ Returns VendorExtensionFilter instance for given app instance""" + if not hasattr(flask.g, "vendor_extension_filter"): + flask.g.vendor_extension_filter = VendorExtensionFilter() + return flask.g.vendor_extension_filter + + +def _get_vendor_extension(json_dict, ext_id): + """ Retrieves given extension from JSON dictionary + + Arguments: + json_dict -- JSON dictionary, presumably containing or eligible to contain + 'vendorExtensions' top level field + ext_id -- Extemnsion ID to look for + Returns 'parameters' field of extension with given ID. None if extension + not found + """ + try: + for extension in json_dict.get("vendorExtensions", []): + if extension["extensionId"] == ext_id: + return extension["parameters"] + except (TypeError, ValueError, LookupError): + pass + return None + + +def fail_done(t): + LOGGER.debug('fail_done()') + task_stat = t.getStat() + dataif = t.getDataif() + error_data = None + with dataif.open(os.path.join("/responses", t.getId(), "engine-error.txt")) as hfile: + try: + error_data = hfile.read().decode("utf-8", errors="replace") + except BaseException: + LOGGER.debug("engine-error.txt not found") + else: + LOGGER.debug("Error data: %s", error_data) + _translate_afc_error(error_data) + with dataif.open(os.path.join("/responses", task_stat['hash'])) as hfile: + hfile.delete() + with dataif.open(os.path.join("/responses", t.getId())) as hfile: + hfile.delete() + # raise for internal AFC E2yyngine error + raise AP_Exception(-1, error_data) + + +def in_progress(t): + # get progress and ETA + LOGGER.debug('in_progress()') + task_stat = t.getStat() + if not isinstance(task_stat['runtime_opts'], type(None)) and \ + task_stat['runtime_opts'] & RNTM_OPT_GUI: + return flask.jsonify( + percent=0, + message='In progress...' + ), 202 + + return flask.make_response( + flask.json.dumps(dict(percent=0, message="Try Again")), + 503) + + +def success_done(t): + LOGGER.debug('success_done()') + task_stat = t.getStat() + dataif = t.getDataif() + hash_val = task_stat['hash'] + + resp_data = None + with dataif.open(os.path.join("/responses", hash_val, "analysisResponse.json.gz")) as hfile: + resp_data = hfile.read() + if task_stat['runtime_opts'] & RNTM_OPT_DBG: + with dataif.open(os.path.join(task_stat['history_dir'], + "analysisResponse.json.gz")) as hfile: + hfile.write(resp_data) + LOGGER.debug('resp_data size=%d', sys.getsizeof(resp_data)) + resp_json = json.loads(zlib.decompress(resp_data, 16 + zlib.MAX_WBITS)) + + # if task_stat['runtime_opts'] & RNTM_OPT_GUI: + # Add the map data file (if it is generated) into a vendor extension + kmz_data = None + try: + with dataif.open(os.path.join("/responses", t.getId(), "results.kmz")) as hfile: + kmz_data = hfile.read() + except BaseException: + pass + map_data = None + try: + with dataif.open(os.path.join("/responses", t.getId(), "mapData.json.gz")) as hfile: + map_data = hfile.read() + except BaseException: + pass + if kmz_data or map_data: + # TODO: temporary support python2 where kmz_data is of type str + if isinstance(kmz_data, str): + kmz_data = kmz_data.encode('base64') + if isinstance(kmz_data, bytes): + kmz_data = kmz_data.decode('iso-8859-1') + resp_json["availableSpectrumInquiryResponses"][0].setdefault( + "vendorExtensions", + []).append( + { + "extensionId": "openAfc.mapinfo", + "parameters": { + "kmzFile": kmz_data if kmz_data else None, + "geoJsonFile": zlib.decompress( + map_data, + 16 + + zlib.MAX_WBITS).decode('iso-8859-1') if map_data else None}}) + get_vendor_extension_filter().filter( + resp_json, is_input=False, + is_gui=bool(task_stat['runtime_opts'] & RNTM_OPT_GUI), + is_internal=bool(task_stat['is_internal_request'])) + resp_data = json.dumps(resp_json) + resp = flask.make_response(resp_data) + resp.content_type = 'application/json' + LOGGER.debug("returning data: %s", resp.data) + with dataif.open(os.path.join("/responses", t.getId())) as hfile: + hfile.delete() + return resp + + +response_map = { + afctask.Task.STAT_SUCCESS: success_done, + afctask.Task.STAT_FAILURE: fail_done, + afctask.Task.STAT_PROGRESS: in_progress +} + + +rcache_settings = RcacheClientSettings() +# In this validation rcache is True to handle 'not update_on_send' case +rcache_settings.validate_for(db=True, rmq=True, rcache=True) +rcache = RcacheClient(rcache_settings, rmq_receiver=True) \ + if rcache_settings.enabled else None + + +class ReqInfo(NamedTuple): + """ Information about single AFC Request, needed for its processing """ + # 0-based request index within message + req_idx: int + + # Hash computed over essential part of request and of AFC config + req_cfg_hash: str + + # Request ID from message + request_id: str + + # AFC Request as dictionary + request: Dict[str, Any] + + # AFC Config file content as string + config_str: str + + # AFC Config file path. None if tasks not used + config_path: Optional[str] + + # Region identifier + region: str + + # Root directory for AFC Engine debug files + history_dir: Optional[str] + + # AFC Engine Request type + request_type: str + + # AFC Engine computation options + runtime_opts: int + + # Task ID. Objstore directory name for AFC Engine artifacts + task_id: str + + +class RatAfc(MethodView): + ''' RAT AFC resources + ''' + + def _auth_cert_id(self, cert_id, ruleset): + ''' Authenticate certification id. Return new indoor value + for bell application + ''' + LOGGER.debug("(%d) %s::%s()", threading.get_native_id(), + self.__class__, inspect.stack()[0][3]) + indoor_certified = True + + certId = CertId.query.filter_by(certification_id=cert_id).first() + if not certId: + raise DeviceUnallowedException("") + if not certId.ruleset.name == ruleset: + raise InvalidValueException(["ruleset", ruleset]) + # add check for certId.valid + + now = datetime.datetime.now() + delta = now - certId.refreshed_at + if delta.days > 1: + LOGGER.debug("(%d) %s::%s() stale CertId %s", + threading.get_native_id(), + self.__class__, inspect.stack()[0][3], + certId.certification_id) + + if not certId.location & certId.OUTDOOR: + raise DeviceUnallowedException("Outdoor operation not allowed") + elif certId.location & certId.INDOOR: + indoor_certified = True + else: + indoor_certified = False + + return indoor_certified + + def _auth_ap(self, serial_number, prefix, cert_id, rulesets, version): + ''' Authenticate an access point. If must match the serial_number and + certification_id in the database to be valid + ''' + LOGGER.debug("(%d) %s::%s()Starting auth_ap,serial: %s; prefix: %s; " + "certId: %s ruleset %s; version %s", + threading.get_native_id(), + self.__class__, inspect.stack()[0][3], + serial_number, prefix, cert_id, rulesets, version) + + deny_ap = AccessPointDeny.query.filter_by( + serial_number=serial_number). filter_by( + certification_id=cert_id).first() + if deny_ap: + raise DeviceUnallowedException("") # InvalidCredentialsException() + + deny_ap = AccessPointDeny.query.filter_by(certification_id=cert_id).\ + filter_by(serial_number=None).first() + if deny_ap: + # denied all devices matching certification id + raise DeviceUnallowedException("") # InvalidCredentialsException() + + ruleset = prefix + + # Test AP will by pass certification ID check as Indoor Certified + if cert_id == "TestCertificationId" \ + and serial_number == "TestSerialNumber": + return True + + if cert_id == "HeatMapCertificationId" \ + and serial_number == "HeatMapSerialNumber": + return True + + # Assume that once we got here, we already trim the cert_obj list down + # to only one entry for the country we're operating in + return self._auth_cert_id(cert_id, ruleset) + + def get(self): + ''' GET method for Analysis Status ''' + task_id = flask.request.args['task_id'] + LOGGER.debug("(%d) %s::%s() task_id=%s", threading.get_native_id(), + self.__class__, inspect.stack()[0][3], task_id) + + dataif = DataIf() + t = afctask.Task( + task_id, dataif, RatafcMsghndCfgIface().get_ratafc_tout()) + task_stat = t.get() + + if t.ready(task_stat): # The task is done + if t.successful(task_stat): # The task is done w/o exception + return response_map[task_stat['status']](t) + else: # The task raised an exception + raise werkzeug.exceptions.InternalServerError( + 'Task execution failed') + else: # The task in progress or pending + if task_stat['status'] == afctask.Task.STAT_PROGRESS: # The task in progress + # 'PROGRESS' is task.state value, not task.result['status'] + return response_map[afctask.Task.STAT_PROGRESS](t) + else: # The task yet not started + LOGGER.debug("(%d) %s::%s() not ready state: %s", + threading.get_native_id(), + self.__class__, inspect.stack()[0][3], + task_stat['status']) + return flask.make_response( + flask.json.dumps(dict(percent=0, message='Pending...')), + 202) + + def post(self): + ''' POST method for RAT AFC + ''' + LOGGER.debug("(%d) %s::%s()", threading.get_native_id(), + self.__class__, inspect.stack()[0][3]) + als_req_id = als.als_afc_req_id() + results = {"availableSpectrumInquiryResponses": []} + request_ids = set() + dataif = DataIf() + + try: + is_internal_request = urlparse(flask.request.url).path.\ + endswith("/availableSpectrumInquiryInternal") + is_gui = flask.request.args.get('gui') == 'True' + get_vendor_extension_filter().filter( + json_dict=flask.request.json, is_input=True, is_gui=is_gui, + is_internal=is_internal_request) + # start request + args = flask.request.json + # check request version + ver = flask.request.json["version"] + if not (ver in ALLOWED_VERSIONS): + raise VersionNotSupportedException([ver]) + results["version"] = ver + + # split multiple requests into an array of individual requests + requests = map( + lambda r: { + "availableSpectrumInquiryRequests": [r], + "version": ver}, + args["availableSpectrumInquiryRequests"]) + request_ids = \ + set([r["requestId"] + for r in args["availableSpectrumInquiryRequests"]]) + + LOGGER.debug("(%d) %s::%s() Running AFC analysis with params: %s", + threading.get_native_id(), + self.__class__, inspect.stack()[0][3], args) + request_type = 'AP-AFC' + + use_tasks = (rcache is None) or \ + (flask.request.args.get('conn_type') == 'async') or \ + (flask.request.args.get('gui') == 'True') + + als.als_afc_request(req_id=als_req_id, req=args) + uls_id = "Unknown" + geo_id = "Unknown" + indoor_certified = True + req_infos = {} + + # Creating req_infos - list of ReqInfo objects + for req_idx, request in enumerate(requests): + try: + # authenticate + LOGGER.debug("(%d) %s::%s() Request: %s", + threading.get_native_id(), + self.__class__, inspect.stack()[0][3], + request) + individual_request = \ + request["availableSpectrumInquiryRequests"][0] + prefix = None + device_desc = individual_request.get('deviceDescriptor') + + devices = device_desc.get('certificationId') + if not devices: + raise MissingParamException( + missing_params=['certificationId']) + + # Pick one ruleset that is being used for the + # deployment of this AFC (RULESETS) + certId = None + region = None + for r in devices: + prefix = r.get('rulesetId') + if not prefix: + # empty/non-exist ruleset + break + if prefix in RULESETS: + region = rulesetIdToRegionStr(prefix) + certId = r.get('id') + break + prefix = prefix.strip() + + if prefix is None: + raise MissingParamException( + missing_params=['rulesetId']) + elif not prefix: + raise InvalidValueException(["rulesetId", prefix]) + + if region: + if certId is None: + # certificationId id field does not exist + raise MissingParamException( + missing_params=['certificationId', 'id']) + elif not certId: + raise InvalidValueException( + ["certificationId", certId]) + else: + # ruleset is not in list + raise DeviceUnallowedException("") + + serial = device_desc.get('serialNumber') + if serial is None: + raise MissingParamException( + missing_params=['serialNumber']) + elif not serial: + raise InvalidValueException(["serialNumber", serial]) + + indoor_certified = \ + self._auth_ap(serial, prefix, certId, + device_desc.get('rulesetIds'), ver) + + if indoor_certified: + runtime_opts = RNTM_OPT_CERT_ID | RNTM_OPT_NODBG_NOGUI + else: + runtime_opts = RNTM_OPT_NODBG_NOGUI + + debug_opt = flask.request.args.get('debug') + if debug_opt == 'True': + runtime_opts |= RNTM_OPT_DBG + edebug_opt = flask.request.args.get('edebug') + if edebug_opt == 'True': + runtime_opts |= RNTM_OPT_SLOW_DBG + if is_gui: + runtime_opts |= RNTM_OPT_GUI + opt = flask.request.args.get('nocache') + if opt == 'True': + runtime_opts |= RNTM_OPT_NOCACHE + if use_tasks: + runtime_opts |= RNTM_OPT_AFCENGINE_HTTP_IO + LOGGER.debug("(%d) %s::%s() runtime %d", + threading.get_native_id(), + self.__class__, inspect.stack()[0][3], + runtime_opts) + + # Retrieving config + config = AFCConfig.query.filter( + AFCConfig.config['regionStr'].astext == region).first() + if not config: + raise DeviceUnallowedException( + "No AFC configuration for ruleset") + afc_config = config.config + if region[:5] == "TEST_" or region[:5] == 'DEMO_': + afc_config = dict(afc_config) + afc_config['regionStr'] = region[5:] + config_str = json.dumps(afc_config, sort_keys=True) + + # calculate hash of config + hashlibobj = hashlib.md5() + hashlibobj.update(config_str.encode('utf-8')) + config_hash = hashlibobj.hexdigest() + config_path = \ + os.path.join("/afc_config", prefix, config_hash, + "afc_config.json") if use_tasks else None + + # calculate hash of config + request + hashlibobj.update( + json.dumps( + {k: v for k, v in individual_request.items() + if k != "requestId"}, + sort_keys=True).encode('utf-8')) + req_cfg_hash = hashlibobj.hexdigest() + + history_dir = None + if runtime_opts & (RNTM_OPT_DBG | RNTM_OPT_SLOW_DBG): + history_dir =\ + os.path.join( + "/history", str(serial), + str(datetime.datetime.now().isoformat())) + req_infos[req_cfg_hash] = \ + ReqInfo( + req_idx=req_idx, req_cfg_hash=req_cfg_hash, + request_id=individual_request["requestId"], + request=request, config_str=config_str, + config_path=config_path, region=region, + history_dir=history_dir, request_type=request_type, + runtime_opts=runtime_opts, + task_id=str(uuid.uuid4())) + except AP_Exception as e: + results["availableSpectrumInquiryResponses"].append( + {"requestId": individual_request["requestId"], + "rulesetId": prefix or "Unknown", + "response": {"responseCode": e.response_code, + "shortDescription": e.description, + "supplementalInfo": + json.dumps(e.supplemental_info) + if e.supplemental_info is not None + else None}}) + + # Creating 'responses' - dictionary of responses as JSON strings, + # indexed by request/config hashes + # First looking up in the cache + responses = self._cache_lookup(dataif=dataif, req_infos=req_infos) + + # Computing the responses not found in cache + # First handling async case + if len(responses) != len(req_infos): + if flask.request.args.get('conn_type') == 'async': + # Special case of async request + if len(req_infos) > 1: + raise \ + AP_Exception(-1, + "Unsupported multipart async request") + assert use_tasks + task = \ + self._start_processing( + dataif=dataif, + req_info=list(req_infos.values())[0], + is_internal_request=is_internal_request, + use_tasks=True) + # No ALS logging for config/response (request is logged + # though). Can be fixed if anybody cares + return flask.jsonify(taskId=task.getId(), + taskState=task.get()["status"]) + responses.update( + self._compute_responses( + dataif=dataif, use_tasks=use_tasks, + req_infos={k: v for k, v in req_infos.items() + if k not in responses}, + is_internal_request=is_internal_request)) + + # Preparing responses for requests + for req_info in req_infos.values(): + response = responses.get(req_info.req_cfg_hash) + if not response: + # No response for request + continue + dataAsJson = json.loads(response) + actualResult = dataAsJson.get( + "availableSpectrumInquiryResponses") + if actualResult is not None: + engine_result_ext = \ + _get_vendor_extension(actualResult[0], + "openAfc.used_data") or {} + uls_id = engine_result_ext.get("openAfc.uls_id", uls_id) + geo_id = engine_result_ext.get("openAfc.geo_id", geo_id) + actualResult[0]["requestId"] = req_info.request_id + results["availableSpectrumInquiryResponses"].append( + actualResult[0]) + als.als_afc_config( + req_id=als_req_id, config_text=req_info.config_str, + customer=req_info.region, geo_data_version=geo_id, + uls_id=uls_id, req_indices=[req_info.req_idx]) + except Exception as e: + LOGGER.error(traceback.format_exc()) + lineno = "Unknown" + frame = sys.exc_info()[2] + while frame is not None: + f_code = frame.tb_frame.f_code + if os.path.basename(f_code.co_filename) == \ + os.path.basename(__file__): + lineno = frame.tb_lineno + frame = frame.tb_next + LOGGER.error('catching exception: %s, raised at line %s', + getattr(e, 'message', repr(e)), str(lineno)) + + # Disaster recovery - filling-in unfulfilled stuff + if "version" not in results: + results["version"] = ALLOWED_VERSIONS[-1] + for missing_id in \ + request_ids - \ + set(r["requestId"] for r in + results["availableSpectrumInquiryResponses"]): + response_info = {"responseCode": -1} + req_info_l = [ri for ri in req_infos.values() + if ri.request_id == missing_id] + if req_info_l: + response_info["shortDescription"] = \ + f"AFC general failure. Task ID {req_info_l[0].task_id}" + results["availableSpectrumInquiryResponses"].append( + {"requestId": missing_id, + "rulesetId": "Unknown", + "response": response_info}) + + # Removing internal vendor extensions + get_vendor_extension_filter().filter( + json_dict=results, is_input=False, is_gui=is_gui, + is_internal=is_internal_request) + + # Logging response to ALS + als.als_afc_response(req_id=als_req_id, resp=results) + + # Logginfg failed requests + for result in results["availableSpectrumInquiryResponses"]: + if result["response"]["responseCode"] != 0: + LOGGER.error( + f"AFC Failure on request {result['requestId']}: " + f"{result['response'].get('shortDescription', '')} " + f"{result['response'].get('supplementalInfo', '')}") + + LOGGER.debug("Final results: %s", str(results)) + resp = flask.make_response(flask.json.dumps(results), 200) + resp.content_type = 'application/json' + return resp + + def _cache_lookup(self, dataif, req_infos): + """ Looks up cached results + + Arguments: + dataif -- Objstore handle (used only if RCache not used) + req_infos -- Dictionary of ReqInfo objects, indexed by request/config + hashes + Returns dictionary of found responses (as strings), indexed by + request/config hashes + """ + cached_keys = \ + [req_cfg_hash for req_cfg_hash in req_infos.keys() + if not (req_infos[req_cfg_hash].runtime_opts & + (RNTM_OPT_GUI | RNTM_OPT_NOCACHE))] + if not cached_keys: + return {} + if rcache is None: + ret = {} + for req_cfg_hash in cached_keys: + try: + with dataif.open(os.path.join( + "/responses", req_cfg_hash, + "analysisResponse.json.gz")) as hfile: + resp_data = hfile.read() + except BaseException: + continue + ret[req_cfg_hash] = \ + zlib.decompress(resp_data, 16 + zlib.MAX_WBITS) + return ret + return rcache.lookup_responses(cached_keys) + + def _start_processing(self, dataif, req_info, is_internal_request, + rcache_queue=None, use_tasks=False): + """ Initiates processing on remote worker + If 'use_tasks' - returns created Task + """ + request_str = json.dumps(req_info.request, sort_keys=True) + if use_tasks: + with dataif.open(req_info.config_path) as hfile: + if not hfile.head(): + hfile.write(req_info.config_str.encode("utf-8")) + with dataif.open(os.path.join("/responses", req_info.req_cfg_hash, + "analysisRequest.json")) as hfile: + hfile.write(request_str) + + if req_info.runtime_opts & RNTM_OPT_DBG: + for fname, content in [("analysisRequest.json", request_str), + ("afc_config.json", req_info.config_str)]: + with dataif.open(os.path.join(req_info.history_dir, fname)) \ + as hfile: + hfile.write(content.encode("utf-8")) + build_task(dataif=dataif, request_type=req_info.request_type, + task_id=req_info.task_id, hash_val=req_info.req_cfg_hash, + config_path=req_info.config_path if use_tasks else None, + history_dir=req_info.history_dir, + runtime_opts=req_info.runtime_opts, + rcache_queue=rcache_queue, + request_str=None if use_tasks else request_str, + config_str=None if use_tasks else req_info.config_str) + if use_tasks: + return afctask.Task(task_id=req_info.task_id, dataif=dataif, + hash_val=req_info.req_cfg_hash, + history_dir=req_info.history_dir, + is_internal_request=is_internal_request) + return None + + @prometheus_metric_flask_afc_waiting_reqs.track_inprogress() + def _compute_responses(self, dataif, use_tasks, req_infos, + is_internal_request): + """ Prepares worker tasks and waits their completion + + Arguments: + dataif -- Objstore handle (used only if RCache not used) + use_tasks -- True to use task-based communication with + worker, False to use RabbitMQ-based + communication + req_infos -- Dictionary of ReqInfo objects, indexed by + request/config hashes + is_internal_request -- True for internal request massage, False for + external request message + Returns dictionary of found responses (as strings), indexed by + request/config hashes + """ + with (contextlib.nullcontext() + if use_tasks else rcache.rmq_create_rx_connection()) as rmq_conn: + tasks = {} + for req_cfg_hash, req_info in req_infos.items(): + tasks[req_cfg_hash] = \ + self._start_processing( + dataif=dataif, req_info=req_info, use_tasks=use_tasks, + is_internal_request=is_internal_request, + rcache_queue=None if use_tasks + else rmq_conn.rx_queue_name()) + if use_tasks: + ret = {} + for req_cfg_hash, task in tasks.items(): + task_stat = \ + task.wait( + timeout=RatafcMsghndCfgIface().get_ratafc_tout()) + ret[req_cfg_hash] = \ + response_map[task_stat['status']](task).data + return ret + ret = \ + rcache.rmq_receive_responses( + rx_connection=rmq_conn, + req_cfg_digests=req_infos.keys(), + timeout_sec=RatafcMsghndCfgIface().get_ratafc_tout()) + for req_cfg_hash, response in ret.items(): + req_info = req_infos[req_cfg_hash] + if (not response) or (not req_info.history_dir): + continue + with dataif.open(os.path.join(req_info.history_dir, + "analysisResponse.json.gz")) \ + as hfile: + hfile.write(zlib.compress(response.encode("utf-8"))) + return ret + + +class RatAfcSec(RatAfc): + def get(self): + LOGGER.debug("(%d) %s::%s()", threading.get_native_id(), + self.__class__, inspect.stack()[0][3]) + user_id = auth(roles=['Analysis', 'Trial', 'Admin']) + return super().get() + + def post(self): + LOGGER.debug("(%d) %s::%s()", threading.get_native_id(), + self.__class__, inspect.stack()[0][3]) + user_id = auth(roles=['Analysis', 'Trial', 'Admin']) + return super().post() + + +class RatAfcInternal(MethodView): + """ Add rule for availableSpectrumInquiryInternal """ + pass + + +class HealthCheck(MethodView): + + def get(self): + '''GET method for HealthCheck''' + msg = 'The ' + flask.current_app.config['AFC_APP_TYPE'] + ' is healthy' + + return flask.make_response(msg, 200) + + +class ReadinessCheck(MethodView): + + def get(self): + '''GET method for Readiness Check''' + msg = 'The ' + flask.current_app.config['AFC_APP_TYPE'] + ' is ' + + hconn = RmqHealthcheck(flask.current_app.config['BROKER_URL']) + if hconn.healthcheck(): + msg += 'not ready' + return flask.make_response(msg, 503) + + msg += 'ready' + return flask.make_response(msg, 200) + + +# registration of default runtime options + +module.add_url_rule('/availableSpectrumInquirySec', + view_func=RatAfcSec.as_view('RatAfcSec')) + +module.add_url_rule('/availableSpectrumInquiry', + view_func=RatAfc.as_view('RatAfc')) + +module.add_url_rule('/availableSpectrumInquiryInternal', + view_func=RatAfc.as_view('RatAfcInternal')) + +module.add_url_rule('/healthy', view_func=HealthCheck.as_view('HealthCheck')) + +module.add_url_rule( + '/ready', view_func=ReadinessCheck.as_view('ReadinessCheck')) + +# Local Variables: +# mode: Python +# indent-tabs-mode: nil +# python-indent: 4 +# End: +# +# vim: sw=4:et:tw=80:cc=+1 diff --git a/src/ratapi/ratapi/views/ratapi.py b/src/ratapi/ratapi/views/ratapi.py new file mode 100644 index 0000000..06d91df --- /dev/null +++ b/src/ratapi/ratapi/views/ratapi.py @@ -0,0 +1,1322 @@ +# +# This Python file uses the following encoding: utf-8 +# +# Portions copyright (C) 2021 Broadcom. +# All rights reserved. The term “Broadcom” refers solely +# to the Broadcom Inc. corporate affiliate that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which +# is included with this software program. +# +''' The custom REST api for using the web UI and configuring AFC. +''' + +import contextlib +import logging +import os +import shutil +import sys +import pkg_resources +import flask +import json +import glob +import re +import inspect +import gevent +import datetime +import requests +import appcfg +import threading +from flask.views import MethodView +import werkzeug.exceptions +from defs import RNTM_OPT_DBG_GUI, RNTM_OPT_DBG +from afc_worker import run +from fst import DataIf +from ncli import MsgPublisher +from hchecks import RmqHealthcheck, ObjstHealthcheck +from ..util import AFCEngineException, require_default_uls, getQueueDirectory +from afcmodels.aaa import User, AccessPointDeny, AFCConfig, MTLS +from afcmodels.base import db +from .auth import auth +from appcfg import ObjstConfig + +#: Logger for this module +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(appcfg.AFC_RATAPI_LOG_LEVEL) + +#: All views under this API blueprint +module = flask.Blueprint('ratapi-v1', 'ratapi') +baseRegions = ['US', 'CA', 'BR', 'GB'] + + +def regions(): + return baseRegions + list(map(lambda s: 'DEMO_' + s, baseRegions)) + \ + list(map(lambda s: 'TEST_' + s, baseRegions)) + + +def rulesets(): + return ['US_47_CFR_PART_15_SUBPART_E', + 'CA_RES_DBS-06', + 'BRAZIL_RULESETID', + 'UNITEDKINGDOM_RULESETID'] + list(map(lambda s: 'DEMO_' + s, + baseRegions)) + list(map(lambda s: 'TEST_' + s, + baseRegions)) + + +# after 1.4 use Ruleset ID +def regionStrToRulesetId(region_str): + """ Input: region_str: regionStr field of the afc config. + Output: nra + nra: can match with the NRA field of the AP, e.g. FCC + Eg. 'USA' => 'FCC' + """ + map = { + 'DEFAULT': 'US_47_CFR_PART_15_SUBPART_E', + 'US': 'US_47_CFR_PART_15_SUBPART_E', + 'CA': 'CA_RES_DBS-06', + 'BR': 'BRAZIL_RULESETID', + 'GB': 'UNITEDKINGDOM_RULESETID' + + } + region_str = region_str.upper() + try: + # for test and demo + if region_str.startswith("DEMO_") or region_str.startswith("TEST_"): + return region_str + + return map[region_str] + except BaseException: + raise werkzeug.exceptions.NotFound('Invalid Region %s' % region_str) + + +def rulesetIdToRegionStr(rulesetId): + map = { + 'US_47_CFR_PART_15_SUBPART_E': 'US', + 'CA_RES_DBS-06': 'CA', + 'BRAZIL_RULESETID': 'BR', + 'UNITEDKINGDOM_RULESETID': 'GB' + + } + rulesetId = rulesetId.upper() + try: + if rulesetId.startswith("DEMO_") or rulesetId.startswith("TEST_"): + if rulesetId in rulesets(): + return rulesetId + + return map[rulesetId] + except BaseException: + raise werkzeug.exceptions.NotFound('Invalid ruleset %s' % rulesetId) + + +def build_task( + dataif, + request_type, + task_id, + hash_val, + config_path, + history_dir, + runtime_opts=RNTM_OPT_DBG_GUI, + rcache_queue=None, + request_str=None, + config_str=None): + """ + Shared logic between PAWS and All other analysis for constructing and async call to run task + """ + + prot, host, port = dataif.getProtocol() + args = [ + prot, + host, + port, + request_type, + task_id, + hash_val, + config_path, + history_dir, + runtime_opts, + flask.current_app.config['NFS_MOUNT_PATH'], + rcache_queue, + request_str, + config_str + ] + LOGGER.debug("build_task() {}".format(args)) + run.apply_async(args) + + +class GuiConfig(MethodView): + ''' Allow the web UI to obtain configuration, including resolved URLs. + ''' + + def get(self): + ''' GET for gui config + ''' + LOGGER.debug(f"({threading.get_native_id()})" + f" {self.__class__.__name__}::{inspect.stack()[0][3]}()" + f" {flask.request.cookies}") + + # Figure out the current server version + try: + if sys.version_info.major != 3: + serververs = pkg_resources.require('ratapi')[0].version + else: + serververs = pkg_resources.get_distribution('ratapi').version + except Exception as err: + LOGGER.error('Failed to fetch server version: {0}'.format(err)) + serververs = 'unknown' + + # TODO: temporary support python2 + if sys.version_info.major != 3: + from urlparse import urlparse + else: + from urllib.parse import urlparse + + u = urlparse(flask.request.url) + + if 'USE_CAPTCHA' in flask.current_app.config and \ + flask.current_app.config['USE_CAPTCHA']: + about_sitekey = flask.current_app.config['CAPTCHA_SITEKEY'] + else: + about_sitekey = None + + if flask.current_app.config['OIDC_LOGIN']: + login_url = flask.url_for('auth.LoginAPI') + logout_url = flask.url_for('auth.LogoutAPI') + about_url = flask.url_for('ratapi-v1.About') + about_login_url = flask.url_for('auth.AboutLoginAPI') + else: + login_url = flask.url_for('user.login'), + logout_url = flask.url_for('user.logout'), + about_url = None + about_login_url = None + + # TODO: temporary support python2 + resp = flask.jsonify( + uls_url=flask.url_for('ratapi-v1.UlsFiles'), + antenna_url=flask.url_for('ratapi-v1.AntennaFiles'), + history_url=flask.url_for("ratapi-v1.History0"), + afcconfig_defaults=flask.url_for( + 'ratapi-v1.AfcConfigFile', filename='default'), + lidar_bounds=flask.url_for('ratapi-v1.LiDAR_Bounds'), + ras_bounds=flask.url_for('ratapi-v1.RAS_Bounds'), + google_apikey=flask.current_app.config['GOOGLE_APIKEY'], + rat_api_analysis=flask.url_for('ratapi-v1.Phase1Analysis', + request_type='p_request_type'), + uls_convert_url=flask.url_for( + 'ratapi-v1.UlsDb', uls_file='p_uls_file'), + status_url=flask.url_for('auth.UserAPI'), + login_url=login_url, + logout_url=logout_url, + admin_url=flask.url_for('admin.User', user_id=-1), + ap_deny_admin_url=flask.url_for('admin.AccessPointDeny', id=-1), + cert_id_admin_url=flask.url_for('admin.CertId', id=-1), + mtls_admin_url=flask.url_for('admin.MTLS', id=-1), + dr_admin_url=flask.url_for('admin.DeniedRegion', regionStr="XX"), + rat_afc=flask.url_for('ap-afc.RatAfcSec'), + about_url=about_url, + about_login_url=about_login_url, + about_sitekey=about_sitekey if about_sitekey else None, + about_csrf=flask.url_for('ratapi-v1.AboutCSRF'), + app_name=flask.current_app.config['USER_APP_NAME'], + version=serververs, + ) + return resp + + +class HealthCheck(MethodView): + + def get(self): + '''GET method for HealthCheck''' + cert_bdl_name = 'certificate/client.bundle.pem' + bundle_data = '' + try: + with DataIf().open(cert_bdl_name) as hfile: + if hfile.head(): + LOGGER.debug(f"Cert bundle already exists.") + raise + # create client bundle certificate file if not exists + for certs in db.session.query(MTLS).all(): + LOGGER.info(f"{certs.id}") + bundle_data += certs.cert + db.session.commit() # pylint: disable=no-member + if len(bundle_data) == 0: + LOGGER.debug(f"No certificates stored.") + raise + # write certificates bundle for clients to objst cache. + hfile.write(bundle_data) + + # note relevant listen peers + cmd = 'cmd_restart' + publisher = MsgPublisher( + flask.current_app.config['BROKER_URL'], + flask.current_app.config['BROKER_EXCH_DISPAT']) + publisher.publish(cmd) + publisher.close() + except BaseException: + pass + + msg = 'The ' + flask.current_app.config['AFC_APP_TYPE'] + ' is healthy' + LOGGER.info(f"{msg}") + return flask.make_response(msg, 200) + + +def check_rmq(cfg): + LOGGER.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()" + f" {cfg['BROKER_URL']}") + hconn = RmqHealthcheck(cfg['BROKER_URL']) + if hconn.healthcheck(): + return 1 + return 0 + + +class ReadinessCheck(MethodView): + + def get(self): + '''GET method for Readiness Check''' + LOGGER.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()" + f" cfg: {flask.current_app.config}") + msg = 'The ' + flask.current_app.config['AFC_APP_TYPE'] + ' is' + objst_chk = ObjstHealthcheck(flask.current_app.config) + checks = [gevent.spawn(objst_chk.healthcheck), + gevent.spawn(check_rmq, flask.current_app.config)] + gevent.joinall(checks) + for i in checks: + if i.value != 0: + msg += 'not ready' + return flask.make_response(msg, 503) + msg += 'ready' + return flask.make_response(msg, 200) + + +class ReloadAnalysis(MethodView): + + ACCEPTABLE_FILES = { + 'analysisRequest.json.gz': dict( + content_type='application/json', + ) + } + + def _open(self, rel_path, mode, username, user=None): + ''' Open a configuration file. + + :param rel_path: The specific config name to open. + :param mode: The file open mode. + :return: The opened file. + :rtype: file-like + need to find the latest file? how to do that? - Glob + ''' + files = glob.glob(os.path.join( + flask.current_app.config['HISTORY_DIR'], username + "*")) + dates = [] + for x in files: + + dateMatch = re.search( + '\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{6}', x) + if dateMatch: + date = datetime.datetime.strptime( + dateMatch.group(), '%Y-%m-%dT%H:%M:%S.%f') + dates.append(date) # dates loaded + + strDate = str(max(dates)) + strDate = strDate.replace(" ", "T") + fileName = username + '-' + strDate + + LOGGER.debug(os.path.join( + flask.current_app.config['HISTORY_DIR'], fileName)) + if mode == 'wb' and user is not None and not os.path.exists( + os.path.join(flask.current_app.config['HISTORY_DIR'], fileName)): + # create scoped user directory so people don't clash over each + # others config + os.mkdir(os.path.join( + flask.current_app.config['HISTORY_DIR'], fileName)) + + config_path = '' + if user is not None and os.path.exists(os.path.join( + flask.current_app.config['HISTORY_DIR'], fileName)): + config_path = os.path.join( + flask.current_app.config['HISTORY_DIR'], fileName) + else: + config_path = os.path.join( + flask.current_app.config['HISTORY_DIR'], fileName) + if not os.path.exists(config_path): + os.makedirs(config_path) + + file_path = os.path.join(config_path, rel_path) + LOGGER.debug('Opening analysisRequest file "%s"', file_path) + if not os.path.exists(file_path) and mode != 'wb': + raise werkzeug.exceptions.NotFound() + + handle = open(file_path, mode) + + if mode == 'wb': + os.chmod(file_path, 0o666) + + return handle + + def get(self): + ''' GET method for afc config + ''' + LOGGER.debug(f"({threading.get_native_id()})" + f" {self.__class__}::{inspect.stack()[0][3]}()") + LOGGER.debug('getting analysisRequest') + user_id = auth(roles=['AP', 'Analysis', 'Admin']) + user = User.query.filter_by(id=user_id).first() + responseObject = { + 'status': 'success', + 'data': { + 'userId': user.id, + 'email': user.email, + 'roles': user.roles, + 'email_confirmed_at': user.email_confirmed_at, + 'active': user.active, + 'firstName': user.first_name, + 'lastName': user.last_name, + } + } + # ensure that webdav is populated with default files + require_default_uls() + filename = 'analysisRequest.json.gz' + if filename not in self.ACCEPTABLE_FILES: + raise werkzeug.exceptions.NotFound() + filedesc = self.ACCEPTABLE_FILES[filename] + + resp = flask.make_response() + with self._open(filename, 'rb', user.email, user_id) as conf_file: + resp.data = conf_file.read() + LOGGER.debug(resp.data) + json_resp = json.loads(resp.data) + LOGGER.debug(json_resp) + # has key deviceDesc and deviceDesc.serialNumber == "analysis-ap" => PointAnalysis + # has key deviceDesc and deviceDesc.serialNumber != "analysis-ap" => Virtual AP + # has key spacing => HeatMap + # has key FSID => ExclusionZone + if ('deviceDesc' in json_resp and json_resp['deviceDesc'] + ['serialNumber'] == "analysis-ap"): + resp.headers['AnalysisType'] = 'PointAnalysis' + + elif ('deviceDesc' in json_resp and json_resp['deviceDesc']['serialNumber'] != "analysis-ap"): + resp.headers['AnalysisType'] = 'VirtualAP' + + elif ('key spacing' in json_resp): + resp.headers['AnalysisType'] = 'HeatMap' + + elif ('FSID' in json_resp): + resp.headers['AnalysisType'] = 'ExclusionZone' + else: + resp.headers['AnalysisType'] = 'None' + LOGGER.debug(json_resp['deviceDesc']['serialNumber']) + LOGGER.debug(resp.headers['AnalysisType']) + # json.dumps(json_resp, resp.data) + # LOGGER.debug(resp.data) + resp.content_type = filedesc['content_type'] + return resp + + +class AfcConfigFile(MethodView): + ''' Allow the web UI to manipulate configuration directly. + ''' + + def get(self, filename): + ''' GET method for afc config + ''' + filename = filename.upper() + LOGGER.debug('AfcConfigFile.get({})'.format(filename)) + user_id = auth(roles=['AP', 'Analysis', 'Super']) + # ensure that webdav is populated with default files + require_default_uls() + + resp = flask.make_response() + config = AFCConfig.query.filter( + AFCConfig.config['regionStr'].astext == filename).first() + if config: + resp.data = json.dumps(config.config) + resp.content_type = 'application/json' + return resp + else: + raise werkzeug.exceptions.NotFound() + + def put(self, filename): + ''' PUT method for afc config + ''' + import als + from flask_login import current_user + + user_id = auth(roles=['Super']) + LOGGER.debug("current user: %s", user_id) + + if flask.request.content_type != 'application/json': + raise werkzeug.exceptions.UnsupportedMediaType() + + bytes = flask.request.stream.read() + rcrd = json.loads(bytes) + filename = rcrd['regionStr'].upper() + LOGGER.debug('AfcConfigFile.put({})'.format(filename)) + # validate the region string + regionStrToRulesetId(filename) + # make sure the config region string is upper case + rcrd['regionStr'] = filename + ordered_bytes = json.dumps(rcrd, sort_keys=True) + try: + config = AFCConfig.query.filter( + AFCConfig.config['regionStr'].astext == filename).first() + if not config: + config = AFCConfig(rcrd) + db.session.add(config) + als.als_json_log('afc_config', + {'action': 'create', + 'user': current_user.username, + 'region': filename, + 'from': flask.request.remote_addr, + 'content': ordered_bytes}) + else: + config.config = rcrd + config.created = datetime.datetime.now() + als.als_json_log('afc_config', + {'action': 'update', + 'user': current_user.username, + 'region': filename, + 'from': flask.request.remote_addr, + 'content': ordered_bytes}) + db.session.commit() + + except BaseException: + raise werkzeug.exceptions.NotFound() + + return flask.make_response('AFC configuration file updated', 204) + + +class AfcRegions(MethodView): + ''' Allow the web UI to manipulate configuration directly. + ''' + + def get(self): + ''' GET method for afc config + ''' + resp = flask.make_response() + resp.data = ' '.join(regions()) + resp.content_type = 'text/plain' + return resp + + +class About(MethodView): + ''' Allow the web UI to manipulate configuration directly. + ''' + + def get(self): + ''' GET method for About + ''' + + resp = flask.make_response() + about_content = "about.html" + resp.data = flask.render_template(about_content) + resp.content_type = 'text/html' + return resp + + def post(self): + ''' POST method for About + ''' + + from flask import request + from flask_mail import Mail, Message + + try: + dest_email = flask.current_app.config['REGISTRATION_DEST_EMAIL'] + dest_pdl_email = flask.current_app.config['REGISTRATION_DEST_PDL_EMAIL'] + src_email = flask.current_app.config['REGISTRATION_SRC_EMAIL'] + approve_link = flask.current_app.config['REGISTRATION_APPROVE_LINK'] + + if 'USE_CAPTCHA' in flask.current_app.config and \ + flask.current_app.config['USE_CAPTCHA']: + captcha_secret = flask.current_app.config['CAPTCHA_SECRET'] + captcha_verify = flask.current_app.config['CAPTCHA_VERIFY'] + else: + captcha_secret = None + + bytes = flask.request.stream.read() + rcrd = json.loads(bytes) + name = rcrd['name'] + email = rcrd['email'] + org = rcrd['org'] + + # verify captcha + if captcha_secret: + token = rcrd['token'] + dictToSend = {'secret': captcha_secret, + 'response': token} + res = requests.post(captcha_verify, data=dictToSend) + + LOGGER.debug("Got verify response " + + str(res.json()["success"])) + + if not res.json()["success"]: + return flask.make_response("No valid captcha", 400) + + recipients = [dest_email] + if dest_pdl_email: + recipients.append(dest_pdl_email) + + mail = Mail(flask.current_app) + msg = Message(f"AFC Access Request by {email}", + sender=src_email, + recipients=recipients) + + msg.body = f'''Name: {name}\nEmail: {email}\nOrg: {org} +Approve request at: {approve_link}''' + mail.send(msg) + return flask.make_response( + f"Thank you {name}. An access request for {email} has been submitted", 204) + except BaseException: + raise werkzeug.exceptions.NotFound() + + +class AboutCSRF(MethodView): + ''' Allow the web UI to manipulate configuration directly. + ''' + + def get(self): + ''' GET method for About + ''' + LOGGER.debug(f"({threading.get_native_id()})" + f" {self.__class__.__name__}::{inspect.stack()[0][3]}()") + + resp = flask.make_response() + about_content = "about_csrf.html" + + resp.data = flask.render_template(about_content) + resp.content_type = 'text/html' + return resp + + +class LiDAR_Bounds(MethodView): + ''' Allow the web UI to manipulate configuration directly. + ''' + + def _open(self, abs_path, mode, user=None): + ''' Open a file. + + :param abs_path: The specific config name to open. + :param mode: The file open mode. + :return: The opened file. + :rtype: file-like + ''' + + LOGGER.debug('Opening file "%s"', abs_path) + if not os.path.exists(abs_path) and mode != 'wb': + raise werkzeug.exceptions.NotFound() + + handle = open(abs_path, mode) + + return handle + + def get(self): + ''' GET method for LiDAR_Bounds + ''' + LOGGER.debug("LiDAR_Bounds.get()") + user_id = auth(roles=['AP', 'Analysis']) + + import xdg.BaseDirectory + + try: + resp = flask.make_response() + datapath = next(xdg.BaseDirectory.load_data_paths( + 'fbrat', 'rat_transfer', 'proc_lidar_2019')) + full_path = os.path.join(datapath, 'LiDAR_Bounds.json.gz') + if not os.path.exists(full_path): + raise werkzeug.exceptions.NotFound( + 'LiDAR bounds file not found') + with self._open(full_path, 'rb', user_id) as data_file: + resp.data = data_file.read() + resp.content_type = 'application/json' + resp.content_encoding = 'gzip' + return resp + except StopIteration: + raise werkzeug.exceptions.NotFound('Path not found to file') + + +class RAS_Bounds(MethodView): + ''' Allow the web UI to manipulate configuration directly. + ''' + + def _open(self, abs_path, mode, user=None): + ''' Open a file. + + :param abs_path: The specific config name to open. + :param mode: The file open mode. + :return: The opened file. + :rtype: file-like + ''' + + LOGGER.debug('Opening file "%s"', abs_path) + if not os.path.exists(abs_path) and mode != 'wb': + raise werkzeug.exceptions.NotFound() + + handle = open(abs_path, mode) + + return handle + + def get(self): + ''' GET method for RAS_Bounds + ''' + LOGGER.debug("RAS_Bounds.get()") + LOGGER.debug('getting RAS bounds') + user_id = auth(roles=['AP', 'Analysis']) + + import xdg.BaseDirectory + + try: + resp = flask.make_response() + datapath = next(xdg.BaseDirectory.load_data_paths( + 'fbrat', 'rat_transfer', 'proc_lidar_2019')) + full_path = os.path.join(datapath, 'RAS_ExclusionZone.json') + if not os.path.exists(full_path): + raise werkzeug.exceptions.NotFound( + 'RAS exclusion zone file not found') + with self._open(full_path, 'rb', user_id) as data_file: + resp.data = data_file.read() + resp.content_type = 'application/json' + # resp.content_encoding = 'gzip' + return resp + except StopIteration: + raise werkzeug.exceptions.NotFound('Path not found to file') + + +class Phase1Analysis(MethodView): + ''' Run analysis using AFC engine and display graphical results on map and graphs + ''' + + methods = ['POST'] + + def _open(self, abs_path, mode): + ''' Open a response file. + + :param rel_path: The specific file name to open. + + :return: The opened file. + :rtype: file-like + ''' + + LOGGER.debug('Attempting to open response file "%s"', abs_path) + if not os.path.exists(abs_path): + raise werkzeug.exceptions.InternalServerError( + description='Your analysis was unable to be processed.') + return open(abs_path, mode) + + def post(self, request_type): + ''' Run analysis + + :param request_type: 'PointAnalysis', 'ExclusionZoneAnalysis', or 'HeatmapAnalysis' + ''' + LOGGER.debug(f"({threading.get_native_id()})" + f" {self.__class__.__name__}::{inspect.stack()[0][3]}()") + + user_id = auth(roles=['Analysis']) + user = User.query.filter_by(id=user_id).first() + + args = flask.request.data + LOGGER.debug("Running phase 1 analysis with params: %s", args) + + valid_requests = ['PointAnalysis', + 'ExclusionZoneAnalysis', 'HeatmapAnalysis'] + if request_type not in valid_requests: + raise werkzeug.exceptions.BadRequest('Invalid request type') + temp_dir = getQueueDirectory( + flask.current_app.config['TASK_QUEUE'], request_type) + request_file_path = os.path.join(temp_dir, 'analysisRequest.json.gz') + + response_file_path = os.path.join( + temp_dir, 'analysisResponse.json.gz') + + LOGGER.debug("Writing request file: %s", request_file_path) + with open(request_file_path, "w") as fle: + fle.write(args) # write JSON to request file + LOGGER.debug("Request file written") + + task = build_task(request_file_path, response_file_path, + request_type, user_id, user, temp_dir) + + if task.state == 'FAILURE': + raise werkzeug.exceptions.InternalServerError( + 'Task was unable to be started', dict( + id=task.id, info=str( + task.info))) + + include_kml = request_type in [ + 'ExclusionZoneAnalysis', 'PointAnalysis'] + + return flask.jsonify( + taskId=task.id, + statusUrl=flask.url_for( + 'ratapi-v1.AnalysisStatus', task_id=task.id), + kmlUrl=(flask.url_for('ratapi-v1.AnalysisKmlResult', + task_id=task.id) if include_kml else None) + ) + + +class AnalysisKmlResult(MethodView): + ''' Get a KML result file from AFC Engine ''' + + methods = ['GET'] + + def _open(self, abs_path, mode): + ''' Open a response file. + + :param rel_path: The specific file name to open. + + :return: The opened file. + :rtype: file-like + ''' + + LOGGER.debug('Attempting to open response file "%s"', abs_path) + if not os.path.exists(abs_path): + raise werkzeug.exceptions.InternalServerError( + description='Your analysis was unable to be processed.') + return open(abs_path, mode) + + def get(self, task_id): + ''' GET method for KML Result ''' + LOGGER.debug(f"({threading.get_native_id()})" + f" {self.__class__.__name__}::{inspect.stack()[0][3]}()") + + task = run.AsyncResult(task_id) + LOGGER.debug('state: %s', task.state) + + if task.successful() and task.result['status'] == 'DONE': + if not os.path.exists(task.result['result_path']): + return flask.make_response(flask.json.dumps( + dict(message='Resource already deleted')), 410) + if 'kml_path' not in task.result or not os.path.exists( + task.result['kml_path']): + return werkzeug.exceptions.NotFound( + 'This task did not produce a KML') + resp = flask.make_response() + LOGGER.debug("Reading kml file: %s", task.result['kml_path']) + with self._open(task.result['kml_path'], 'rb') as resp_file: + resp.data = resp_file.read() + resp.content_type = 'application/octet-stream' + return resp + + raise werkzeug.exceptions.NotFound('KML not found') + + +class AnalysisStatus(MethodView): + ''' Check status of task ''' + + methods = ['GET', 'DELETE'] + + def _open(self, abs_path, mode): + ''' Open a response file. + + :param rel_path: The specific file name to open. + :return: The opened file. + :rtype: file-like + ''' + + LOGGER.debug('Attempting to open response file "%s"', abs_path) + if not os.path.exists(abs_path): + raise werkzeug.exceptions.InternalServerError( + description='Your analysis was unable to be processed.') + return open(abs_path, mode) + + def get(self, task_id): + ''' GET method for Analysis Status ''' + LOGGER.debug(f"({threading.get_native_id()})" + f" {self.__class__.__name__}::{inspect.stack()[0][3]}()") + + task = run.AsyncResult(task_id) + + LOGGER.debug('state: %s', task.state) + + if task.info and task.info.get('progress_file') is not None and \ + not os.path.exists(os.path.dirname(task.info['progress_file'])): + return flask.make_response(flask.json.dumps( + dict(percent=0, message="Try Again")), 503) + + if task.state == 'PROGRESS': + auth(is_user=task.info['user_id']) + # get progress and ETA + if not os.path.exists(task.info['progress_file']): + return flask.jsonify( + percent=0, + message='Initializing...' + ), 202 + progress_file = task.info['progress_file'] + with open(progress_file, 'r') as prog: + lines = prog.readlines() + if len(lines) <= 0: + return flask.make_response(flask.json.dumps( + dict(percent=0, message="Try Again")), 503) + LOGGER.debug(lines) + return flask.jsonify( + percent=float(lines[0]), + message=lines[1], + ), 202 + + if not task.ready(): + return flask.make_response(flask.json.dumps( + dict(percent=0, message='Pending...')), 202) + + if task.failed(): + raise werkzeug.exceptions.InternalServerError( + 'Task execution failed') + + if task.successful() and task.result['status'] == 'DONE': + auth(is_user=task.result['user_id']) + if not os.path.exists(task.result['result_path']): + return flask.make_response(flask.json.dumps( + dict(message='Resource already deleted')), 410) + # read temporary file generated by afc-engine + resp = flask.make_response() + LOGGER.debug("Reading result file: %s", task.result['result_path']) + with self._open(task.result['result_path'], 'rb') as resp_file: + resp.data = resp_file.read() + resp.content_type = 'application/json' + resp.content_encoding = "gzip" + return resp + + elif task.successful() and task.result['status'] == 'ERROR': + auth(is_user=task.result['user_id']) + if not os.path.exists(task.result['error_path']): + return flask.make_response(flask.json.dumps( + dict(message='Resource already deleted')), 410) + # read temporary file generated by afc-engine + LOGGER.debug("Reading error file: %s", task.result['error_path']) + with self._open(task.result['error_path'], 'rb') as error_file: + raise AFCEngineException( + description=error_file.read(), + exit_code=task.result['exit_code']) + + else: + raise werkzeug.exceptions.NotFound('Task not found') + + def delete(self, task_id): + ''' DELETE method for Analysis Status ''' + + task = run.AsyncResult(task_id) + + if not task.ready(): + # task is still running, terminate it + LOGGER.debug('Terminating %s', task_id) + task.revoke(terminate=True) + return flask.make_response(flask.json.dumps( + dict(message='Task deleted')), 200) + + if task.failed(): + task.forget() + return flask.make_response(flask.json.dumps( + dict(message='Task deleted')), 200) + + if task.successful() and task.result['status'] == 'DONE': + auth(is_user=task.result['user_id']) + if not os.path.exists(task.info['result_path']): + return flask.make_response(flask.json.dumps( + dict(message='Resource already deleted')), 410) + LOGGER.debug('Deleting %s', task.info.get('result_path')) + os.remove(task.info['result_path']) + if 'kml_path' in task.info and os.path.exists( + task.info['kml_path']): + os.remove(task.info['kml_path']) + task.forget() + return flask.make_response(flask.json.dumps( + dict(message='Task deleted')), 200) + + elif task.successful() and task.result['status'] == 'ERROR': + auth(is_user=task.result['user_id']) + if not os.path.exists(task.info['error_path']): + return flask.make_response(flask.json.dumps( + dict(message='Resource already deleted')), 410) + LOGGER.debug('Deleting %s', task.info.get('result_path')) + os.remove(task.info['error_path']) + task.forget() + return flask.make_response(flask.json.dumps( + dict(message='Task deleted')), 200) + + else: + raise werkzeug.exceptions.NotFound('Task not found') + + +class UlsDb(MethodView): + ''' Resource for converting uls files ''' + + methods = ['POST'] + + def post(self, uls_file): + ''' POST method for ULS Db convert ''' + + auth(roles=['Super']) + + from ..db.generators import create_uls_db + + uls_path = os.path.join( + flask.current_app.config['NFS_MOUNT_PATH'], + 'rat_transfer', + 'ULS_Database', + uls_file) + if not os.path.exists(uls_path): + raise werkzeug.exceptions.BadRequest( + "File does not exist: " + uls_file) + + file_base = os.path.splitext(uls_path)[0] + LOGGER.debug('converting uls from csv(%s) to sqlite3(%s)', + uls_path, file_base + '.sqlite3') + try: + invalid_rows, errors = create_uls_db(file_base, uls_path) + LOGGER.debug('conversion complete') + + return flask.jsonify( + invalidRows=invalid_rows, + errors=errors + ) + + except Exception as err: + raise werkzeug.exceptions.InternalServerError( + description=err.message) + + +class UlsParse(MethodView): + ''' Resource for daily parse of ULS data ''' + + methods = ['GET', 'POST', 'PUT'] + + def get(self): + ''' GET method for last successful runtime of uls parse + ''' + LOGGER.debug('getting last successful runtime of uls parse') + user_id = auth(roles=['Admin']) + + try: + datapath = flask.current_app.config["STATE_ROOT_PATH"] + \ + '/daily_uls_parse/data_files/lastSuccessfulRun.txt' + if not os.path.exists(datapath): + raise werkzeug.exceptions.NotFound( + 'last succesful run file not found') + lastSuccess = '' + with open(datapath, 'r') as data_file: + lastSuccess = data_file.read() + return flask.jsonify( + lastSuccessfulRun=lastSuccess, + ) + except Exception as err: + raise werkzeug.exceptions.InternalServerError( + description=err.message) + + def post(self): + ''' POST method for manual daily ULS Db parsing ''' + + auth(roles=['Super']) + + LOGGER.debug('Kicking off daily uls parse') + try: + task = parseULS.apply_async( + args=[flask.current_app.config["STATE_ROOT_PATH"], True]) + + LOGGER.debug('uls parse started') + + if task.state == 'FAILURE': + raise werkzeug.exceptions.InternalServerError( + 'Task was unable to be started', dict(id=task.id, info=str(task.info))) + + return flask.jsonify( + taskId=task.id, + + statusUrl=flask.url_for( + 'ratapi-v1.DailyULSStatus', task_id=task.id), + ) + + except Exception as err: + raise werkzeug.exceptions.InternalServerError( + description=err.message) + + def put(self): + ''' Put method for setting the automatic daily ULS time ''' + + auth(roles=['Super']) + args = json.loads(flask.request.data) + LOGGER.debug('Recieved arg %s', args) + hours = args["hours"] + mins = args["mins"] + if hours == 0: + hours = "00" + if mins == 0: + mins = "00" + timeStr = str(hours) + ":" + str(mins) + LOGGER.debug('Updating automated ULS time to ' + timeStr + " UTC") + datapath = flask.current_app.config["STATE_ROOT_PATH"] + \ + '/daily_uls_parse/data_files/nextRun.txt' + if not os.path.exists(datapath): + raise werkzeug.exceptions.NotFound('next run file not found') + with open(datapath, 'w') as data_file: + data_file.write(timeStr) + try: + return flask.jsonify( + newTime=timeStr + ), 200 + except Exception as err: + raise werkzeug.exceptions.InternalServerError( + description=err.message) + + +class DailyULSStatus(MethodView): + ''' Check status of task ''' + + methods = ['GET', 'DELETE'] + + def resetManualParseFile(self): + ''' Overwrites the file for manual task id with a blank string ''' + datapath = flask.current_app.config["STATE_ROOT_PATH"] + \ + '/daily_uls_parse/data_files/currentManualId.txt' + with open(datapath, 'w') as data_file: + data_file.write("") + + def get(self, task_id): + ''' GET method for uls parse Status ''' + LOGGER.debug("Getting ULS Parse status with task id: " + task_id) + task = parseULS.AsyncResult(task_id) + # LOGGER.debug('state: %s', task.state) + + if task.state == 'PROGRESS': + LOGGER.debug("Found Task in progress") + # todo: add percent progress + return flask.jsonify( + percent="WIP", + ), 202 + if not task.ready(): + LOGGER.debug("Found Task pending") + return flask.make_response(flask.json.dumps( + dict(percent=0, message='Pending...')), 202) + if task.state == 'REVOKED': + LOGGER.debug("Found task already in progress") + # LOGGER.debug("task info %s", task.info) + raise werkzeug.exceptions.ServiceUnavailable() + + elif task.failed(): + LOGGER.debug("Found failed task") + self.resetManualParseFile() + raise werkzeug.exceptions.InternalServerError( + 'Task excecution failed') + + if task.successful(): + self.resetManualParseFile() + results = task.result + return flask.jsonify( + entriesUpdated=results[0], + entriesAdded=results[1], + finishTime=results[2] + ), 200 + + else: + raise werkzeug.exceptions.NotFound('Task not found') + + def delete(self, task_id): + ''' DELETE method for ULS Status ''' + + task = parseULS.AsyncResult(task_id) + + if not task.ready(): + # task is still running, terminate it + LOGGER.debug('Terminating %s', task_id) + task.revoke(terminate=True) + return flask.make_response(flask.json.dumps( + dict(message='Task deleted')), 200) + if task.failed(): + task.forget() + return flask.make_response(flask.json.dumps( + dict(message='Task deleted')), 200) + + if task.successful() and task.result['status'] == 'DONE': + auth(is_user=task.result['user_id']) + task.forget() + return flask.make_response(flask.json.dumps( + dict(message='Task deleted')), 200) + + elif task.successful() and task.result['status'] == 'ERROR': + auth(is_user=task.result['user_id']) + task.forget() + return flask.make_response(flask.json.dumps( + dict(message='Task deleted')), 200) + + else: + raise werkzeug.exceptions.NotFound('Task not found') + + +class BackendFiles(): + ''' Allow the web UI to manipulate configuration directly. + ''' + + def get(self, url): + ''' GET method for afc config + ''' + import requests + headers = { + 'accept': 'text/html,application/xhtml+xml,application/xml', + 'Accept-Encoding': 'gzip, deflate', + 'cache-control': 'max-age=0', + 'content-type': 'application/x-www-form-urlencoded', + 'user-agent': 'rat_server/1.0' + } + resp = requests.get(url, headers) + response = flask.make_response() + response.content_type = 'text/html' + response.data = resp.content + return response + + +class UlsFiles(MethodView): + ''' Allow the web UI to manipulate configuration directly. + ''' + + def get(self): + ''' GET method for uls + ''' + user_id = auth(roles=['AP', 'Analysis', 'Admin']) + url = "http://localhost/" + flask.url_for('files.uls_db') + be = BackendFiles() + return be.get(url) + + +class AntennaFiles(MethodView): + ''' Allow the web UI to manipulate configuration directly. + ''' + + def get(self): + ''' GET method for uls + ''' + user_id = auth(roles=['AP', 'Analysis', 'Admin']) + url = "http://localhost/" + flask.url_for('files.antenna_pattern') + be = BackendFiles() + return be.get(url) + + +class AfcRulesetIds(MethodView): + ''' Allow the web UI to manipulate configuration directly. + ''' + + def get(self): + ''' GET method for afc config + ''' + resp = flask.make_response() + resp.data = ' '.join(rulesets()) + resp.content_type = 'text/plain' + return resp + + +class History(MethodView): + def get(self, path=None): + LOGGER.debug(f"History::get({path})") + user_id = auth(roles=['Analysis', 'Trial', 'Admin']) + conf = appcfg.ObjstConfig() + fwd_proto = flask.request.headers.get('X-Forwarded-Proto') + if not fwd_proto: + forward_proto = flask.request.scheme + try: + rurl = flask.request.base_url + if path is not None: + path_len = len(path) + rurl = flask.request.base_url[:-path_len] + response = requests.get( + conf.AFC_OBJST_SCHEME + + "://" + + conf.AFC_OBJST_HOST + + ":" + + conf.AFC_OBJST_HIST_PORT + + ( + ("/" + + path) if path is not None else ""), + headers={ + 'X-Forwarded-Proto': fwd_proto}, + params={ + "url": rurl}, + stream=True) + if response.headers['Content-Type'].startswith("application/octet-stream") \ + and "Content-Encoding" not in response.headers: + # results.kmz case. Apache can't decompress it. + resp = flask.make_response() + resp.data = response.raw.read() + return resp + else: + return flask.render_template_string(response.text) + except Exception as exc: + LOGGER.error(f"Unreachable history host. {exc}") + return f"Unreachable history host. {exc}" + + +class History0(History): + pass + + +class GetRuleset(MethodView): + """ Get all active rulesets """ + + def get(self): + try: + configs = AFCConfig.query.all() + regionStrs = set() + for config in configs: + regionStrs.add(regionStrToRulesetId( + config.config['regionStr'])) + except BaseException: + return flask.make_response('DB error', 404) + resp = flask.make_response() + resp.data = "{ \n\t\"rulesetId\": [" + ", ".join( + '"{0}"'.format(x) for x in regionStrs) + "]\n}\n" + resp.content_type = 'application/json' + return resp + + +class GetAfcConfigByRuleset(MethodView): + """ Get afc_config by rulesets """ + + def get(self, ruleset): + regionStr = rulesetIdToRegionStr(ruleset) # returns 404 if not found + try: + config = AFCConfig.query.filter( + AFCConfig.config['regionStr'].astext == regionStr).first() + except BaseException: + return flask.make_response('DB error', 404) + if config is None: + return flask.make_response("Ruleset unactive", 404) + resp = flask.make_response() + resp.data = json.dumps(config.config, sort_keys=True, indent=4) + "\n" + resp.content_type = 'application/json' + return resp + + +module.add_url_rule('/guiconfig', view_func=GuiConfig.as_view('GuiConfig')) +module.add_url_rule('/afcconfig/', + view_func=AfcConfigFile.as_view('AfcConfigFile')) +module.add_url_rule('/files/lidar_bounds', + view_func=LiDAR_Bounds.as_view('LiDAR_Bounds')) +module.add_url_rule('/files/ras_bounds', + view_func=RAS_Bounds.as_view('RAS_Bounds')) +module.add_url_rule('/analysis/p1/', + view_func=Phase1Analysis.as_view('Phase1Analysis')) +module.add_url_rule('/analysis/status/p1/', + view_func=AnalysisStatus.as_view('AnalysisStatus')) +module.add_url_rule('/analysis/kml/p1/', + view_func=AnalysisKmlResult.as_view('AnalysisKmlResult')) +module.add_url_rule('/convert/uls/csv/sql/', + view_func=UlsDb.as_view('UlsDb')) +module.add_url_rule('/replay', + view_func=ReloadAnalysis.as_view('ReloadAnalysis')) +module.add_url_rule('/regions', + view_func=AfcRegions.as_view('AfcRegions')) +module.add_url_rule('/about', + view_func=About.as_view('About')) +module.add_url_rule('/rulesetIds', + view_func=AfcRulesetIds.as_view('AfcRulesetIds')) +module.add_url_rule('/healthy', + view_func=HealthCheck.as_view('HealthCheck')) +module.add_url_rule('/ready', + view_func=ReadinessCheck.as_view('ReadinessCheck')) +module.add_url_rule('/history', + view_func=History0.as_view('History0')) +module.add_url_rule('/history/', + view_func=History.as_view('History')) +module.add_url_rule('/ulsFiles/', + view_func=UlsFiles.as_view('UlsFiles')) +module.add_url_rule('/antennaFiles/', + view_func=AntennaFiles.as_view('AntennaFiles')) +module.add_url_rule('/GetRulesetIDs', + view_func=GetRuleset.as_view('GetRuleset')) +module.add_url_rule( + '/GetAfcConfigByRulesetID/', + view_func=GetAfcConfigByRuleset.as_view('GetAfcConfigByRuleset')) +module.add_url_rule('/about_csrf', + view_func=AboutCSRF.as_view('AboutCSRF')) diff --git a/src/ratapi/ratapi/xml_utils.py b/src/ratapi/ratapi/xml_utils.py new file mode 100644 index 0000000..603ff4a --- /dev/null +++ b/src/ratapi/ratapi/xml_utils.py @@ -0,0 +1,83 @@ +''' Shared utility classes and functions related to XML processing. +''' + +import re +import datetime + + +def int_to_xml(value): + ''' Convert to XML Schema canonical "int" text. + + :param value: The number to convert. + :type value: int or None + :return: The integer text. + :rtype: unicode or None + ''' + if value is None: + return None + + return unicode(value) + + +def xml_to_int(text, default=None): + ''' Convert from XML Schema "integer" text to python int. + + :param text: The text to convert. + :type text: str or unicode or None + :param default: The default value to use in case text is None. + :type default: int or None + :return: The integer value. + :rtype: int or None + ''' + if text is None: + return default + + return int(text) + + +def datetime_to_xml(value): + ''' Convert to XML Schema canonical "dateTime" text. + ''' + if value is None: + return None + return value.strftime(u'%Y-%m-%dT%H:%M:%SZ') + + +#: Regular expression for matching XML Schema date-time values in UTC (with or without separators) +XSD_DATETIME_ZULU_RE = re.compile( + r'(\d{4})-?(\d{2})-?(\d{2})T(\d{2}):?(\d{2}):?(\d{2})(\.(\d{1,6}))?Z') + + +def xml_to_datetime(text, default=None): + ''' Convert from XML Schema "dateTime" text to python datetime object. + + :param text: The text to convert. + :type text: str or unicode + :param default: The default value to use in case text is None. + :type default: int or None + :return: The time value. + :rtype: :py:cls:`datetime.datetime` + ''' + import math + + if text is None: + return default + + match = XSD_DATETIME_ZULU_RE.match(text) + if match is None: + raise ValueError('Invalid XML datetime "{0}"'.format(text)) + + kwargs = dict( + year=xml_to_int(match.group(1)), + month=xml_to_int(match.group(2)), + day=xml_to_int(match.group(3)), + hour=xml_to_int(match.group(4)), + minute=xml_to_int(match.group(5)), + second=xml_to_int(match.group(6)), + ) + subsec = match.group(8) + if subsec: + kwargs['microsecond'] = int(xml_to_int( + subsec) * math.pow(10, 6 - len(subsec))) + + return datetime.datetime(**kwargs) diff --git a/src/ratapi/setup.py.in b/src/ratapi/setup.py.in new file mode 100644 index 0000000..df8aaf4 --- /dev/null +++ b/src/ratapi/setup.py.in @@ -0,0 +1,61 @@ +import os +import setuptools + +if 'DESTDIR' not in os.environ: + os.environ['DESTDIR'] = '' + +setuptools.setup( + name='ratapi', + # Label compatible with PEP 440 + version='@PROJECT_VERSION@+@SVN_LAST_REVISION@'.lower(), + + author='RKF Engineering Solutions, LLC', + url='http://www.rkf-eng.com/', + license='Redistributable, no modification', + + description='AFC', + package_dir={ + '': '@DIST_LIB_PACKAGE_DIR_ESCAPED@' + }, + packages=[ + 'ratapi', + 'ratapi.migrations', + 'ratapi.migrations.versions', + 'ratapi.views', + 'ratapi.db', + 'ratapi.db.models', + ], + package_data = { + 'ratapi': [ + 'migrations/alembic.ini', + 'migrations/script.py.mako', + 'templates/flask_user_layout.html', + 'templates/login.html', + 'templates/about.html', + 'templates/about_csrf.html', + 'templates/*.html', + ] + }, + entry_points={ + 'console_scripts': [ + 'rat-manage-api = ratapi.manage:main', + ] + }, + + install_requires=[ + 'werkzeug', + 'flask', + 'flask-script', + 'flask_jsonrpc', + 'flask-user', + 'flask-sqlalchemy', + 'flask-mail', + 'pyxdg', + 'cryptography', + 'wsgidav', + 'SQLAlchemy', + 'numpy', + 'prettytable', + #'jwt', + ], +) diff --git a/src/ratcommon/CMakeLists.txt b/src/ratcommon/CMakeLists.txt new file mode 100644 index 0000000..0c401fb --- /dev/null +++ b/src/ratcommon/CMakeLists.txt @@ -0,0 +1,17 @@ +# All source files to same target +set(TGT_NAME "ratcommon") + +file(GLOB ALL_CPP "*.cpp") +list(APPEND ALL_CPP "${CMAKE_CURRENT_BINARY_DIR}/RatVersion.cpp") +file(GLOB ALL_HEADER "*.h") +list(APPEND ALL_HEADER "${CMAKE_CURRENT_BINARY_DIR}/RatVersion.h") +add_dist_library(TARGET ${TGT_NAME} SOURCES ${ALL_CPP} HEADERS ${ALL_HEADER} EXPORTNAME fbratTargets) +set(TARGET_LIBS ${TARGET_LIBS} ${TGT_NAME} PARENT_SCOPE) + +configure_file(RatVersion.h.in RatVersion.h @ONLY) +configure_file(RatVersion.cpp.in RatVersion.cpp @ONLY) + +target_link_libraries(${TGT_NAME} PUBLIC afclogging) +target_link_libraries(${TGT_NAME} PUBLIC Qt5::Core) +target_link_libraries(${TGT_NAME} PUBLIC Qt5::Network) +target_link_libraries(${TGT_NAME} PUBLIC minizip) diff --git a/src/ratcommon/CsvReader.cpp b/src/ratcommon/CsvReader.cpp new file mode 100644 index 0000000..c4a710d --- /dev/null +++ b/src/ratcommon/CsvReader.cpp @@ -0,0 +1,430 @@ +// + +#include +#include "CsvReader.h" + +namespace +{ + +/** Simple state machine to match a text string exactly. + */ +class TextMatcher +{ + Q_DISABLE_COPY(TextMatcher) + public: + /** Create a new matcher + * + * @param text The exact text to match. + */ + TextMatcher(const QString &text) + { + _pat = text; + _curs = _pat.begin(); + } + + /** Get the text size + * + * @return The number of characters in the fully-matched text. + */ + int size() const + { + return _pat.count(); + } + + /** Determine if this matcher has succeeded. + * + * @return True if this matcher has matched all characters. + */ + bool matched() const + { + return (_curs == _pat.end()); + } + + /** Reset this matcher to the beginning. + */ + void reset() + { + _curs = _pat.begin(); + } + + /** Attempt to add a character to this matcher. + * + * @param chr The character to attempt to accept. + * @return True if this new character is accepted into the matcher, whether + * or not the full match is completed. + * If already matched, then a new character is never accepted. + * @post If this matcher did not accept the character, the state is reset. + * @sa matched() + */ + bool addChar(const QChar &chr) + { + if (matched()) { + return false; + } + if (*_curs == chr) { + ++_curs; + return true; + } else { + reset(); + return false; + } + } + + private: + /// Exact text to match + QString _pat; + /// Current test cursor + QString::const_iterator _curs; +}; + +/** A single-row state machine for parsing CSV data. + * Each instance of this class handles exactly one row of decoding. + */ +class FieldMachine +{ + public: + enum State { + StartField, + UnquotedField, + QuotedField, + QuotedFirstQuote, + EndOfRow, + }; + + /// Create the new row extractor + FieldMachine() : keepQuotes(false), state(StartField) + { + } + + /// Clean up resources + ~FieldMachine() + { + qDeleteAll(eol); + } + + /** Attempt to add a new character into the parser state machine. + * + * @param chr The new character from CSV data. + * @return True if the character resulted in the end of a field string + * in #field. + * @throw CsvReader::FileError If the new character is invalid. + */ + bool addChar(const QChar &chr) + { + switch (state) { + case StartField: + if (chr == sep) { + // may be blank/empty + return true; + } + // Only first character of field determines quote status + else if (chr == quote) { + state = QuotedField; + if (keepQuotes) { + field.append(quote); + } + } else { + state = UnquotedField; + // Re-process in new state + return addChar(chr); + } + return false; + + case UnquotedField: + if (chr == sep) { + state = StartField; + // Separator is dropped + return true; + } else { + field.append(chr); + // EOL text is kept in unquoted field until full + // match + return checkEol(chr, false); + } + break; + + case QuotedField: + if (chr == quote) { + state = QuotedFirstQuote; + } else { + // Any other character is part of the field + field.append(chr); + } + return false; + + case QuotedFirstQuote: + // Two quotes decode into one + if (chr == quote) { + state = QuotedField; + field.append(quote); + return false; + } else { + if (keepQuotes) { + field.append(quote); + } + if (chr == sep) { + state = StartField; + // Separator is dropped + return true; + } + + state = UnquotedField; + // Re-process in new state + return addChar(chr); + } + break; + + case EndOfRow: + throw CsvReader::FileError("Characters after end-of-row"); + } + + return false; + } + + /** Determine if #field is fully valid. + * + * @return True if the field has been completed. + */ + bool fieldEnd() const + { + switch (state) { + case StartField: + case EndOfRow: + return true; + + default: + return false; + } + } + + /** Determine if the entire row is finished. + * + * @return True if no more characters are accepted. + */ + bool rowEnd() const + { + return (state == EndOfRow); + } + + /// Field-separator character + QChar sep; + /// Quotation character + QChar quote; + /// Determine whether start/end quotation characters are kept + bool keepQuotes; + /// End-of-row alternatives. Objects are owned by this object. + QList eol; + + /// State machine + State state; + /// Field string accumulator + QString field; + + private: + /** Read the new character into the EOL checker. + * + * @param chr The character to read. + * @param require If true, then one of the EOL strings must accept the new + * character. + * @return The matcher object, if any of the EOL strings matched fully + * at the current character. + * @throw CsvReader::FileError If the EOL is required but the new character + * was not accepted. + * @post If returned non-null, the #state is reset to EndOfRow and the + */ + bool checkEol(const QChar &chr, bool require) + { + bool anyAccept = false; + for (QList::iterator it = eol.begin(); it != eol.end(); + ++it) { + if ((**it).addChar(chr)) { + anyAccept = true; + } + } + + if (require && !anyAccept) { + throw CsvReader::FileError("Bad or missing end-of-row"); + } + + for (QList::const_iterator it = eol.begin(); it != eol.end(); + ++it) { + if ((**it).matched()) { + state = EndOfRow; + field.chop((**it).size()); + return true; + } + } + return false; + } +}; +} + +CsvReader::CsvReader(const QString &fileName) : _ownFile(true), _nominalColumnCount(-1) +{ + _defaultOpts(); + + // Destructor is not called if constructor throws + QScopedPointer file(new QFile(fileName)); + + if (!file->open(QIODevice::ReadOnly)) { + throw FileError(QString("Failed to open \"%1\" for reading: %2") + .arg(fileName, file->errorString())); + } + _dev = file.take(); +#if defined(CSV_USE_TEXT_STREAM) + _str.setDevice(_dev); +#endif +} + +CsvReader::CsvReader(QIODevice &device) : _ownFile(false), _nominalColumnCount(-1) +{ + _defaultOpts(); + + if (!device.isOpen()) { + if (!device.open(QIODevice::ReadOnly)) { + QFile *qFile = qobject_cast(&device); + if (qFile != NULL) { + throw FileError(QString("Failed to open \"%1\" for reading: %2") + .arg(qFile->fileName()) + .arg(device.errorString())); + } else { + throw FileError(QString("Failed to open for reading: %1") + .arg(device.errorString())); + } + } + } + _dev = &device; +#if defined(CSV_USE_TEXT_STREAM) + _str.setDevice(_dev); +#endif +} + +CsvReader::~CsvReader() +{ + if (_ownFile) { + delete _dev; + } +} + +void CsvReader::setCharacters(const QChar &separator, const QChar "e) +{ + if (separator == quote) { + throw std::logic_error("Cannot use same character for quote and separator"); + } + _sep = separator; + _quote = quote; +} + +bool CsvReader::atEnd() const +{ +#if defined(CSV_USE_TEXT_STREAM) + return _str.atEnd(); +#else + return _dev->atEnd(); +#endif +} + +void CsvReader::setValidateLineEnding(bool to) +{ + _validateLineEnding = to; +} + +void CsvReader::setLineEndingString(const QString &to) +{ + _lineEnding = to; +} + +void CsvReader::_defaultOpts() +{ + _sep = ','; + _quote = '"'; + _keepQuotes = false; + _validateLineEnding = false; + _lineEnding = "\r\n"; + _fieldTrim = false; +} + +QStringList CsvReader::readRow() +{ + if (atEnd()) { + throw FileError("Attempt to read past end of CSV file"); + } + + FieldMachine process; + process.sep = _sep; + process.quote = _quote; + process.keepQuotes = _keepQuotes; + process.eol << new TextMatcher(_lineEnding); + if (!_validateLineEnding) { + process.eol << new TextMatcher("\n"); + } + + QStringList fields; + QChar chr; + + if (_nominalColumnCount > 0) + fields.reserve(_nominalColumnCount); + while (true) { +#if defined(CSV_USE_TEXT_STREAM) + _str >> chr; + switch (_str.status()) { + case QTextStream::Ok: + process.addChar(chr); + break; + + case QTextStream::ReadPastEnd: + // Insert trailing EOL if necessary + if (process.state == FieldMachine::QuotedField) { + throw FileError(QString("End of file within quoted field")); + } + foreach(const QChar &chrVal, _lineEnding) + { + process.addChar(chrVal); + } + break; + + default: + throw FileError(QString("Failed to read line from file: %1") + .arg(_str.device()->errorString())); + } +#else + if (_dev->atEnd()) { + // Insert trailing EOL if necessary + if (process.state == FieldMachine::QuotedField) { + throw FileError(QString("End of file within quoted field")); + } + foreach(const QChar &chr, _lineEnding) + { + process.addChar(chr); + } + } else { + char c; + if (_dev->getChar(&c)) { + chr = char(c); + process.addChar(chr); + } else { + throw FileError(QString("Failed to read line from file: %1") + .arg(_dev->errorString())); + } + } +#endif + + if (process.fieldEnd()) { + // qDebug() << "OUT" << process.field; + fields.append(process.field); + process.field.clear(); + } + + if (process.rowEnd()) { + break; + } + } + + if (_fieldTrim) { + for (QStringList::iterator it = fields.begin(); it != fields.end(); ++it) { + *it = it->trimmed(); + } + } + + return fields; +} diff --git a/src/ratcommon/CsvReader.h b/src/ratcommon/CsvReader.h new file mode 100644 index 0000000..580f97d --- /dev/null +++ b/src/ratcommon/CsvReader.h @@ -0,0 +1,153 @@ +// + +#ifndef CSV_READER_H +#define CSV_READER_H + +#include +#include +#include +#include +#include + +// Handle UTF-8 properly +#define CSV_USE_TEXT_STREAM + +/** Read files per comma separated value format of RFC-4180. + * Optional file properties are non-standard separator and quotation characters. + */ +class CsvReader +{ + public: + /** Any error associated with reading a CSV file. + */ + class FileError : public std::runtime_error + { + public: + FileError(const QString &msg) : runtime_error(msg.toStdString()) + { + } + }; + + /** Open a file for reading upon construction. + * @param fileName The name of the file to open for the lifetime of the + * CsvReader object. + * @throw FileError if file cannot be opened. + */ + CsvReader(const QString &fileName); + + /** Bind the reader to a given input device, opening it if necessary. + * @param device The device to read from. + * The lifetime of the device must be longer than the CsvReader to + * avoid a dangling pointer. + * @throw FileError if the device is not readable. + */ + CsvReader(QIODevice &device); + + /// Clean up after the file + virtual ~CsvReader(); + + /** Use a non-standard separator or quotation character. + * @param separator The field separator character. + * @param quote The field quotation character. + * @throw std::logic_error If the characters are the same. + */ + void setCharacters(const QChar &separator, const QChar "e); + + /** Set strict enforcement of a specific line ending. + * @param validate True if validation should be performed. + */ + void setValidateLineEnding(bool validate); + + /** Set the line ending for strict validation. Default is \n + * @param end Ending to expect. + */ + void setLineEndingString(const QString &to); + + /** Determine if whitespace at start and end of fields should be removed + * by this reader. + * This trimming occurs after all standard CSV processing. + * + * @param trim If true, field strings will be trimmed by the reader. + */ + void setFieldsTrimmed(bool trim) + { + _fieldTrim = trim; + } + + /** Keep quotes at the edges of the CSV fields. + * @param keep If true, will keep field-surrounding quotes in field text. + */ + void setKeepQuotes(bool keep) + { + _keepQuotes = keep; + } + + /** Determine if the end-of-file has been reached. + * If this is true, then readRow will always fail. + * + * The conditions for being at CSV end-of-file are: + * - The source device is no longer readable + * - The source device is itself at EOF + * + * A sequential device may never bet at EOF and so the CSV stream will + * never at end on a sequential source device (i.e. QFile on stdin). In + * this case readRow() will raise an exception if no source data is + * available. + * + * @return True if readRow() should not be used. + */ + bool atEnd() const; + + /** Read a list of elements from a row in the file. + * + * @return The list of fields present in the next row of the file. + * If keepQuotes is enabled, then any quotation of the text data is kept + * in the CSV fields. + * @throw FileError if file cannot be read from. + */ + QStringList readRow(); + + /** + * Set the nominal column count to speed up readRow. Note that + * true number of columns still driven by file contents; this just controls + * Qlist::reserve() in readRow(). + * + * @param to The new column count. + */ + void setNominalColumnCount(int to) + { + _nominalColumnCount = to; + } + + private: + /** Set control options to defaults. + */ + void _defaultOpts(); + + /// Inserted between values + QChar _sep; + /// Surround values to be quoted + QChar _quote; + + /// Keep quotes. Off by default. + bool _keepQuotes; + /// Strict line ending validation. + bool _validateLineEnding; + /// Trim field strings + bool _fieldTrim; + + /// Line ending to check. + QString _lineEnding; + + /// Set to true if this object owns the IO device + bool _ownFile; + /// Underlying IO device + QIODevice *_dev; +#if defined(CSV_USE_TEXT_STREAM) + /// Underlying input stream + QTextStream _str; +#endif + int _nominalColumnCount; +}; + +#endif // CSV_READER_H diff --git a/src/ratcommon/CsvWriter.cpp b/src/ratcommon/CsvWriter.cpp new file mode 100644 index 0000000..51a42f4 --- /dev/null +++ b/src/ratcommon/CsvWriter.cpp @@ -0,0 +1,124 @@ +// + +#include +#include +#include "CsvWriter.h" + +const CsvWriter::EndRow CsvWriter::endr = CsvWriter::EndRow(); + +CsvWriter::CsvWriter(const QString &fileName) : _ownFile(true), _colI(0) +{ + _defaultOpts(); + + // Destructor is not called if constructor throws + QScopedPointer file(new QFile(fileName)); + if (!file->open(QIODevice::WriteOnly)) { + throw FileError(QString("Failed to open \"%1\" for writing: %2") + .arg(fileName, file->errorString())); + } + _str.setDevice(file.take()); + _str.setCodec("UTF-8"); +} + +CsvWriter::CsvWriter(QIODevice &device) : _ownFile(false), _colI(0) +{ + _defaultOpts(); + + if (!device.isOpen()) { + if (!device.open(QIODevice::WriteOnly)) { + throw FileError(QString("Failed to open for writing: %1") + .arg(device.errorString())); + } + } + _str.setDevice(&device); +} + +CsvWriter::~CsvWriter() +{ + if (_colI > 0) { + writeEndRow(); + } + if (_ownFile) { + delete _str.device(); + } +} + +void CsvWriter::setCharacters(const QChar &separator, const QChar "e) +{ + if (separator == quote) { + throw std::logic_error("Cannot use same character for quote and separator"); + } + _quotedChars.remove(_sep); + _quotedChars.remove(_quote); + _sep = separator; + _quote = quote; + _quotedChars.insert(_sep); + _quotedChars.insert(_quote); +} + +void CsvWriter::writeRow(const QStringList &elements) +{ + foreach(const QString &elem, elements) + { + writeRecord(elem); + } + writeEndRow(); +} + +void CsvWriter::writeRecord(const QString &rec) +{ + // Escape the text if necessary + QString text = rec; + + bool doQuote = false; + if (_quotedCols.contains(_colI)) { + doQuote = true; + } else if (!_quotedExpr.isEmpty() && (_quotedExpr.indexIn(text) >= 0)) { + doQuote = true; + } else { + foreach(const QChar &val, _quotedChars) + { + if (text.contains(val)) { + doQuote = true; + break; + } + } + } + + if (doQuote) { + // Escape quote with an extra quote + text.replace(_quote, QString("%1%1").arg(_quote)); + // Wrap with quote characters + text = QString("%1%2%1").arg(_quote).arg(text); + } + + // Insert separator character if necessary + if (_colI > 0) { + text.push_front(_sep); + } + _write(text); + ++_colI; +} + +void CsvWriter::writeEndRow() +{ + _write(_eol); + _colI = 0; +} + +void CsvWriter::_defaultOpts() +{ + _sep = ','; + _quote = '"'; + _eol = "\r\n"; + _quotedChars << _sep << _quote << '\r' << '\n'; +} + +void CsvWriter::_write(const QString &text) +{ + _str << text; + if (_str.status() != QTextStream::Ok) { + throw FileError( + QString("Failed to write output: %1").arg(_str.device()->errorString())); + } +} diff --git a/src/ratcommon/CsvWriter.h b/src/ratcommon/CsvWriter.h new file mode 100644 index 0000000..4992d6a --- /dev/null +++ b/src/ratcommon/CsvWriter.h @@ -0,0 +1,152 @@ +// + +#ifndef CSV_WRITER_H +#define CSV_WRITER_H + +#include "ratcommon_export.h" +#include +#include +#include +#include +#include + +/** Write files per comma separated value format of RFC-4180. + * Optional file properties are non-standard separator, quotation characters, + * and end-of-line string. + */ +class RATCOMMON_EXPORT CsvWriter +{ + /// Empty type for EOL placeholder + struct EndRow {}; + + public: + /** Any error associated with writing a CSV file. + */ + class FileError : public std::runtime_error + { + public: + FileError(const QString &msg) : runtime_error(msg.toStdString()) + { + } + }; + + /** Open the file for reading upon construction. + * @param fileName The name of the file to open for the lifetime of the + * CsvWriter object. + * @throw FileError if file cannot be opened. + */ + CsvWriter(const QString &fileName); + + /** Bind the writer to a given output device, opening it if necessary. + * @param device The device to write to. + * The lifetime of the device must be longer than the CsvWriter to + * avoid a dangling pointer. + * @throw FileError if the device is not writable. + */ + CsvWriter(QIODevice &device); + + /** Close the file if constructed with the fileName argument. + * If necessary, an end-of-row marker is written. + */ + ~CsvWriter(); + + /** Use a non-standard separator or quotation character. + * @param separator The field separator character. + * @param quote The field quotation character. + * @throw std::logic_error If the characters are the same. + */ + void setCharacters(const QChar &separator, const QChar "e); + + /** Set a static list of which columns should be unconditionally quoted. + * @param cols Each value is a column index to be quoted. + */ + void setQuotedColumns(const QSet &cols) + { + _quotedCols = cols; + } + + /** Define a non-standard definition of when to quote a CSV field. + * The standard is to quote if a quote, separator, or EOL is encountered. + */ + void setQuotedMatch(const QRegExp ®ex) + { + _quotedExpr = regex; + } + + /** Read a list of elements from a row in the file. + * @param records The list of records to write. + * @throw FileError if file write fails. + * @post All of the elements and an end-of-row marker are written to the stream. + */ + void writeRow(const QStringList &records); + + /** Write a single record to the CSV stream. + * When all records in a row are written, writeEndRow() should be called. + * @param record The element to write + * @throw FileError if file write fails. + */ + void writeRecord(const QString &record); + + /** Write the end-of-row indicator and start a new row. + * @pre Some number of records should be written with writeRecord(). + */ + void writeEndRow(); + + /// Placeholder for finishing row writes + static const EndRow endr; + + /** Write a single element to the CSV stream. + * @param record The element to write + * @return The modified CSV stream. + */ + CsvWriter &operator<<(const QString &record) + { + writeRecord(record); + return *this; + } + + /** Write and end-of-row indicator and start a new row. + * @return The modified CSV stream. + */ + CsvWriter &operator<<(const EndRow &) + { + writeEndRow(); + return *this; + } + + private: + /** Set control options to defaults. + * @post The #_sep, #_quote, and #_eol characters are set to RFC defaults. + */ + void _defaultOpts(); + + /** Write data to the device and verify status. + * @param text The text to write. + * @throw FileError if a problem occurs. + */ + void _write(const QString &text); + + /// Inserted between values + QChar _sep; + /// Surround values to be quoted + QChar _quote; + /// Appended to end row + QByteArray _eol; + + /// List of strings which, if contained, will cause the value to be quoted + QSet _quotedChars; + /// List of column indices (zero-indexed) to be quoted unconditionally + QSet _quotedCols; + /// Expression used to determine which values to quote + QRegExp _quotedExpr; + + /// Set to true if this object owns the IO device + bool _ownFile; + /// Underlying output stream + QTextStream _str; + + /// The current column index (starting at zero) + int _colI; +}; + +#endif // CSV_WRITER_H diff --git a/src/ratcommon/EnvironmentFlag.cpp b/src/ratcommon/EnvironmentFlag.cpp new file mode 100644 index 0000000..8e4ae59 --- /dev/null +++ b/src/ratcommon/EnvironmentFlag.cpp @@ -0,0 +1,36 @@ +// + +#include "EnvironmentFlag.h" +#include +#include +#include + +EnvironmentFlag::EnvironmentFlag(const std::string &name) : _name(name) +{ + _mutex.reset(new std::mutex()); +} + +EnvironmentFlag::~EnvironmentFlag() +{ +} + +const std::string &EnvironmentFlag::value() +{ + readValue(); + return *_value; +} + +bool EnvironmentFlag::operator()() +{ + readValue(); + return !(_value->empty()); +} + +void EnvironmentFlag::readValue() +{ + std::lock_guard lock(*_mutex); + if (!_value) { + const auto val = ::qgetenv(_name.data()); + _value.reset(new std::string(val.toStdString())); + } +} diff --git a/src/ratcommon/EnvironmentFlag.h b/src/ratcommon/EnvironmentFlag.h new file mode 100644 index 0000000..b871797 --- /dev/null +++ b/src/ratcommon/EnvironmentFlag.h @@ -0,0 +1,55 @@ +// + +#ifndef CPOBG_SRC_CPOCOMMON_ENVIRONMENTFLAG_H_ +#define CPOBG_SRC_CPOCOMMON_ENVIRONMENTFLAG_H_ + +//#include "cpocommon_export.h" +#include +#include + +namespace std +{ +class mutex; +} + +/** Extract an environment variable one time and cache. + * The flag is considered set if it is defined to any non-empty value. + */ +class /*CPOCOMMON_EXPORT*/ EnvironmentFlag +{ + public: + /** Define the flag name. + * + * @param name The name to read from the environment. + */ + EnvironmentFlag(const std::string &name); + + /// Allow forward declarations + ~EnvironmentFlag(); + + /** Get the raw value. + * + * This function is thread safe. + * @return The environment data. + */ + const std::string &value(); + + /** Get the flag state. + * + * This function is thread safe. + * @return True if the flag is set. + */ + bool operator()(); + + private: + /** Read and cache the value. + * @post The #_value is set. + */ + void readValue(); + + std::string _name; + std::unique_ptr _mutex; + std::unique_ptr _value; +}; + +#endif /* CPOBG_SRC_CPOCOMMON_ENVIRONMENTFLAG_H_ */ diff --git a/src/ratcommon/ExceptionSafeCoreApp.cpp b/src/ratcommon/ExceptionSafeCoreApp.cpp new file mode 100644 index 0000000..3af8bdf --- /dev/null +++ b/src/ratcommon/ExceptionSafeCoreApp.cpp @@ -0,0 +1,68 @@ +// + +#include "ExceptionSafeCoreApp.h" +#include "afclogging/Logging.h" + +namespace +{ +/// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "ExceptionSafeCoreApp") +} + +ExceptionSafeCoreApp::ExceptionSafeCoreApp(int &argc, char **argv) : QCoreApplication(argc, argv) +{ +} + +bool ExceptionSafeCoreApp::notify(QObject *obj, QEvent *eventVal) +{ + // Cache the meta-object for error logging, in case the object is deleted + const QMetaObject *const mObj = obj->metaObject(); + try { + return QCoreApplication::notify(obj, eventVal); + } catch (std::exception &e) { + logError(obj, mObj, eventVal, e.what()); + } catch (...) { + logError(obj, mObj, eventVal, "Unknown exception"); + } + + return false; +} + +void ExceptionSafeCoreApp::logError(const QObject *obj, + const QMetaObject *mObj, + const QEvent *eventVal, + const QString &msg) const +{ + QString targetObj; + if (obj) { + targetObj = QString("0x%1").arg(quint64(obj), 0, 16); + } else { + targetObj = "NULL"; + } + QString targetClass; + if (mObj) { + targetClass = mObj->className(); + } else { + targetClass = "QObject"; + } + + QString eType; + if (eventVal) { + eType = QString::number(eventVal->type()); + } else { + eType = "UNKNOWN"; + } + + LOGGER_ERROR(logger) << QString("Failed sending event type %1 to %2(%3): %4") + .arg(eType) + .arg(targetClass, targetObj) + .arg(msg); +} + +int ExceptionSafeCoreApp::exec() +{ + LOGGER_INFO(logger) << "Entering event loop"; + const int status = QCoreApplication::exec(); + LOGGER_INFO(logger) << "Finished event loop"; + return status; +} diff --git a/src/ratcommon/ExceptionSafeCoreApp.h b/src/ratcommon/ExceptionSafeCoreApp.h new file mode 100644 index 0000000..9c1d1c1 --- /dev/null +++ b/src/ratcommon/ExceptionSafeCoreApp.h @@ -0,0 +1,44 @@ +// + +#ifndef SRC_RATCOMMON_EXCEPTIONSAFECOREAPP_H_ +#define SRC_RATCOMMON_EXCEPTIONSAFECOREAPP_H_ + +#include "ratcommon_export.h" +#include + +/** Provide a QCoreApplication override which intercepts exceptions in + * event handlers and logs error messages. + */ +class RATCOMMON_EXPORT ExceptionSafeCoreApp : public QCoreApplication +{ + Q_OBJECT + public: + /** No extra behavior in constructor. + * + * @param argc The number of command arguments. + * @param argv The command arguments. + */ + ExceptionSafeCoreApp(int &argc, char **argv); + + /// Interface for QCoreApplication + virtual bool notify(QObject *obj, QEvent *event); + + /** Log an error message. + * + * @param obj The object associated with the error. + * @param mObj The meta-object associated with @c obj. + * @param event The event associated with the error. + * @param msg The error message. + */ + void logError(const QObject *obj, + const QMetaObject *mObj, + const QEvent *event, + const QString &msg) const; + + /** Overload to provide status messages. + * @return The exit code of the QCoreApplication. + */ + static int exec(); +}; + +#endif /* SRC_RATCOMMON_EXCEPTIONSAFECOREAPP_H_ */ diff --git a/src/ratcommon/FileHelpers.cpp b/src/ratcommon/FileHelpers.cpp new file mode 100644 index 0000000..16dfbd1 --- /dev/null +++ b/src/ratcommon/FileHelpers.cpp @@ -0,0 +1,71 @@ +// + +#include "FileHelpers.h" +#include "afclogging/ErrStream.h" +#include +#include +#include + +std::unique_ptr FileHelpers::open(const QString &name, QIODevice::OpenMode mode) +{ + std::unique_ptr file(new QFile(name)); + if (!file->open(mode)) { + throw Error(QString("Error opening file \"%1\" in mode %2: %3") + .arg(name) + .arg(mode) + .arg(file->errorString())); + } + return file; +} + +std::unique_ptr FileHelpers::openWithParents(const QString &name, QIODevice::OpenMode mode) +{ + ensureParents(QFileInfo(name)); + return open(name, mode); +} + +void FileHelpers::ensureParents(const QFileInfo &fileName) +{ + if (fileName.exists()) { + return; + } + + ensureExists(fileName.absoluteDir()); +} + +void FileHelpers::ensureExists(const QDir &path) +{ + if (path.exists()) { + return; + } + + const QString fullPath = path.absolutePath(); + if (!QDir::root().mkpath(fullPath)) { + throw Error(QString("Failed to create path \"%1\"").arg(fullPath)); + } +} + +void FileHelpers::remove(const QFileInfo &filePath) +{ + QFile file(filePath.absoluteFilePath()); + if (!file.remove()) { + throw Error(ErrStream() << "Failed to remove \"" << file.fileName() + << "\": " << file.errorString()); + } +} + +void FileHelpers::removeTree(const QDir &root) +{ + QDir dir(root); + for (const auto &dirInfo : dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) { + removeTree(dirInfo.absoluteFilePath()); + } + for (const auto &fileInfo : dir.entryInfoList(QDir::Files | QDir::Hidden | QDir::System)) { + FileHelpers::remove(fileInfo); + } + + if (!dir.rmdir(dir.absolutePath())) { + throw Error(ErrStream() + << "Failed to remove directory \"" << dir.absolutePath() << "\""); + } +} diff --git a/src/ratcommon/FileHelpers.h b/src/ratcommon/FileHelpers.h new file mode 100644 index 0000000..b6c3f79 --- /dev/null +++ b/src/ratcommon/FileHelpers.h @@ -0,0 +1,68 @@ +// + +#ifndef SRC_RATCOMMON_FILEHELPERS_H_ +#define SRC_RATCOMMON_FILEHELPERS_H_ + +#include +#include +#include + +class QFile; +class QString; +class QFileInfo; +class QDir; + +namespace FileHelpers +{ + +/// Error indicating file system issue +struct Error : public std::runtime_error { + Error(const QString &msg) : runtime_error(msg.toStdString()) + { + } +}; + +/** Open a file for reading or writing. + * + * @param name The full file name to open. + * @param mode The mode to open as. + * @return The newly opened file. + * @throw FileError If the file fails to open. + */ +std::unique_ptr open(const QString &name, QIODevice::OpenMode mode); + +/** Open a file for reading or writing, creating parent paths as necessary. + * + * @param name The full file name to open. + * @param mode The mode to open as. + * @return The newly opened file. + * @throw FileError If the file fails to open. + */ +std::unique_ptr openWithParents(const QString &name, QIODevice::OpenMode mode); + +/** Ensure that parent directories of a file exist, creating as necessary. + * + * @param fileName The absolute file path to require. + * @throw Error If parents cannot be created. + */ +void ensureParents(const QFileInfo &fileName); + +void ensureExists(const QDir &path); + +/** Remove a single file entry. + * + * @param filePath The path to remove. + * @throw Error If the file cannot be removed. + */ +void remove(const QFileInfo &filePath); + +/** Remove a directory tree recursively. + * + * @param root The root of the tree to remove, which is also removed. + * @throw Error If the tree cannot be fully removed. + */ +void removeTree(const QDir &root); + +} // End namespace + +#endif /* SRC_CPOCOMMON_FILEHELPERS_H_ */ diff --git a/src/ratcommon/GzipCsv.cpp b/src/ratcommon/GzipCsv.cpp new file mode 100644 index 0000000..8d855f5 --- /dev/null +++ b/src/ratcommon/GzipCsv.cpp @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2022 Broadcom. All rights reserved. + * The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate + * that owns the software below. + * This work is licensed under the OpenAFC Project License, a copy of which is + * included with this software program. + */ + +#include +#include "GzipCsv.h" +#include "FileHelpers.h" +#include +#include +#include + +namespace +{ +// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "GzipCsv") + +} // end namespace + +GzipCsv::GzipCsv(const std::string &filename) +{ + if (filename.empty()) { + return; + } + LOGGER_INFO(logger) << "Opening '" << QString::fromStdString(filename) << "'"; + QString qfilename = QString::fromStdString(filename); + _fileWriter = FileHelpers::open(qfilename, QIODevice::WriteOnly); + _gzipWriter.reset(new GzipStream(_fileWriter.get())); + if (!_gzipWriter->open(QIODevice::WriteOnly)) { + throw std::runtime_error( + QString("Gzip \"%1\" failed to open").arg(qfilename).toStdString()); + } + _csvWriter.reset(new CsvWriter(*_gzipWriter)); +} + +GzipCsv::operator bool() const +{ + return (bool)_fileWriter; +} + +void GzipCsv::clearRow() +{ + for (auto &colDef : _columns) { + colDef->resetValue(); + } +} + +void GzipCsv::completeRow() +{ + if (!*this) { + return; + } + if (!_headingWritten) { + _headingWritten = true; + for (auto &colDef : _columns) { + _csvWriter->writeRecord(colDef->name()); + } + _csvWriter->writeEndRow(); + } + for (auto &colDef : _columns) { + _csvWriter->writeRecord(colDef->formatValue()); + } + clearRow(); + _csvWriter->writeEndRow(); +} + +void GzipCsv::writeRow(const std::vector &columns) +{ + for (const auto &col : columns) { + _csvWriter->writeRecord(QString::fromStdString(col)); + } + _csvWriter->writeEndRow(); +} + +void GzipCsv::addColumn(ColBase *column) +{ + _columns.push_back(column); +} + +/////////////////////////////////////////////////////////////////////////////// + +GzipCsv::ColBase::ColBase(GzipCsv *container, const std::string &name) : + _name(QString::fromStdString(name)) +{ + container->addColumn(this); +} + +void GzipCsv::ColBase::checkSet() const +{ + if (_valueSet) { + return; + } + throw std::runtime_error(QString("Attempt to read value from column \"%1\" that was not " + "set yet") + .arg(_name) + .toStdString()); +} + +/////////////////////////////////////////////////////////////////////////////// + +GzipCsv::ColInt::ColInt(GzipCsv *container, const std::string &name) : ColBase(container, name) +{ +} + +QString GzipCsv::ColInt::formatValue() const +{ + return isValueSet() ? QString::number(_value) : QString(); +} + +/////////////////////////////////////////////////////////////////////////////// + +GzipCsv::ColDouble::ColDouble(GzipCsv *container, + const std::string &name, + const std::string &format) : + ColBase(container, name), _format(format) +{ +} + +QString GzipCsv::ColDouble::formatValue() const +{ + if (!isValueSet()) { + return QString(); + } + if (!_format.empty()) { + return QString::asprintf(_format.c_str(), _value); + } + return QString::fromStdString(boost::lexical_cast(_value)); +} + +/////////////////////////////////////////////////////////////////////////////// + +GzipCsv::ColStr::ColStr(GzipCsv *container, const std::string &name) : ColBase(container, name) +{ +} + +QString GzipCsv::ColStr::formatValue() const +{ + return isValueSet() ? QString::fromStdString(_value) : QString(); +} + +/////////////////////////////////////////////////////////////////////////////// + +GzipCsv::ColBool::ColBool(GzipCsv *container, + const std::string &name, + const std::vector &tf) : + ColBase(container, name) +{ + assert(tf.size() == 2); + for (const auto &s : tf) { + _tf.push_back(QString::fromStdString(s)); + } +} + +QString GzipCsv::ColBool::formatValue() const +{ + return isValueSet() ? _tf[_value ? 0 : 1] : QString(); +} + +/////////////////////////////////////////////////////////////////////////////// + +GzipCsv::ColEnum::ColEnum(GzipCsv *container, + const std::string &name, + const std::map &items, + const std::string &defName) : + ColBase(container, name), _defName(QString::fromStdString(defName)) +{ + for (const auto &kvp : items) { + _items[kvp.first] = QString::fromStdString(kvp.second); + } +} + +QString GzipCsv::ColEnum::formatValue() const +{ + if (!isValueSet()) { + return QString(); + } + const auto &it = _items.find(_value); + return ((it == _items.end()) ? _defName : it->second) + QString::asprintf(" (%d)", _value); +} diff --git a/src/ratcommon/GzipCsv.h b/src/ratcommon/GzipCsv.h new file mode 100644 index 0000000..e324926 --- /dev/null +++ b/src/ratcommon/GzipCsv.h @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2022 Broadcom. All rights reserved. + * The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate + * that owns the software below. + * This work is licensed under the OpenAFC Project License, a copy of which is + * included with this software program. + */ + +/** @file + * GZIPped CSV writer that separate field definition from their formatting and + * ordering. + * + * Each CSV format (set of columns and their types) has its own class: + * #include "GzipCsv.h" + * //// Declaration + * class FooCsv : public GzipCsv { + * public: + * FooCsv(const std::string &filename); + * + * // Declaring CSV columns - in filing order + * ColDouble bar; + * ColStr baz; + * + * FooCsv(const std::string &filename) + * : GzipCsv(filename), + * // Defining names and formattings of columns + * bar(this, "BAR", "12f"), baz(this, "BAZ") + * {} + * }; + * ... + * //// Use + * // Initialization. Empty name means nothing will be written + * fooCsv = FooCsv(file_name); + * ... + * // Optional check if writer was activated (initialized with nonempty name) + * if (fooCsv) { + * // Setting column values in any order + * fooCsv.baz = "a string"; + * fooCsv.bar = 57.179; + * // Writing row with values previously set (and resetting values) + * fooCsv.writeRow(); + * } + */ + +#ifndef GZIP_CSV_H +#define GZIP_CSV_H + +#include "CsvWriter.h" +#include "GzipStream.h" +#include +#include +#include +#include +#include +#include +#include + +/** Base class for CSV GZIP writers. + * Defines initialization/writing logic, but does not define specific columns - + * they should be defined in derived classes. Also servers as a namespace for + * column classes (to make type names shorter in declaration of derived classes) + */ +class GzipCsv : private boost::noncopyable +{ + public: + /////////////////////////////////////////////////////////////////////////// + // CLASSES FOR COLUMNS + /////////////////////////////////////////////////////////////////////////// + + /** Abstract base class for columns. + * Defines general management, but leavse value storing and formatting to + * derived classes + */ + class ColBase : private boost::noncopyable + { + public: + /** Default virtual destructor */ + virtual ~ColBase() = default; + + /** True if column value was set */ + bool isValueSet() const + { + return _valueSet; + } + + /** Mark column value as not set */ + void resetValue() + { + _valueSet = false; + } + + protected: + /** Constructor + * @param container GzipCsv-derived object that stores column values + * @param name Column name + */ + ColBase(GzipCsv *container, const std::string &name); + + /** Mark column as set (assigned value) */ + void markSet() + { + _valueSet = true; + } + + /** Column name */ + const QString &name() const + { + return _name; + } + + /** Returns column value formatted for putting to CSV. "" if not set + */ + virtual QString formatValue() const = 0; + + /** Raise exception if value not set */ + void checkSet() const; + + private: + friend class GzipCsv; + + // INSTANCE DATA + + const QString _name; /*!< Column name */ + bool _valueSet = false; /*!< Column value set */ + }; + + /////////////////////////////////////////////////////////////////////////// + + /** Class for integer columns */ + class ColInt : public ColBase + { + public: + /** Constructor + * @param container GzipCsv-derived object that stores column values + * @param name Column name + */ + ColInt(GzipCsv *container, const std::string &name); + + /** Setting field value */ + ColInt &operator=(int value) + { + markSet(); + _value = value; + return *this; + } + + /** Returning field value (assert if not set) */ + int value() const + { + checkSet(); + return _value; + } + + protected: + /** Returns column value formatted for putting to CSV. "" if not set + */ + virtual QString formatValue() const; + + private: + // INSTANCE DATA + + int _value; /*!< Column value (for current row) */ + }; + + /////////////////////////////////////////////////////////////////////////// + + /** Class for floating point columns */ + class ColDouble : public ColBase + { + public: + /** Constructor + * @param container GzipCsv-derived object that stores column values + * @param name Column name + * @pasram format Printf-style format. Empty to show at maximum + * precision + */ + ColDouble(GzipCsv *container, + const std::string &name, + const std::string &format = ""); + + /** Setting field value */ + ColDouble &operator=(double value) + { + markSet(); + _value = value; + return *this; + } + + /** Returning field value (assert if not set) */ + double value() const + { + checkSet(); + return _value; + } + + protected: + /** Returns column value formatted for putting to CSV. "" if not set + */ + virtual QString formatValue() const; + + private: + // INSTANCE DATA + + double _value; /*!< Column value (for current row) */ + std::string _format; /*!< Printf-style format. Empty for maximum + precision */ + }; + + /////////////////////////////////////////////////////////////////////////// + + /** Class for string columns */ + class ColStr : public ColBase + { + public: + /** Constructor + * @param container GzipCsv-derived object that stores column values + * @param name Column name + */ + ColStr(GzipCsv *container, const std::string &name); + + /** Setting field value */ + ColStr &operator=(const std::string &value) + { + markSet(); + _value = value; + return *this; + } + ColStr &operator=(char value) + { + markSet(); + _value = value; + return *this; + } + + /** Returning field value (assert if not set) */ + const std::string &value() const + { + checkSet(); + return _value; + } + + protected: + /** Returns column value formatted for putting to CSV. "" if not set + */ + virtual QString formatValue() const; + + private: + // INSTANCE DATA + + std::string _value; /*!< Column value (for current row) */ + }; + + /////////////////////////////////////////////////////////////////////////// + + /** Class for bool columns */ + class ColBool : public ColBase + { + public: + /** Constructor + * @param container GzipCsv-derived object that stores column values + * @param name Column name + * @param tf Column values for true and false + */ + ColBool(GzipCsv *container, + const std::string &name, + const std::vector &tf = {"True", "False"}); + + /** Setting field value */ + ColBool &operator=(bool value) + { + markSet(); + _value = value; + return *this; + } + + /** Returning field value (assert if not set) */ + bool value() const + { + checkSet(); + return _value; + } + + protected: + /** Returns column value formatted for putting to CSV */ + virtual QString formatValue() const; + + private: + // INSTANCE DATA + + bool _value; /*!< Column value (for current row) */ + std::vector _tf; /*!< Column values for true and false */ + }; + + /////////////////////////////////////////////////////////////////////////// + + /** Class for enum columns */ + class ColEnum : public ColBase + { + public: + /** Constructor + * @param container GzipCsv-derived object that stores column values + * @param name Column name + * @param items Enum item descriptors + * @param defName Name for unknown item + */ + ColEnum(GzipCsv *container, + const std::string &name, + const std::map &items, + const std::string &defName = "Unknown"); + + /** Setting field value */ + ColEnum &operator=(int value) + { + markSet(); + _value = value; + return *this; + } + + /** Returning field value (assert if not set) */ + int value() const + { + checkSet(); + return _value; + } + + protected: + /** Returns column value formatted for putting to CSV */ + virtual QString formatValue() const; + + private: + // INSTANCE DATA + + int _value; /*!< Column value (for current row) */ + std::map _items; /*!< Item descriptors */ + QString _defName; /* &columns); + + private: + friend class ColBase; + + /** Append reference to column to vector of columns */ + void addColumn(ColBase *column); + + // INSTANCE DATA + + /** True if heading row have been written */ + bool _headingWritten = false; + + /** File writer, used by gzip writer */ + std::unique_ptr _fileWriter; + + /** GZIP writer used by CSV writer */ + std::unique_ptr _gzipWriter; + + /** CSV writer */ + std::unique_ptr _csvWriter; + + /** Vector of columns */ + std::vector _columns; +}; +#endif /* GZIP_CSV_H */ diff --git a/src/ratcommon/GzipStream.cpp b/src/ratcommon/GzipStream.cpp new file mode 100644 index 0000000..c98321a --- /dev/null +++ b/src/ratcommon/GzipStream.cpp @@ -0,0 +1,336 @@ +// + +#include "GzipStream.h" + +namespace +{ +/// uncompressed buffer size +const int gzBufSize = 8192; + +QIODevice::OpenMode maskRw(QIODevice::OpenMode mode) +{ + return mode & (QIODevice::ReadOnly | QIODevice::WriteOnly); +} +} + +GzipStream::GzipStream(QIODevice *dev, QObject *parentVal) : + QIODevice(parentVal), _dev(nullptr), _pos(0) +{ + setSourceDevice(dev); +} + +GzipStream::~GzipStream() +{ + close(); +} + +void GzipStream::setSourceDevice(QIODevice *dev) +{ + close(); + + if (_dev) { + disconnect(_dev, nullptr, this, nullptr); + } + _dev = dev; + if (_dev) { + connect(_dev, &QIODevice::readyRead, this, &QIODevice::readyRead); + connect(_dev, + &QIODevice::readChannelFinished, + this, + &QIODevice::readChannelFinished); + } +} + +void GzipStream::setCompressParams(const CompressParams ¶ms) +{ + _params = params; +} + +bool GzipStream::open(OpenMode mode) +{ + if (isOpen()) { + setErrorString("GzipStream file already open"); + return false; + } + + if (!_dev) { + setErrorString("No underlying device"); + return false; + } + if (!_dev->isOpen()) { + setErrorString(QString("Underlying device not open")); + return false; + } + + const auto rwmode = maskRw(mode); + if (rwmode == NotOpen) { + setErrorString("Can only open for either reading or writing"); + return false; + } + if (!openStream(rwmode)) { + return false; + } + + QIODevice::open(mode); + if (rwmode == QIODevice::ReadOnly) { + emit readyRead(); + } + return true; +} + +bool GzipStream::isSequential() const +{ + return true; +} + +qint64 GzipStream::bytesAvailable() const +{ + return QIODevice::bytesAvailable() + _readBuf.size(); +} + +bool GzipStream::atEnd() const +{ + if (!_dev) { + return true; + } + return QIODevice::atEnd() && _readBuf.isEmpty() && _dev->atEnd(); +} + +qint64 GzipStream::pos() const +{ + // subtract read-but-buffered size + return _pos - QIODevice::bytesAvailable() + QIODevice::bytesToWrite(); +} + +bool GzipStream::seek(qint64 posVal) +{ + Q_UNUSED(posVal); + return false; +} + +bool GzipStream::reset() +{ + if (!isOpen()) { + return false; + } + // short circuit if already at start + if (pos() == 0) { + return true; + } + + const auto mode = openMode(); + const auto rwmode = maskRw(mode); + QIODevice::close(); + closeStream(rwmode); + if (!_dev->reset()) { + return false; + } + openStream(rwmode); + QIODevice::open(mode); + if (rwmode == QIODevice::ReadOnly) { + emit readyRead(); + } + return true; +} + +void GzipStream::close() +{ + if (!isOpen()) { + return; + } + + const auto rwmode = maskRw(openMode()); + QIODevice::close(); + closeStream(rwmode); +} + +qint64 GzipStream::readData(char *data, qint64 maxSize) +{ + // Populate the read buffer as much as possible + while ((_readBuf.size() < maxSize) && !_dev->atEnd()) { + readSource(_params.bufSize); + } + + const qint64 copySize = std::min(maxSize, qint64(_readBuf.size())); + ::memcpy(data, _readBuf.data(), copySize); + // truncate remaining + _readBuf = _readBuf.mid(copySize); + + if (copySize < maxSize) { + emit readChannelFinished(); + } + _pos += copySize; + return copySize; +} +qint64 GzipStream::writeData(const char *data, qint64 dataSize) +{ + // zlib can only write chunks sized by "uInt" and Qt copy by "int" + const qint64 maxChunkSize = std::numeric_limits::max(); + + for (qint64 startIx = 0; startIx < dataSize; startIx += maxChunkSize) { + const qint64 chunkSize = std::min(dataSize - startIx, maxChunkSize); + // zlib requires non-const input buffer + QByteArray bufPlain(data + startIx, chunkSize); + + _stream.next_in = reinterpret_cast(bufPlain.data()); + _stream.avail_in = uInt(chunkSize); + if (writeOut(false) < 0) { + return -1; + } + } + + emit bytesWritten(dataSize); + _pos += dataSize; + return dataSize; +} + +bool GzipStream::closeStream(OpenMode rwmode) +{ + _pos = 0; + _readBuf.clear(); + + int status; + switch (rwmode) { + case ReadOnly: + status = ::inflateEnd(&_stream); + break; + + case WriteOnly: + _stream.next_in = nullptr; + _stream.avail_in = 0; + writeOut(true); + status = ::deflateEnd(&_stream); + break; + + default: + return false; + } + if (status != Z_OK) { + setError(); + return false; + } + + return true; +} + +bool GzipStream::openStream(OpenMode rwmode) +{ + _pos = 0; + _readBuf.clear(); + + _stream.zalloc = Z_NULL; + _stream.zfree = Z_NULL; + _stream.opaque = Z_NULL; + + int status; + switch (rwmode) { + case ReadOnly: + if (!_dev->isReadable()) { + setErrorString(QString("Underlying device not readable")); + return false; + } + status = ::inflateInit2(&_stream, _params.windowBits); + if (status != Z_OK) { + setError(); + return false; + } + if (readSource(_params.bufSize) < 0) { + ::inflateEnd(&_stream); + return false; + } + break; + + case WriteOnly: + if (!_dev->isWritable()) { + setErrorString(QString("Underlying device not writable")); + return false; + } + status = ::deflateInit2(&_stream, + _params.compressLevel, + Z_DEFLATED, + _params.windowBits, + _params.memLevel, + Z_DEFAULT_STRATEGY); + if (status != Z_OK) { + setError(); + return false; + } + break; + + default: + return false; + } + + return true; +} + +qint64 GzipStream::writeOut(bool finish) +{ + const int flush = finish ? Z_FINISH : Z_NO_FLUSH; + + QByteArray bufComp(_params.bufSize, Qt::Uninitialized); + qint64 totalOut = 0; + do { + _stream.next_out = reinterpret_cast(bufComp.data()); + _stream.avail_out = bufComp.size(); + + const int status = ::deflate(&_stream, flush); + switch (status) { + case Z_OK: + case Z_STREAM_END: + // Either status is acceptable pending on avail_out + break; + default: + setError(); + return -1; + } + + const qint64 have = bufComp.size() - _stream.avail_out; + if (_dev->write(bufComp.data(), have) != have) { + setErrorString("Failed to write device"); + return -1; + } + totalOut += have; + } while (_stream.avail_out == 0); + + return totalOut; +} + +qint64 GzipStream::readSource(qint64 sizeVal) +{ + QByteArray bufComp = _dev->read(sizeVal); + if (bufComp.isEmpty()) { + return 0; + } + _stream.next_in = reinterpret_cast(bufComp.data()); + _stream.avail_in = bufComp.size(); + + QByteArray bufPlain(_params.bufSize, Qt::Uninitialized); + qint64 totalIn = 0; + do { + _stream.next_out = reinterpret_cast(bufPlain.data()); + _stream.avail_out = bufPlain.size(); + + const int status = ::inflate(&_stream, Z_NO_FLUSH); + switch (status) { + case Z_OK: + case Z_STREAM_END: + // Either status is acceptable pending on avail_out + break; + default: + setError(); + return -1; + } + + const qint64 have = bufPlain.size() - _stream.avail_out; + _readBuf += QByteArray(bufPlain.data(), have); + totalIn += have; + } while (_stream.avail_out == 0); + + return totalIn; +} + +void GzipStream::setError() +{ + setErrorString(QString::fromUtf8(_stream.msg)); +} diff --git a/src/ratcommon/GzipStream.h b/src/ratcommon/GzipStream.h new file mode 100644 index 0000000..bc466f7 --- /dev/null +++ b/src/ratcommon/GzipStream.h @@ -0,0 +1,141 @@ +// +#ifndef GZIP_FILE_H +#define GZIP_FILE_H + +#include +#include + +/** Represent a gzip encoded data stream with QIODevice interface. + * + * The "source" QIODevice is the compressed side of the file access, while + * the GzipStream is the uncompressed side. + * This device does not allow the size() to be pre-computed, but it does + * accumulate total data read/written in the form of pos(). In this way + * reading to the end of the file tells the total size. + */ +class GzipStream : public QIODevice +{ + Q_OBJECT + public: + /// Optional parameters for the compressed stream + struct CompressParams { + /** Compression level control between 0 and 9 inclusive. + */ + int compressLevel = 6; + /** Window size in number of bits between 8 and 15 inclusive. + * Add 16 to this value to write a gzip stream. + */ + int windowBits = MAX_WBITS + 16; + /** Memory use control between 0 and 9 inclusive. + */ + int memLevel = 8; + /** The size of compressed data buffer for accessing the underlying + * device. + */ + int bufSize = 10240; + }; + + /** Create a new device object which wraps a source device. + * @param dev The source device to read from or write to. + */ + explicit GzipStream(QIODevice *dev = nullptr, QObject *parent = nullptr); + + /** Finalize and close the gzip stream. + * @post The source device + */ + virtual ~GzipStream(); + + /** Set the underlying device to use for compressed data. + * + * @param dev The device to read/write compressed. + * @post This device is closed. + */ + void setSourceDevice(QIODevice *dev); + + /** Set parameters used for write mode (compression). + * This is only used during open() so should be called before open(). + * + * @param params The parameters to use when writing. + */ + void setCompressParams(const CompressParams ¶ms); + + /** Interface for QIODevice. + * Open the gzip stream for reading or writing (but not both). + * + * @pre The source device must already be open with the same mode. + * @param mode The mode to open, either ReadOnly or WriteOnly. + * @return True if the stream is opened. + */ + virtual bool open(OpenMode mode) override; + + /// Interface for QIODevice + virtual bool isSequential() const override; + /// Interface for QIODevice + virtual qint64 bytesAvailable() const override; + /// Interface for QIODevice + virtual bool atEnd() const override; + /// Interface for QIODevice + virtual qint64 pos() const override; + /// Interface for QIODevice + virtual bool seek(qint64 pos) override; + /// Interface for QIODevice + virtual bool reset() override; + + public slots: + /** Interface for QIODevice. + * Close this device and the source device. + * @post Any write stream is finalized and flushed. + */ + virtual void close() override; + + protected: + /// File read accessor required by QIODevice. + virtual qint64 readData(char *data, qint64 maxSize) override; + /// File write accessor required by QIODevice. + virtual qint64 writeData(const char *data, qint64 maxSize) override; + + private: + /** Internal function to close the stream independent of this QIODevice. + * + * @param mode The mode that the stream was opened with. + * @return True iff successful. + * @post If returning false sets this errorString() + */ + bool closeStream(OpenMode mode); + /** Internal function to open the stream independent of this QIODevice. + * + * @param mode The mode to open the stream as. + * @return True iff successful. + * @pre The #_params must be set. + * @post If returning false sets this errorString() + */ + bool openStream(OpenMode mode); + /** Encode and write part of the stream. + * + * @pre The input buffer of #_stream is defined. + * @param finish True if this is the last write in the stream. + * @return The number of compressed octets written. + */ + qint64 writeOut(bool finish); + /** Read and decode part of the stream. + * + * @param size The desired size of compressed data to read. + * @return The number of plaintext octets read. + */ + qint64 readSource(qint64 size); + /// Relay any error from the socket to this device + void setError(); + + /// Underlying compressed device + QIODevice *_dev; + /// Count of plaintext octets read/written to current #_dev + qint64 _pos; + /// Set parameters + CompressParams _params; + /// Streaming state + z_stream _stream; + /// Read overflow buffer + QByteArray _readBuf; +}; + +#endif // GZIP_FILE_H diff --git a/src/ratcommon/PostSet.cpp b/src/ratcommon/PostSet.cpp new file mode 100644 index 0000000..1a15756 --- /dev/null +++ b/src/ratcommon/PostSet.cpp @@ -0,0 +1,15 @@ +// + +#include "PostSet.h" +#include "afclogging/Logging.h" + +namespace +{ +/// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "PostSet") +} + +void logPostSetError(const char *msg) +{ + LOGGER_ERROR(logger) << "Failed in assignment: " << (msg ? msg : "non-std::exception type"); +} diff --git a/src/ratcommon/PostSet.h b/src/ratcommon/PostSet.h new file mode 100644 index 0000000..fa89011 --- /dev/null +++ b/src/ratcommon/PostSet.h @@ -0,0 +1,106 @@ +// + +#ifndef SRC_RATCOMMON_POSTSET_H_ +#define SRC_RATCOMMON_POSTSET_H_ + +#include "ratcommon_export.h" +#include + +/** Log an error condition. + * + * @param msg The exception message. + */ +RATCOMMON_EXPORT +void logPostSetError(const char *msg); + +/** A class to overwrite a variable upon destruction. + * This enforces a postcondition upon a lexical scope. + * + * Example of use: + * @code + * struct SomeC : public BaseC{ + * int val; + * // At end of call, val is guaranteed to be 20 + * // thatFunc() or otherFunc() may modify val temporarily + * int someThing(){ + * PostSet ps(val, 20); + * if(foo){ + * return thatFunc(); + * } + * return otherFunc(); + * } + * }; + * @endcode + * @tparam Type The type of value to set. + */ +template +class PostSet +{ + public: + /** Bind to a variable to overwrite and value to write with. + * @param var The variable to overwrite. + * Its lifetime must be at least as long as this object + * @param val The value to set when this object is destroyed. + */ + PostSet(Type &var, const Type &val) : _var(var), _val(val) + { + } + + /** Bind to a variable and initialize with a new value. + * @param var The variable to overwrite. + * Its lifetime must be at least as long as this object + * @param pre The value to set immediately. + * @param post The value to set when this object is destroyed. + */ + PostSet(Type &var, const Type &pre, const Type &post) : _var(var), _val(post) + { + _var = pre; + } + + /// Perform the write + ~PostSet() + { + try { + _var = _val; + } catch (std::exception &err) { + logPostSetError(err.what()); + } catch (...) { + logPostSetError(nullptr); + } + } + + private: + /// The variable to overwrite + Type &_var; + /// A copy of the value to set + Type _val; +}; + +/** Helper function to define a post-setter only final state. + * + * @tparam Type The value type to set. + * @param var The variable to overwrite. + * Its lifetime must be at least as long as this object + * @param post The value to set when this object is destroyed. + */ +template +std::unique_ptr> make_PostSet(Type &var, const Type &post) +{ + return std::unique_ptr>(new PostSet(var, post)); +} + +/** Helper function to define a post-setter with initial state. + * + * @tparam Type The value type to set. + * @param var The variable to overwrite. + * Its lifetime must be at least as long as this object + * @param pre The value to set immediately. + * @param post The value to set when this object is destroyed. + */ +template +std::unique_ptr> make_PostSet(Type &var, const Type &pre, const Type &post) +{ + return std::unique_ptr>(new PostSet(var, pre, post)); +} + +#endif /* SRC_RATCOMMON_POSTSET_H_ */ diff --git a/src/ratcommon/RatVersion.cpp.in b/src/ratcommon/RatVersion.cpp.in new file mode 100644 index 0000000..e26b7fc --- /dev/null +++ b/src/ratcommon/RatVersion.cpp.in @@ -0,0 +1,14 @@ +/// @copyright +#include "RatVersion.h" + +QString RatVersion::versionName(){ + return QString("@PROJECT_VERSION@"); +} + +QString RatVersion::revisionName(){ + return QString("@SVN_LAST_REVISION@"); +} + +QString RatVersion::combinedName(){ + return QString("%1-%2").arg(versionName(), revisionName()); +} diff --git a/src/ratcommon/RatVersion.h.in b/src/ratcommon/RatVersion.h.in new file mode 100644 index 0000000..65f3ec9 --- /dev/null +++ b/src/ratcommon/RatVersion.h.in @@ -0,0 +1,35 @@ +/// @copyright +#ifndef SRC_RATCOMMON_RATVERSION_H_ +#define SRC_RATCOMMON_RATVERSION_H_ + +#include "ratcommon_export.h" +#include + +/// The RAT version at build time +#define RAT_BUILD_VERSION_NAME "@PROJECT_VERSION@" + +namespace RatVersion{ + +/** Get a text name for this version of CPO. + * The version is of the form "X.Y.Z". + * @return The version text. + */ +RATCOMMON_EXPORT +QString versionName(); + +/** Get a text name for this SCM revision of CPO. + * The version is of the form "XXXM". + * @return The revision text. + */ +RATCOMMON_EXPORT +QString revisionName(); + +/** Get a text name for this version/revision of CPO. + * @return The combined version and revision text. + */ +RATCOMMON_EXPORT +QString combinedName(); + +} // End RatVersion + +#endif /* SRC_RATCOMMON_RATVERSION_H_ */ diff --git a/src/ratcommon/SearchPaths.cpp b/src/ratcommon/SearchPaths.cpp new file mode 100644 index 0000000..206372f --- /dev/null +++ b/src/ratcommon/SearchPaths.cpp @@ -0,0 +1,160 @@ +// + +#include +#include +#include +#include "SearchPaths.h" +#include "afclogging/Logging.h" + +namespace +{ +/// Logger for all instances of class +LOGGER_DEFINE_GLOBAL(logger, "SearchPaths") + +/** A convenience class to perform multiple path extensions. + */ +class Extender +{ + public: + /** Create a new extender. + * + * @param suffix The suffix to append. If non-empty, the suffix itself + * will be forced to start with a path separator. + */ + Extender(const QString &suffix) : _suffix(suffix) + { + if (!_suffix.isEmpty()) { + _suffix.prepend(QDir::separator()); + } + } + + /** Append the application-specific suffix to the search paths. + */ + QString operator()(const QString &base) const + { + return QDir::toNativeSeparators(base + _suffix); + } + + private: + /// Common suffix for paths + QString _suffix; +}; +} + +bool SearchPaths::init(const QString &pathSuffix) +{ + const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + const Extender extend(pathSuffix); + + QStringList configPaths; + QStringList dataPaths; +#if defined(Q_OS_WIN) + { + const QString var = env.value("LOCALAPPDATA"); + for (const auto &path : var.split(QDir::listSeparator(), QString::SkipEmptyParts)) { + const auto extendedPath = extend(path); + configPaths.append(extendedPath); + dataPaths.append(extendedPath); + } + } +#endif + for (const auto &path : + QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation)) { + const auto extendedPath = extend(path); + if (configPaths.isEmpty() || (configPaths.last() != extendedPath)) { + configPaths.append(extendedPath); + } + } + for (const auto &path : + QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation)) { + const auto extendedPath = extend(path); + if (dataPaths.isEmpty() || (dataPaths.last() != extendedPath)) { + dataPaths.append(extendedPath); + } + } + + LOGGER_DEBUG(logger) << "Using config paths: " << configPaths.join(" "); + LOGGER_DEBUG(logger) << "Using data paths: " << dataPaths.join(" "); + + QDir::setSearchPaths("config", configPaths); + QDir::setSearchPaths("data", dataPaths); + return true; +} + +namespace +{ +/** Determine if a full path is writable. + * @param path The path to check. + * @return True if the path itself exists and is writable, or if the + * longest existing parent directory is writable. + */ +bool canWrite(const QString &path) +{ + const QFileInfo pathInfo(path); + if (pathInfo.exists()) { + return pathInfo.isWritable(); + } else { + return canWrite(pathInfo.absolutePath()); + } +} +} + +QStringList SearchPaths::allPaths(const QString &prefix, const QString &fileName) +{ + QStringList fullPaths; + foreach(const QString &path, QDir::searchPaths(prefix)) + { + const QDir testDir(path); + const QString fullPath( + QDir::toNativeSeparators(testDir.absoluteFilePath(fileName))); + fullPaths.append(fullPath); + } + return fullPaths; +} + +QString SearchPaths::forWriting(const QString &prefix, const QString &fileName) +{ + foreach(const QString &path, QDir::searchPaths(prefix)) + { + const QDir testDir(path); + const QString fullPath( + QDir::toNativeSeparators(testDir.absoluteFilePath(fileName))); + const bool finished = canWrite(fullPath); + LOGGER_DEBUG(logger) << "forWriting " << prefix << " \"" << fileName << "\" is " + << finished << " at " << fullPath; + if (finished) { + return fullPath; + } + } + + LOGGER_WARN(logger) << "No forWriting path found under \"" << prefix << "\" with name \"" + << fileName << "\""; + return QString(); +} + +QString SearchPaths::forReading(const QString &prefix, const QString &fileName, bool required) +{ + const QStringList searchList = QDir::searchPaths(prefix); + foreach(const QString &path, searchList) + { + const QDir testDir(path); + const QFileInfo fullPath( + QDir::toNativeSeparators(testDir.absoluteFilePath(fileName))); + const bool finished = fullPath.exists(); + LOGGER_DEBUG(logger) << "forReading " << prefix << " \"" << fileName << "\" is " + << finished << " at " << fullPath.absoluteFilePath(); + if (finished) { + return fullPath.absoluteFilePath(); + } + } + + if (required) { + throw std::runtime_error(QString("No path found for \"%1\" with name \"%2\"") + .arg(prefix, fileName) + .toStdString()); + } + + LOGGER_WARN(logger) << "No forReading path found for \"" << prefix << "\" with name \"" + << fileName << "\""; + return QString(); +} diff --git a/src/ratcommon/SearchPaths.h b/src/ratcommon/SearchPaths.h new file mode 100644 index 0000000..943fb81 --- /dev/null +++ b/src/ratcommon/SearchPaths.h @@ -0,0 +1,55 @@ +// + +#ifndef SEARCH_PATHS_H +#define SEARCH_PATHS_H + +#include + +/** Utilities to locate configuration files in the file system. + * The SearchPaths::init() function is not thread safe, since it writes global + * data. The other functions are thread safe since they are read-only. + */ +namespace SearchPaths +{ +/** Set up the search path for config and data prefix. + * On windows, this uses the operating system standard paths for data. + * If defined, the environment variable LOCALAPPDATA will also be used + * for data search override on windows. + * + * On unix, this uses recommendations defined by: + * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + * + * @param pathSuffix This suffix is appended to each of the base search + * paths. + * @return True if the initialization succeeded. + */ +bool init(const QString &pathSuffix = QString()); + +/** Get the full ordered list of possible file paths. + * @param prefix The search prefix to use with QDir::searchPaths(). + * @param fileName The file path to append to each search path. + * @return All paths of that prefix type. + */ +QStringList allPaths(const QString &prefix, const QString &fileName); + +/** Get the first writable absolute file name in a search prefix. + * @param prefix The search prefix to use with QDir::searchPaths(). + * @param fileName The file path to append to each search path. + * @return The absolute path of the first writable search path, or + * an empty string if no search path is writable. + */ +QString forWriting(const QString &prefix, const QString &fileName); + +/** Get the first existing absolute file name in a search prefix. + * @param prefix The search prefix to use with QDir::searchPaths(). + * @param fileName The file path to append to each search path. + * @param required If true, then the lookup is manditory and a failure + * will throw an exception. + * @return The absolute path of the first search path which contains the + * desired file name, or an empty string if no path has the file. + * @throw std::runtime_error If the path is required but not present. + */ +QString forReading(const QString &prefix, const QString &fileName, bool required = false); +} + +#endif /* SEARCH_PATHS_H */ diff --git a/src/ratcommon/TextHelpers.cpp b/src/ratcommon/TextHelpers.cpp new file mode 100644 index 0000000..31482a4 --- /dev/null +++ b/src/ratcommon/TextHelpers.cpp @@ -0,0 +1,204 @@ +// + +#include "TextHelpers.h" +#include "afclogging/ErrStream.h" +#include +#include +#include +#include +#include +#include +#include + +int TextHelpers::toInt(const QString &text) +{ + bool valid; + const double val = text.toInt(&valid); + if (!valid) { + throw std::runtime_error(ErrStream() + << "Failed to convert '" << text << "' to integer"); + } + return val; +} + +double TextHelpers::toNumber(const QString &text) +{ + bool valid; + const double val = text.toDouble(&valid); + if (!valid) { + throw std::runtime_error(ErrStream() + << "Failed to convert '" << text << "' to double"); + } + return val; +} + +QString TextHelpers::toHex(int value, int digits) +{ + return QString("%1").arg(value, digits, 16, QChar('0')); +} + +int TextHelpers::fromHex(const QString &text) +{ + bool ok; + const int val = text.toInt(&ok, 16); + if (!ok) { + return -1; + } + return val; +} + +QString TextHelpers::toHex(const QByteArray &hash) +{ + QByteArray hex = hash.toHex(); + // Insert separators from back to front + for (int ix = hex.count() - 2; ix > 0; ix -= 2) { + hex.insert(ix, ':'); + } + // Guarantee case + return hex.toUpper(); +} + +namespace +{ +/// Choice of hexadecimal digits +const QString hexDigits("0123456789ABCDEF"); +/// Number of hexDigits chars +const int hexDigitCount = 16; +} + +QString TextHelpers::randomHexDigits(int digits) +{ + QString str; + str.resize(digits); + for (QString::iterator it = str.begin(); it != str.end(); ++it) { + *it = hexDigits.at(::rand() % hexDigitCount); + } + return str; +} + +QString TextHelpers::dateTimeFormat(const QChar &sep) +{ + return QString("dd-MMM-yyyy%1HH:mm:ssZ").arg(sep); +} + +QString TextHelpers::quoted(const QString &text) +{ + return QString("\"%1\"").arg(text); +} + +QStringList TextHelpers::quoted(const QStringList &text) +{ + QStringList result; + foreach(const QString &item, text) + { + result.append(quoted(item)); + } + return result; +} + +QString TextHelpers::peerName(const QAbstractSocket &socket) +{ + QString hostname = socket.peerName(); + if (hostname.isEmpty()) { + hostname = socket.peerAddress().toString(); + } + return QString("%1:%2").arg(hostname).arg(socket.peerPort()); +} + +QByteArray TextHelpers::operationName(const QNetworkReply &reply) +{ + switch (reply.operation()) { + case QNetworkAccessManager::HeadOperation: + return "HEAD"; + case QNetworkAccessManager::GetOperation: + return "GET"; + case QNetworkAccessManager::PutOperation: + return "PUT"; + case QNetworkAccessManager::PostOperation: + return "POST"; + case QNetworkAccessManager::DeleteOperation: + return "DELETE"; + default: + return reply.request() + .attribute(QNetworkRequest::CustomVerbAttribute) + .toByteArray(); + } +} + +QString TextHelpers::qstrerror() +{ +#if defined(Q_OS_WIN) + const int errnum = errno; + const size_t bufLen = 1024; + char buffer[bufLen]; + ::strerror_s(buffer, bufLen, errnum); + const char *stat = buffer; +#else + const int errnum = errno; + const size_t bufLen = 1024; + char buffer[bufLen]; + ::strerror_r(errnum, buffer, bufLen); + const char *const stat = buffer; +#endif + + const char *msg; + if (stat == NULL) { + msg = "Unknown"; + } else { + msg = stat; + } + return QString("(%1) %2").arg(errnum).arg(QString::fromLocal8Bit(msg)); +} + +namespace +{ +QString quoteAndEscape(const QString &in) +{ + QString out(in); + out.replace("\"", "\\\""); + return QString("\"%1\"").arg(out); +} +} + +QString TextHelpers::executableString(const QStringList &args, const QProcessEnvironment &env) +{ + QStringList envkeys = env.keys(); + qSort(envkeys); + QStringList envparts; + foreach(const QString &key, envkeys) + { + envparts << QString("%1=%2").arg(key, quoteAndEscape(env.value(key))); + } + + QStringList cmdparts; + foreach(const QString &arg, args) + { + cmdparts << quoteAndEscape(arg); + } + + return (envparts + cmdparts).join(" "); +} + +QString TextHelpers::combineLabelUnit(const QString &label, const QString &unit) +{ + if (unit.isEmpty()) { + return label; + } + return QString("%1 (%2)").arg(label, unit); +} + +QString TextHelpers::nonfiniteText(double value) +{ + // match default QString behavior + if (std::isnan(value)) { + return "nan"; + } + if (std::isinf(value)) { + if (value > 0) { + return "+inf"; + } else { + return "-inf"; + } + } + return QString(); +} diff --git a/src/ratcommon/TextHelpers.h b/src/ratcommon/TextHelpers.h new file mode 100644 index 0000000..dcdc69a --- /dev/null +++ b/src/ratcommon/TextHelpers.h @@ -0,0 +1,154 @@ +// + +#ifndef SRC_RATCOMMON_TEXTHELPERS_H_ +#define SRC_RATCOMMON_TEXTHELPERS_H_ + +//#include "ratcommon_export.h" +#include + +class QAbstractSocket; +class QNetworkReply; +class QProcessEnvironment; +class UtcDateTime; +class DateTimeInterval; + +/** Namespace for simple text conversion functions. + * Most convert to/from Qt string representations and throw RuntimeError to + * signal failures. + */ +namespace TextHelpers +{ + +/** Convert text to an integer value. + * + * @param text The text representing a number. + * @return The number represented. + * @throw RuntimeError if the text cannot be converted. + */ +// RATCOMMON_EXPORT +int toInt(const QString &text); + +/** Convert text to a floating point value. + * + * @param text The text representing a number. + * @return The number represented. + * @throw RuntimeError if the text cannot be converted. + */ +// RATCOMMON_EXPORT +double toNumber(const QString &text); + +/** Convert an integer to hexadecimal digits. + * + * @param value The value to convert. + * @param digits The required number of digits in the result. + * Unused digits will be padded with zeros. + * @return + */ +// RATCOMMON_EXPORT +QString toHex(int value, int digits = -1); + +/** Convert from hexadecimal string representation. + * + * @param text The string with no leading "0x" or any other characters. + * @return The number represented. + */ +// RATCOMMON_EXPORT +int fromHex(const QString &text); + +/** Convert a byte array into a printable hexadecimal string. + * The result is of the form "12:34:AB" where colons separate the octets, + * and all alpha characters are uppercase. + * This is the same form as used by the "openssl x509" command. + * + * @param hash The raw bytes of a data set (i.e. a fingerprint). + * @return The formatted hexadecimal string. + */ +// RATCOMMON_EXPORT +QString toHex(const QByteArray &hash); + +/** Construct a string with random hexadecimal digits. + * + * @param digits The number of base-16 digits to choose. + * @return The resulting string of length @c digits. + */ +// RATCOMMON_EXPORT +QString randomHexDigits(int digits); + +/** This string is used for QDateTime::toString() to consistently format + * QDateTime for display. + * @param sep The date--time separator character. + * The ISO-standard value here is 'T'. + * @return The display format string. + */ +// RATCOMMON_EXPORT +QString dateTimeFormat(const QChar &sep = QChar(' ')); + +/** Surround a string with quotation characters. + * @param text The text to quote. + * @return The quoted text. + */ +// RATCOMMON_EXPORT +QString quoted(const QString &text); + +/** Surround each element of a string list with quotation characters. + * @param text The text items to quote. + * @return The quoted text items. + */ +// RATCOMMON_EXPORT +QStringList quoted(const QStringList &text); + +/** Get a name for a socket connection. + * + * @param socket The socket to name. + * @return A combination of the socket's peer name and port. + */ +// RATCOMMON_EXPORT +QString peerName(const QAbstractSocket &socket); + +/** Get the human readable name for an HTTP operation. + * + * @param reply The reply to take the operation from. + * @return The name for the operation, in original character encoding. + */ +// RATCOMMON_EXPORT +QByteArray operationName(const QNetworkReply &reply); + +/** Retrieve the standard errno information in a thread safe way. + * @return The string associated with the local errno value. + * The format is "(N) STR" where N is the original error number. + */ +// RATCOMMON_EXPORT +QString qstrerror(); + +/** Generate a nicely formatted debug string corresponding with a child + * process invocation. + * + * @param args The full set of arguments to execute. + * The first @c args string is the executable itself. + * @param env The full environment of the child process. + * @return A string suitable for copy-paste into a bash terminal for diagnosis. + */ +// RATCOMMON_EXPORT +QString executableString(const QStringList &args, const QProcessEnvironment &env); + +/** Combine a display label and an optional value unit together. + * This simple function provides consistent use of unit labels. + * + * @param label The non-empty label for the value. + * @param unit A possibly-empty unit for the value. + * @return The combined representation. + */ +// RATCOMMON_EXPORT +QString combineLabelUnit(const QString &label, const QString &unit); + +/** Get text name for non-finite floating point values. + * + * @param value The non-finite value to get a name for. + * @return The human-readable name for the value. + */ +// RATCOMMON_EXPORT +QString nonfiniteText(double value); + +} + +#endif /* SRC_RATCOMMON_TEXTHELPERS_H_ */ diff --git a/src/ratcommon/ZipWriter.cpp b/src/ratcommon/ZipWriter.cpp new file mode 100644 index 0000000..89c77eb --- /dev/null +++ b/src/ratcommon/ZipWriter.cpp @@ -0,0 +1,235 @@ +// + +#include "ZipWriter.h" +#include "afclogging/ErrStream.h" +#include +#include +#include +#include + +namespace +{ +/// Largest allowed block size (for 64-bit operation) +const qint64 zipMaxBlock = std::numeric_limits::max(); +} + +class ZipWriter::Private +{ + public: + /// Underlying file object + zipFile file; + /// File comment text + QString comment; + /// Compression level to use for all content (valid range [0, 9]) + int compression = 0; + /** Pointer to child file, which is owned by this object. + * Underlying file access allows only one content file to be opened at a + * time and this enforces that behavior. + */ + ContentFile *openChild = nullptr; +}; + +/** Represent a file within a Zip archive. + * This interface behaves as a normal QIODevice. + * The parent ZipWriter must not be deleted while a ContentFile is used. + */ +class ZipWriter::ContentFile : public QIODevice +{ + Q_DISABLE_COPY(ContentFile) + public: + /** Open an internal file for reading. + * Use the given archive at the current file position. + * The openMode() flags are set to WriteOnly. + * @throw FileError if the internal file fails to open. + */ + ContentFile(ZipWriter &parent); + + /** Close the internal file. + */ + virtual ~ContentFile(); + + // QIODevice interfaces. None of these throw exceptions. + virtual bool isSequential() const override; + /// Restrict open modes + virtual bool open(QIODevice::OpenMode mode) override; + /// Flush the output + virtual void close() override; + /// Seeking is not allowed by the zip library + virtual bool seek(qint64 pos) override; + + protected: + /** File read accessor required by QIODevice. + * @throw FileError in all cases, this is a write-only device. + */ + virtual qint64 readData(char *data, qint64 maxSize) override; + /*** File write accessor required by QIODevice. + * @throw FileError if the write fails. + */ + virtual qint64 writeData(const char *data, qint64 maxSize) override; + + private: + /// Underlying file object + ZipWriter *_parent; + /// Allow only a single open call + bool _canOpen = true; +}; + +ZipWriter::ZipWriter(const QString &fileName, WriterMode mode, int compress) : _impl(new Private) +{ + _impl->compression = compress; + + int minizmode = 0; + switch (mode) { + case Overwrite: + minizmode = APPEND_STATUS_CREATE; + break; + case Append: + minizmode = APPEND_STATUS_ADDINZIP; + break; + } + _impl->file = zipOpen64(fileName.toUtf8().data(), minizmode); + if (_impl->file == nullptr) { + throw FileError(ErrStream() << "ZipWriter failed to open \"" << fileName << "\""); + } +} + +ZipWriter::~ZipWriter() +{ + // Close with comment + zipClose(_impl->file, _impl->comment.toUtf8().data()); +} + +void ZipWriter::setFileComment(const QString &comment) +{ + _impl->comment = comment; +} + +std::unique_ptr ZipWriter::openFile(const QString &intFileName, const QDateTime &modTime) +{ + if (_impl->openChild) { + throw std::logic_error("ZipWriter can only have one ContentFile instance at a " + "time"); + } + + // Default to current time + QDateTime fileTime = modTime; + if (fileTime.isNull()) { + fileTime = QDateTime::currentDateTime(); + } + + // Create file information + zip_fileinfo fileInfo; + // File time stamp + { + const auto date = fileTime.date(); + fileInfo.tmz_date.tm_year = date.year(); + fileInfo.tmz_date.tm_mon = date.month(); + fileInfo.tmz_date.tm_mday = date.day(); + } + { + const auto time = fileTime.time(); + fileInfo.tmz_date.tm_hour = time.hour(); + fileInfo.tmz_date.tm_min = time.minute(); + fileInfo.tmz_date.tm_sec = time.second(); + } + fileInfo.dosDate = 0; + // No attributes + fileInfo.internal_fa = 0; + fileInfo.external_fa = 0; + + // No comment or extra info, always use 64-bit mode + int status = zipOpenNewFileInZip64(_impl->file, + intFileName.toUtf8().data(), + &fileInfo, + nullptr, + 0, + nullptr, + 0, + nullptr, + Z_DEFLATED, + _impl->compression, + 1); + if (status != ZIP_OK) { + throw FileError(ErrStream() << "Failed to create internal file \"" << intFileName + << "\": " << status); + } + + std::unique_ptr child(new ContentFile(*this)); + child->open(QIODevice::WriteOnly); + return std::move(child); +} + +ZipWriter::ContentFile::ContentFile(ZipWriter &parentVal) : _parent(&parentVal) +{ + _parent->_impl->openChild = this; +} + +ZipWriter::ContentFile::~ContentFile() +{ + close(); + _parent->_impl->openChild = nullptr; +} + +bool ZipWriter::ContentFile::isSequential() const +{ + return true; +} + +bool ZipWriter::ContentFile::open(QIODevice::OpenMode mode) +{ + const QIODevice::OpenMode onlyFlags(QIODevice::WriteOnly | QIODevice::Text); + if (mode & ~onlyFlags) { + return false; + } + // The file is only allowed to be opened once by the constructor + // After being closed, it is closed permanently + if (!_canOpen) { + return false; + } + _canOpen = false; + + return QIODevice::open(mode); +} + +void ZipWriter::ContentFile::close() +{ + if (!isOpen()) { + return; + } + + QIODevice::close(); + const int status = zipCloseFileInZip(_parent->_impl->file); + if (status != ZIP_OK) { + setErrorString(QString("Failed to close internal file: %1").arg(status)); + return; + } +} + +bool ZipWriter::ContentFile::seek(qint64 posVal) +{ + Q_UNUSED(posVal); + return false; +} + +qint64 ZipWriter::ContentFile::readData(char *data, qint64 maxSize) +{ + Q_UNUSED(data); + Q_UNUSED(maxSize); + setErrorString("Cannot read this file"); + return -1; +} + +qint64 ZipWriter::ContentFile::writeData(const char *data, qint64 maxSize) +{ + // Clamp to maximum for zip interface + const int blockSize = std::min(zipMaxBlock, maxSize); + + // Status from this function is purely error code, assume entire block written + const int status = zipWriteInFileInZip(_parent->_impl->file, data, blockSize); + if (status < 0) { + setErrorString(ErrStream() << "Failed writing to zip file: " << status); + return -1; + } + emit bytesWritten(blockSize); + return blockSize; +} diff --git a/src/ratcommon/ZipWriter.h b/src/ratcommon/ZipWriter.h new file mode 100644 index 0000000..0b952dd --- /dev/null +++ b/src/ratcommon/ZipWriter.h @@ -0,0 +1,101 @@ +// + +#ifndef CPOBG_SRC_CPOCOMMON_ZIPWRITER_H_ +#define CPOBG_SRC_CPOCOMMON_ZIPWRITER_H_ + +#include +#include +#include +#include +#include + +/** Write to a Zip archive file. + * Files are written sequentially, one-at-a-time. + * All contained (internal) files are write-only and not able to seek. + */ +class ZipWriter +{ + Q_DISABLE_COPY(ZipWriter) + /// Private implementation storage class + class Private; + /// IODevice subclass + class ContentFile; + + public: + /** Any error associated with reading a Zip file is thrown as a FileError. + */ + class FileError : public std::runtime_error + { + public: + FileError(const std::string &msg) : runtime_error(msg) + { + } + }; + + /// Determine how to handle existing Zip files when writing. + enum WriterMode { + /// If file exists, overwrite any content with new Zip file + Overwrite, + /// If file exists, append new content to end of Zip (must ensure unique + /// filenames) + Append, + }; + /// Determine what type of compression to use for writing content + enum CompressionLevel { + /// Do not compress files, simply copy them into the Zip + CompressCopy = Z_NO_COMPRESSION, + /// Use minimal compression for speed + CompressFast = Z_BEST_SPEED, + /// Use maximum compression for size + CompressSmall = Z_BEST_COMPRESSION, + /// Use default compression + CompressDefault = Z_DEFAULT_COMPRESSION, + }; + + /** Open a given file for writing. + * @param fileName The name of the zip archive to open. + * @param mode Determines the behavior when writing to existing files. + * @param compress Sets the compression level for all files written to the + * archive. This is an integer in range [0, 9], and can use the enumerated + * values in CompressionLevel. + */ + ZipWriter(const QString &fileName, + WriterMode mode = Overwrite, + int compress = CompressDefault); + + /** Close the archive. + * This will also close any open content files. + */ + virtual ~ZipWriter(); + + /** Set the comment to be written to this file before it is closed. + * + * @param comment The comment text to write. + */ + void setFileComment(const QString &comment); + + /** Open a desired internal file for writing. + * @param intFileName The file name of the object within the Zip archive. + * @return A reference to an object which exists until the next call + * to openFile() or until the destruction of the parent ZipWriter. + * @param modTime The file modification time to write. + * The time zone of the date-time is ignored within the zip file. + * @throw FileError if the internal file fails to open. + * + * Use of this function should be similar to: + * try{ + * auto cFile = zip.openFile("file.name"); + * } + * catch(ZipWriter::FileError &e){ + * ... + * } + */ + std::unique_ptr openFile(const QString &intFileName, + const QDateTime &modTime = QDateTime()); + + private: + /// PIMPL instance + std::unique_ptr _impl; +}; + +#endif /* ZIP_FILE_H */ diff --git a/src/ratcommon/overload_cast.h b/src/ratcommon/overload_cast.h new file mode 100644 index 0000000..ba7824d --- /dev/null +++ b/src/ratcommon/overload_cast.h @@ -0,0 +1,35 @@ +// + +#ifndef OVERLOAD_CAST_H_ +#define OVERLOAD_CAST_H_ + +/** Shortcut for casting overloaded Qt signal/slot member functions. + * @tparam Args The desired argument(s). + */ +template +struct OVERLOAD { + /** Cast a member function. + * + * @tparam C The class being cast. + * @tparam R Member return type. + * @param pmf The member function to cast. + * @return The cast function, with specific @c Args arguments. + */ + template + static auto OF(R (C::*pmf)(Args...)) -> decltype(pmf) + { + return pmf; + } +}; + +/// Specialize to treat void as empty +template<> +struct OVERLOAD { + template + static auto OF(R (C::*pmf)()) -> decltype(pmf) + { + return pmf; + } +}; + +#endif /* OVERLOAD_CAST_H_ */ diff --git a/src/ratcommon/test/CMakeLists.txt b/src/ratcommon/test/CMakeLists.txt new file mode 100644 index 0000000..2e0b131 --- /dev/null +++ b/src/ratcommon/test/CMakeLists.txt @@ -0,0 +1,5 @@ +# All source files to same target +file(GLOB ALL_CPP "*.cpp") +add_gtest_executable(${TGT_NAME}-test ${ALL_CPP}) +target_link_libraries(${TGT_NAME}-test PRIVATE ${TGT_NAME}) +target_link_libraries(${TGT_NAME}-test PRIVATE gtest_main) diff --git a/src/ratcommon/test/TestEnvironmentFlag.cpp b/src/ratcommon/test/TestEnvironmentFlag.cpp new file mode 100644 index 0000000..373d8d1 --- /dev/null +++ b/src/ratcommon/test/TestEnvironmentFlag.cpp @@ -0,0 +1,35 @@ +#include "../EnvironmentFlag.h" + +class TestEnvironmentFlag : public testing::Test +{ + virtual void SetUp() override + { + ::qputenv("TEST_ENVIRONMENT_FLAG", QByteArray()); + } +}; + +TEST_F(TestEnvironmentFlag, testReadEmpty) +{ + EnvironmentFlag flag("TEST_ENVIRONMENT_FLAG"); + EXPECT_FALSE(flag()); + EXPECT_EQ(std::string(), flag.value()); +} + +TEST_F(TestEnvironmentFlag, testReadNonempty) +{ + ::qputenv("TEST_ENVIRONMENT_FLAG", QByteArray("0")); + EnvironmentFlag flag("TEST_ENVIRONMENT_FLAG"); + EXPECT_TRUE(flag()); + EXPECT_EQ(std::string("0"), flag.value()); +} + +TEST_F(TestEnvironmentFlag, testChangeValue) +{ + EnvironmentFlag flag("TEST_ENVIRONMENT_FLAG"); + EXPECT_FALSE(flag()); + EXPECT_EQ(std::string(), flag.value()); + + ::qputenv("TEST_ENVIRONMENT_FLAG", QByteArray("0")); + EXPECT_FALSE(flag()); + EXPECT_EQ(std::string(), flag.value()); +} diff --git a/src/ratcommon/test/TestPostSet.cpp b/src/ratcommon/test/TestPostSet.cpp new file mode 100644 index 0000000..ab047f3 --- /dev/null +++ b/src/ratcommon/test/TestPostSet.cpp @@ -0,0 +1,65 @@ +// + +#include "../PostSet.h" + +namespace +{ +class BadCls +{ + public: + int val = 0; + + BadCls(int init = 0) : val(init) + { + } + + BadCls &operator=(const BadCls &other) + { + if (other.val == 0) { + throw std::logic_error("never"); + } + val = other.val; + return *this; + } +}; +} + +TEST(TestPostSet, makeOneArg) +{ + int var = 0; + { + auto ps = make_PostSet(var, 2); + ASSERT_EQ(0, var); + } + ASSERT_EQ(2, var); +} + +TEST(TestPostSet, makeTwoArg) +{ + int var = 0; + { + auto ps = make_PostSet(var, 1, 2); + ASSERT_EQ(1, var); + } + ASSERT_EQ(2, var); +} + +TEST(TestPostSet, testPreException) +{ + BadCls var(-1); + ASSERT_EQ(-1, var.val); + + ASSERT_THROW(make_PostSet(var, BadCls(0), BadCls(1)), std::logic_error); + ASSERT_EQ(-1, var.val); +} + +TEST(TestPostSet, testPostException) +{ + BadCls var(-1); + ASSERT_EQ(-1, var.val); + { + auto ps = make_PostSet(var, BadCls(1), BadCls(0)); + ASSERT_EQ(1, var.val); + } + ASSERT_EQ(1, var.val); +} diff --git a/src/ratcommon/test/TestRatVersion.cpp b/src/ratcommon/test/TestRatVersion.cpp new file mode 100644 index 0000000..66943fd --- /dev/null +++ b/src/ratcommon/test/TestRatVersion.cpp @@ -0,0 +1,8 @@ +// + +#include "ratcommon/RatVersion.h" + +TEST(TestRatVersion, identicalVersion) +{ + EXPECT_EQ(QString::fromUtf8(RAT_BUILD_VERSION_NAME), RatVersion::versionName()); +} diff --git a/src/ratcommon/test/TestSearchPaths.cpp b/src/ratcommon/test/TestSearchPaths.cpp new file mode 100644 index 0000000..6b3d668 --- /dev/null +++ b/src/ratcommon/test/TestSearchPaths.cpp @@ -0,0 +1,180 @@ +// + +#include "../SearchPaths.h" +#include + +using namespace FileTestHelpers; + +namespace +{ + +void setTestEnv(const QString &locPath, const QString &sysPath) +{ +#if defined(Q_OS_LINUX) + ::qputenv("XDG_DATA_DIRS", sysPath.toLocal8Bit()); + ::qputenv("XDG_DATA_HOME", locPath.toLocal8Bit()); +#elif defined(Q_OS_WIN) + ::qputenv("LOCALAPPDATA", + QStringList({locPath, sysPath}).join(QDir::listSeparator()).toLocal8Bit()); +#else + #error Unknown OS define +#endif +} + +QStringList filterPathList(const QStringList &list) +{ +#if defined(Q_OS_LINUX) + return list; +#elif defined(Q_OS_WIN) + // Last four elements are fixed by Qt/OS + return list.mid(0, list.size() - 4); +#else + #error Unknown OS define +#endif +} + +} + +class TestSearchPaths : public testing::Test +{ + protected: + std::unique_ptr _app; + std::unique_ptr _tmp; + + /** Construct an absolute path with native separators. + * @param subPath The relative path. + * @return The corresponding absolute path. + */ + QString nativeAbsolutePath(const QString &subPath) const + { + if (!_tmp) { + throw std::logic_error("no tmp dir"); + } + return QDir::toNativeSeparators(_tmp->absoluteFilePath(subPath)); + } + + static void SetUpTestCase() + { + UnitTestHelpers::initLogging(); + } + + virtual void SetUp() + { + _app.reset(new UnitTestCoreApp()); + + _tmp.reset(new TestDir("paths")); + ASSERT_TRUE(_tmp->mkdir("system1")); + ASSERT_TRUE(makeFile(nativeAbsolutePath("system1/system.txt"), "system1")); + ASSERT_TRUE(makeFile(nativeAbsolutePath("system1/only1.txt"), "system1")); + ASSERT_TRUE(makeFile(nativeAbsolutePath("system1/read.txt"), "system1")); + ASSERT_TRUE(makeFile(nativeAbsolutePath("system1/write.txt"), "system1")); + ASSERT_TRUE(_tmp->mkdir("system2")); + ASSERT_TRUE(makeFile(nativeAbsolutePath("system2/system.txt"), "system2")); + ASSERT_TRUE(makeFile(nativeAbsolutePath("system2/only2.txt"), "system2")); + ASSERT_TRUE(makeFile(nativeAbsolutePath("system2/read.txt"), "system2")); + ASSERT_TRUE(makeFile(nativeAbsolutePath("system2/write.txt"), "system2")); + ASSERT_TRUE(_tmp->mkdir("local")); + ASSERT_TRUE(makeFile(nativeAbsolutePath("local/read.txt"), "local")); + } + virtual void TearDown() + { + _tmp.reset(); + _app.reset(); + } +}; + +TEST_F(TestSearchPaths, forReadingSystem) +{ + const QString sysPath = nativeAbsolutePath("system1"); + const QString locPath = nativeAbsolutePath("missing"); + + setTestEnv(locPath, sysPath); + ASSERT_TRUE(SearchPaths::init(QString())); + + // Only one path + ASSERT_EQ(filterPathList(QDir::searchPaths("data")), QStringList({locPath, sysPath})); + + ASSERT_EQ(content("data:system.txt"), QByteArray("system1")); + ASSERT_EQ(content("data:read.txt"), QByteArray("system1")); +} + +TEST_F(TestSearchPaths, forReadingSearchOrder) +{ + const QString sys1Path = nativeAbsolutePath("system1"); + const QString sys2Path = nativeAbsolutePath("system2"); + const QString locPath = nativeAbsolutePath("missing"); + + // System-2 gets priority + setTestEnv(locPath, QStringList({sys2Path, sys1Path}).join(QDir::listSeparator())); + ASSERT_TRUE(SearchPaths::init(QString())); + + // Priority order + ASSERT_EQ(filterPathList(QDir::searchPaths("data")), + QStringList({locPath, sys2Path, sys1Path})); + + ASSERT_EQ(content("data:only1.txt"), QByteArray("system1")); + ASSERT_EQ(content("data:only2.txt"), QByteArray("system2")); + ASSERT_EQ(content("data:system.txt"), QByteArray("system2")); + ASSERT_EQ(content("data:read.txt"), QByteArray("system2")); +} + +TEST_F(TestSearchPaths, forReadingLocal) +{ + const QString sysPath = nativeAbsolutePath("system1"); + const QString locPath = nativeAbsolutePath("local"); + + setTestEnv(locPath, sysPath); + ASSERT_TRUE(SearchPaths::init(QString())); + + // Local gets priority + ASSERT_EQ(filterPathList(QDir::searchPaths("data")), QStringList({locPath, sysPath})); + + ASSERT_EQ(content("data:system.txt"), QByteArray("system1")); + ASSERT_EQ(content("data:read.txt"), QByteArray("local")); +} + +TEST_F(TestSearchPaths, forWritingLocal) +{ + const QString sysPath = nativeAbsolutePath("system1"); + const QString locPath = nativeAbsolutePath("local"); + + setTestEnv(locPath, sysPath); + ASSERT_TRUE(SearchPaths::init(QString())); + + // Local gets priority + ASSERT_EQ(filterPathList(QDir::searchPaths("data")), QStringList({locPath, sysPath})); + + // When reading the file, the system is selected first + ASSERT_EQ(content("data:write.txt"), QByteArray("system1")); + // Writing based on search path opens the system file + { + QFile file("data:write.txt"); + file.open(QFile::WriteOnly); + // non-native separators generated by Qt + ASSERT_EQ(file.fileName(), _tmp->absoluteFilePath("system1/write.txt")); + } + // Must use custom function to open the correct path + { + QFile file(SearchPaths::forWriting("data", "write.txt")); + ASSERT_TRUE(file.open(QFile::WriteOnly)); + ASSERT_EQ(file.fileName(), _tmp->absoluteFilePath("local/write.txt")); + ASSERT_GT(file.write("newdata"), 0); + } + ASSERT_EQ(content("data:write.txt"), QByteArray("newdata")); +} + +TEST_F(TestSearchPaths, pathSuffix) +{ + const QString sysPath = nativeAbsolutePath("system1"); + const QString locPath = nativeAbsolutePath("local"); + + setTestEnv(locPath, sysPath); + ASSERT_TRUE(SearchPaths::init("suffix")); + + // Local gets priority + ASSERT_EQ(filterPathList(QDir::searchPaths("data")), + QStringList({ + (locPath + QDir::separator() + "suffix"), + (sysPath + QDir::separator() + "suffix"), + })); +} diff --git a/src/web/.editorconfig b/src/web/.editorconfig new file mode 100644 index 0000000..b7eb840 --- /dev/null +++ b/src/web/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.snap] +max_line_length = off +trim_trailing_whitespace = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/src/web/.prettierignore b/src/web/.prettierignore new file mode 100644 index 0000000..ec6d3cd --- /dev/null +++ b/src/web/.prettierignore @@ -0,0 +1 @@ +package.json diff --git a/src/web/.prettierrc b/src/web/.prettierrc new file mode 100644 index 0000000..0981b7c --- /dev/null +++ b/src/web/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "printWidth": 120 +} diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt new file mode 100644 index 0000000..18e7b8b --- /dev/null +++ b/src/web/CMakeLists.txt @@ -0,0 +1,12 @@ +# Web Site packaging +set(TGT_NAME "web") + +file(GLOB_RECURSE DEPS "${CMAKE_CURRENT_SOURCE_DIR}/src/*") +# file(GLOB TEMPLATES "webpack.common.in" "webpack.dev.in" "webpack.prod.in") + +add_dist_yarnlibrary( + TARGET ${TGT_NAME} + SETUP_TEMPLATES "webpack.common.in;webpack.dev.in;webpack.prod.in" + SOURCES ${DEPS} + COMPONENT runtime +) diff --git a/src/web/LICENSE b/src/web/LICENSE new file mode 100644 index 0000000..afad93c --- /dev/null +++ b/src/web/LICENSE @@ -0,0 +1,23 @@ +From the original patternfly react seed on which this project was based: + +MIT License + +Copyright (c) 2018 Red Hat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/web/README.md b/src/web/README.md new file mode 100644 index 0000000..4bda716 --- /dev/null +++ b/src/web/README.md @@ -0,0 +1,9 @@ +# Open AFC Web GUI + +This provides a visual way to administer parts of the system including user management, configuration management, and access management. All the functions are also provided via the rat-manage-api tool accessibile in the rat_server container. + + In addition, it provides a graphical interface for making requests and displaying results. This is useful for understanding results or demonstrating the system. + +This a project containing the files required to develop and build web files to be hosted on a server. The project is running on a Typescript/React system built using Webpack and utilizes the Patternfly 4 library for styling. It also uses the Google Maps API for GeoJson display. + +The web application is built as part of the rat_server dockerfile. diff --git a/src/web/__mocks__/fileMock.js b/src/web/__mocks__/fileMock.js new file mode 100644 index 0000000..0a445d0 --- /dev/null +++ b/src/web/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = "test-file-stub"; diff --git a/src/web/__mocks__/styleMock.js b/src/web/__mocks__/styleMock.js new file mode 100644 index 0000000..f053ebf --- /dev/null +++ b/src/web/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/src/web/jest.config.js b/src/web/jest.config.js new file mode 100644 index 0000000..a3dff32 --- /dev/null +++ b/src/web/jest.config.js @@ -0,0 +1,55 @@ +// For a detailed explanation regarding each configuration property, visit: +// https://jestjs.io/docs/en/configuration.html + +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of directory names to be searched recursively up from the requiring module"s location + moduleDirectories: [ + "node_modules", + "/src" + ], + + // An array of file extensions your modules use + moduleFileExtensions: [ + "ts", + "tsx", + "js" + ], + + // A map from regular expressions to module names that allow to stub out resources with a single module + moduleNameMapper: { + "\\.(css|less)$": "/__mocks__/styleMock.js", + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", + "@app/(.*)": "/src/app/$1" + }, + + // A preset that is used as a base for Jest"s configuration + preset: "ts-jest/presets/js-with-ts", + + // The path to a module that runs some code to configure or set up the testing framework before each test + setupFilesAfterEnv: ["/test-setup.js"], + + // The test environment that will be used for testing. + testEnvironment: "jsdom", + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + snapshotSerializers: ["enzyme-to-json/serializer"], + + // The glob patterns Jest uses to detect test files + testMatch: [ + "**/*.test.(ts|tsx)" + ], + + // A map from regular expressions to paths to transformers + transform: { + "^.+\\.(ts|tsx)$": "ts-jest" + } +}; diff --git a/src/web/package.json b/src/web/package.json new file mode 100644 index 0000000..9263da1 --- /dev/null +++ b/src/web/package.json @@ -0,0 +1,93 @@ +{ + "name": "fbrat-web", + "version": "0.0.0", + "description": "AFC Web GUI", + "license": "The use of this software shall be governed by the terms and conditions of the OpenAFC Project License.", + "main": "index.js", + "private": true, + "scripts": { + "build": "webpack --config webpack.prod.js --display minimal", + "build-dev": "webpack --config webpack.dev.js --display normal", + "build-prod": "webpack --config webpack.prod.js --display normal", + "build-docs": "typedoc --out docs --mode modules --ignoreCompilerErrors src/app", + "start": "webpack-dev-server --hot --watch --color --progress --info=true --config webpack.dev.js", + "test": "jest", + "lint": "tslint -c ./tslint.json --project .", + "build:bundle-profile": "webpack --profile --json > stats.json", + "bundle-profile:analyze": "yarn build:bundle-profile && webpack-bundle-analyzer ./stats.json", + "clean": "rimraf www" + }, + "devDependencies": { + "@patternfly/react-charts": "5.2.9", + "@patternfly/react-core": "3.129.3", + "@patternfly/react-icons": "3.14.30", + "@patternfly/react-styles": "3.6.15", + "@patternfly/react-table": "2.24.64", + "@patternfly/react-tokens": "2.7.14", + "@types/enzyme": "3.10.4", + "@types/enzyme-adapter-react-16": "1.0.5", + "@types/google-map-react": "1.1.3", + "@types/jest": "24.0.25", + "@types/node": "13.1.6", + "@types/react": "16.9.17", + "@types/react-dom": "16.9.4", + "@types/react-measure": "2.0.5", + "@types/react-router-dom": "5.1.3", + "@types/recharts": "1.8.5", + "@types/victory": "33.1.2", + "@types/webpack": "4.41.2", + "css-loader": "3.4.2", + "enzyme": "3.11.0", + "enzyme-adapter-react-16": "1.15.2", + "enzyme-to-json": "3.4.3", + "file-loader": "5.0.2", + "google-map-react": "1.1.5", + "hard-source-webpack-plugin": "0.13.1", + "html-webpack-plugin": "3.2.0", + "imagemin": "7.0.1", + "jest": "24.9.0", + "konva": "4.1.2", + "mini-css-extract-plugin": "0.9.0", + "optimize-css-assets-webpack-plugin": "5.0.1", + "prettier": "3.2.5", + "prop-types": "15.6.1", + "raw-loader": "4.0.0", + "react": "16.12.0", + "react-axe": "3.3.0", + "react-dom": "16.12.0", + "react-google-recaptcha": "2.1.0", + "react-konva": "16.12.0-0", + "react-measure": "2.3.0", + "react-router-dom": "5.1.2", + "recharts": "1.8.5", + "regenerator-runtime": "0.13.3", + "rimraf": "3.0.0", + "style-loader": "1.1.2", + "svg-url-loader": "3.0.3", + "terser-webpack-plugin": "2.3.2", + "ts-jest": "24.3.0", + "ts-loader": "6.2.1", + "tsconfig-paths-webpack-plugin": "3.2.0", + "tslint": "5.20.1", + "tslint-config-prettier": "1.18.0", + "tslint-eslint-rules": "5.4.0", + "tslint-react": "4.1.0", + "typedoc": "0.16.2", + "typescript": "3.7.4", + "url-loader": "3.0.0", + "webdav": "2.10.1", + "webpack": "4.41.5", + "webpack-bundle-analyzer": "3.6.0", + "webpack-cli": "3.3.10", + "webpack-dev-server": "3.10.1", + "webpack-merge": "4.2.2" + }, + "dependencies": { + "axios": "^v1.6.0", + "ejs": "^3.1.7", + "highlight.js": "^11.6.0", + "jsdom": "^16.5.0", + "node-fetch": "^3.2.10", + "terser": "^4.8.1" + } +} diff --git a/src/web/src/app/AFCConfig/AFCConfig.tsx b/src/web/src/app/AFCConfig/AFCConfig.tsx new file mode 100644 index 0000000..a4df6ee --- /dev/null +++ b/src/web/src/app/AFCConfig/AFCConfig.tsx @@ -0,0 +1,189 @@ +import * as React from 'react'; +import { PageSection, Title, Alert } from '@patternfly/react-core'; +import { AFCForm } from './AFCForm'; +import { AFCConfigFile, FreqRange, RatResponse } from '../Lib/RatApiTypes'; +import { getDefaultAfcConf, putAfcConfigFile, guiConfig } from '../Lib/RatApi'; +import { logger } from '../Lib/Logger'; +import { Limit } from '../Lib/Admin'; +import { getLastUsedRegionFromCookie } from '../Lib/Utils'; + +/** + * AFCConfic.tsx: main component for afc config page + * author: Sam Smucny + */ + +/** + * Interface definition + */ +interface AFCState { + config: AFCConfigFile; + ulsFiles: string[]; + antennaPatterns: string[]; + regions: string[]; + messageType?: 'Warn'; + messageTitle?: string; + messageValue?: string; + isModalOpen?: boolean; +} + +/** + * Page level component for AFC Config + * @param ulsFiles Injected dependency + * @param afcConf Injected Config from server + * @param antennaPatterns Injected dependency + */ +class AFCConfig extends React.Component< + { + limit: RatResponse; + ulsFiles: RatResponse; + afcConf: RatResponse; + antennaPatterns: RatResponse; + regions: RatResponse; + frequencyBands: RatResponse; + }, + AFCState +> { + constructor( + props: Readonly<{ + limit: RatResponse; + frequencyBands: RatResponse; + ulsFiles: RatResponse; + afcConf: RatResponse; + antennaPatterns: RatResponse; + regions: RatResponse; + }>, + ) { + super(props); + //@ts-ignore + var lastRegFromCookie = getLastUsedRegionFromCookie(); + + const state: AFCState = { + config: getDefaultAfcConf(lastRegFromCookie), + isModalOpen: false, + messageValue: '', + messageTitle: '', + ulsFiles: [], + antennaPatterns: [], + regions: [], + }; + + if (props.afcConf.kind === 'Success') { + let incompatible = false; + // if stored configurations do not have fields present in default, it's incompatible. + if (props.afcConf.result.version !== guiConfig.version) { + for (var p in state.config) { + if (!state.config.hasOwnProperty(p)) continue; + if (!props.afcConf.result.hasOwnProperty(p)) { + incompatible = true; + logger.error( + 'Could not load most recent AFC Config Defaults.', + 'Missing parameters. Current version ' + + guiConfig.version + + ' Incompatible version ' + + props.afcConf.result.version, + ); + Object.assign(state, { + config: getDefaultAfcConf(lastRegFromCookie), + messageType: 'Warn', + messageTitle: 'Invalid Config Version', + messageValue: + 'The current version (' + + guiConfig.version + + ') is not compatible with the loaded configuration. The loaded configuration was created for version ' + + props.afcConf.result.version + + '. The default configuration has been loaded instead. To resolve this AFC Config will need to be updated below.', + }); + break; + } + } + } + + if (!incompatible) { + Object.assign(state, { config: props.afcConf.result }); + state.config.version = guiConfig.version; + } + } else { + logger.error( + 'Could not load most recent AFC Config Defaults.', + 'error code: ', + props.afcConf.errorCode, + 'description: ', + props.afcConf.description, + ); + Object.assign(state, { config: getDefaultAfcConf(lastRegFromCookie) }); + } + + if (props.ulsFiles.kind === 'Success') { + Object.assign(state, { ulsFiles: props.ulsFiles.result }); + } else { + logger.error( + 'Could not load ULS Database files', + 'error code: ', + props.ulsFiles.errorCode, + 'description: ', + props.ulsFiles.description, + ); + Object.assign(state, { ulsFiles: [] }); + } + + if (props.antennaPatterns.kind === 'Success') { + Object.assign(state, { antennaPatterns: props.antennaPatterns.result }); + } else { + logger.error( + 'Could not load antenna pattern files', + 'error code: ', + props.antennaPatterns.errorCode, + 'description: ', + props.antennaPatterns.description, + ); + Object.assign(state, { antennaPatterns: [] }); + } + + if (props.regions.kind === 'Success') { + Object.assign(state, { regions: props.regions.result }); + } else { + logger.error( + 'Could not load regions', + 'error code: ', + props.regions.errorCode, + 'description: ', + props.regions.description, + ); + Object.assign(state, { regions: [] }); + } + + this.state = state; + } + + private submit = (conf: AFCConfigFile): Promise> => + putAfcConfigFile(conf).then((resp) => { + if (resp.kind === 'Success') { + this.setState({ messageType: undefined, messageTitle: '', messageValue: '' }); + } + return resp; + }); + + render() { + return ( + + AFC Configuration + {this.state.messageType === 'Warn' && ( + +
{this.state.messageValue}
+
+ )} + this.submit(x)} + /> +
+ ); + } +} + +export { AFCConfig }; diff --git a/src/web/src/app/AFCConfig/AFCForm.tsx b/src/web/src/app/AFCConfig/AFCForm.tsx new file mode 100644 index 0000000..138b11e --- /dev/null +++ b/src/web/src/app/AFCConfig/AFCForm.tsx @@ -0,0 +1,573 @@ +import * as React from 'react'; +import { + FormGroup, + InputGroup, + TextInput, + InputGroupText, + FormSelect, + FormSelectOption, + ActionGroup, + Checkbox, + Button, + AlertActionCloseButton, + Alert, + Gallery, + GalleryItem, + Card, + CardBody, + Modal, + TextArea, + ClipboardCopy, + ClipboardCopyVariant, + Tooltip, + TooltipPosition, + Radio, + CardHead, + PageSection, +} from '@patternfly/react-core'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; +import { + AFCConfigFile, + PenetrationLossModel, + PolarizationLossModel, + BodyLossModel, + AntennaPatternState, + DefaultAntennaType, + UserAntennaPattern, + RatResponse, + PropagationModel, + APUncertainty, + ITMParameters, + FSReceiverFeederLoss, + FSReceiverNoise, + FreqRange, + CustomPropagation, + ChannelResponseAlgorithm, +} from '../Lib/RatApiTypes'; +import { + getDefaultAfcConf, + guiConfig, + getAfcConfigFile, + putAfcConfigFile, + importCache, + getRegions, + getAllowedRanges, +} from '../Lib/RatApi'; +import { logger } from '../Lib/Logger'; +import { Limit, getDeniedRegionsCsvFile } from '../Lib/Admin'; +import { AllowedRangesDisplay, getDefaultRangesByRegion } from './AllowedRangesForm'; +import DownloadContents from '../Components/DownloadContents'; +import { AFCFormUSACanada } from './AFCFormUSACanada'; +import { mapRegionCodeToName, trimmedRegionStr } from '../Lib/Utils'; + +/** + * AFCForm.tsx: form for generating afc configuration files to be used to update server + * author: Sam Smucny + */ + +/** + * Form component for AFC Config + */ +export class AFCForm extends React.Component< + { + limit: Limit; + frequencyBands: FreqRange[]; + config: AFCConfigFile; + ulsFiles: string[]; + antennaPatterns: string[]; + regions: string[]; + onSubmit: (x: AFCConfigFile) => Promise>; + }, + { + config: AFCConfigFile; + isModalOpen: boolean; + messageSuccess: string | undefined; + messageError: string | undefined; + antennaPatternData: AntennaPatternState; + fsDatabaseDirectory: string; + } +> { + constructor( + props: Readonly<{ + limit: Limit; + frequencyBands: FreqRange[]; + config: AFCConfigFile; + ulsFiles: string[]; + antennaPatterns: string[]; + regions: string[]; + onSubmit: (x: AFCConfigFile) => Promise>; + }>, + ) { + super(props); + let config = props.config as AFCConfigFile; + if (props.frequencyBands.length > 0) { + config.freqBands = props.frequencyBands.filter( + (x) => x.region == config.regionStr || (!x.region && config.regionStr == 'US'), + ); + if (config.freqBands.length == 0) { + // There were none - check if we are a demo/test config and then use the actual + config.freqBands = props.frequencyBands.filter( + (x) => x.region == trimmedRegionStr(config.regionStr) || (!x.region && config.regionStr == 'US'), + ); + } + } else { + config.freqBands = getDefaultRangesByRegion(config.regionStr ?? 'US'); + } + + this.state = { + config: config, + messageError: undefined, + messageSuccess: undefined, + isModalOpen: false, + antennaPatternData: { + defaultAntennaPattern: config.ulsDefaultAntennaType, + }, + fsDatabaseDirectory: 'rat_transfer/ULS_Database/', + }; + } + + private getFrequencyRanges = async (regionStr: string | undefined) => { + return getAllowedRanges().then((rangeRes) => { + if (rangeRes.kind === 'Success') { + // Get for the region if present + let freqBands = rangeRes.result.filter((x) => x.region == regionStr || (!x.region && regionStr == 'US')); + if (freqBands.length == 0) { + // There were none - check if we are a demo/test config and then use the actual + freqBands = rangeRes.result.filter( + (x) => x.region == trimmedRegionStr(regionStr) || (!x.region && regionStr == 'US'), + ); + } + return freqBands; + } else { + return getDefaultRangesByRegion(trimmedRegionStr(regionStr) ?? 'US'); + } + }); + }; + + private setUlsDatabase = (n: string) => + this.setState({ config: Object.assign(this.state.config, { fsDatabaseFile: n }) }); + private setUlsRegion = (n: string) => { + // region changed by user, reload the coresponding configuration for that region + getAfcConfigFile(n).then((res) => { + if (res.kind === 'Success') { + this.updateEntireConfigState(res.result); + document.cookie = `afc-config-last-region=${n};max-age=2592000;path='/';SameSite=strict`; + } else { + if (res.errorCode == 404) { + let defConf = getDefaultAfcConf(n); + this.updateEntireConfigState(defConf); + document.cookie = `afc-config-last-region=${n};max-age=2592000;path='/';SameSite=strict`; + this.setState({ messageSuccess: 'No config found for this region, using region default' }); + } else { + this.setState({ messageError: res.description, messageSuccess: undefined }); + } + } + }); + }; + + private isValid = () => { + const err = (s?: string) => ({ isError: true, message: s || 'One or more inputs are invalid' }); + const model = this.state.config; + + if (model.APUncertainty.points_per_degree <= 0 || model.APUncertainty.height <= 0) return err(); + if (model.ITMParameters.minSpacing < 1 || model.ITMParameters.minSpacing > 30) + return err('Path Min Spacing must be between 1 and 30'); + if (model.ITMParameters.maxPoints < 100 || model.ITMParameters.maxPoints > 10000) + return err('Path Max Points must be between 100 and 10000'); + + if (!model.fsDatabaseFile) return err(); + if (!model.propagationEnv) return err(); + + if (!(model.minEIRPIndoor <= model.maxEIRP) || !(model.minEIRPOutdoor <= model.maxEIRP)) return err(); + + if ( + (this.props.limit.indoorEnforce && model.minEIRPIndoor < this.props.limit.indoorLimit) || + model.maxEIRP < this.props.limit.indoorLimit + ) { + return err('Indoor EIRP value must be at least ' + this.props.limit.indoorLimit + ' dBm'); + } + + if ( + (this.props.limit.outdoorEnforce && model.minEIRPOutdoor < this.props.limit.outdoorLimit) || + model.maxEIRP < this.props.limit.outdoorLimit + ) { + return err('Outdoor EIRP value must be at least ' + this.props.limit.outdoorLimit + ' dBm'); + } + + if (!(model.maxLinkDistance >= 1)) return err(); + + switch (model.buildingPenetrationLoss.kind) { + case 'ITU-R Rec. P.2109': + if (!model.buildingPenetrationLoss.buildingType) return err(); + if (model.buildingPenetrationLoss.confidence > 100 || model.buildingPenetrationLoss.confidence < 0) + return err(); + break; + case 'Fixed Value': + if (model.buildingPenetrationLoss.value === undefined) return err(); + break; + default: + return err(); + } + + const propModel = model.propagationModel; + switch (propModel.kind) { + case 'ITM with no building data': + if (propModel.itmConfidence < 0 || propModel.itmConfidence > 100) return err(); + if (propModel.itmReliability < 0 || propModel.itmReliability > 100) return err(); + if (propModel.win2Confidence < 0 || propModel.win2Confidence > 100) return err(); + if (propModel.win2ProbLosThreshold < 0 || propModel.win2ProbLosThreshold > 100) return err(); + if (propModel.p2108Confidence < 0 || propModel.p2108Confidence > 100) return err(); + if (propModel.terrainSource != 'SRTM (90m)' && propModel.terrainSource != '3DEP (30m)') return err(); + break; + case 'ITM with building data': + if (propModel.itmConfidence < 0 || propModel.itmConfidence > 100) return err(); + if (propModel.itmReliability < 0 || propModel.itmReliability > 100) return err(); + if (propModel.buildingSource != 'LiDAR' && propModel.buildingSource != 'B-Design3D') return err(); + break; + case 'FCC 6GHz Report & Order': + if (propModel.itmConfidence < 0 || propModel.itmConfidence > 100) return err(); + if (propModel.itmReliability < 0 || propModel.itmReliability > 100) return err(); + if (propModel.win2ConfidenceCombined < 0 || propModel.win2ConfidenceCombined > 100) return err(); + if (propModel.p2108Confidence < 0 || propModel.p2108Confidence > 100) return err(); + if ( + propModel.buildingSource != 'LiDAR' && + propModel.buildingSource != 'B-Design3D' && + propModel.buildingSource != 'None' + ) + return err(); + if (propModel.terrainSource != '3DEP (30m)') return err('Invalid terrain source.'); + break; + case 'Brazilian Propagation Model': + if (propModel.itmConfidence < 0 || propModel.itmConfidence > 100) return err(); + if (propModel.itmReliability < 0 || propModel.itmReliability > 100) return err(); + if (propModel.win2ConfidenceCombined < 0 || propModel.win2ConfidenceCombined > 100) return err(); + if (propModel.p2108Confidence < 0 || propModel.p2108Confidence > 100) return err(); + if ( + propModel.buildingSource != 'LiDAR' && + propModel.buildingSource != 'B-Design3D' && + propModel.buildingSource != 'None' + ) + return err(); + if (propModel.terrainSource != 'SRTM (30m)') return err('Invalid terrain source.'); + break; + case 'FSPL': + break; + case 'Ray Tracing': + break; + case 'Custom': + if (propModel.itmConfidence < 0 || propModel.itmConfidence > 100) return err(); + if (propModel.itmReliability < 0 || propModel.itmReliability > 100) return err(); + if (propModel.win2ConfidenceCombined! < 0 || propModel.win2ConfidenceCombined! > 100) return err(); + if (propModel.p2108Confidence < 0 || propModel.p2108Confidence > 100) return err(); + if ( + propModel.buildingSource != 'LiDAR' && + propModel.buildingSource != 'B-Design3D' && + propModel.buildingSource != 'None' + ) + return err(); + if (propModel.buildingSource !== 'None' && propModel.terrainSource != '3DEP (30m)') + return err('Invalid terrain source.'); + break; + case 'ISED DBS-06': + if (propModel.itmConfidence < 0 || propModel.itmConfidence > 100) return err(); + if (propModel.itmReliability < 0 || propModel.itmReliability > 100) return err(); + if (propModel.win2ConfidenceCombined! < 0 || propModel.win2ConfidenceCombined! > 100) return err(); + if (propModel.p2108Confidence < 0 || propModel.p2108Confidence > 100) return err(); + break; + case 'Ofcom Propagation Model': + if (propModel.itmConfidence < 0 || propModel.itmConfidence > 100) return err(); + if (propModel.itmReliability < 0 || propModel.itmReliability > 100) return err(); + if (propModel.win2ConfidenceCombined < 0 || propModel.win2ConfidenceCombined > 100) return err(); + if (propModel.p2108Confidence < 0 || propModel.p2108Confidence > 100) return err(); + if ( + propModel.buildingSource != 'LiDAR' && + propModel.buildingSource != 'B-Design3D' && + propModel.buildingSource != 'None' + ) + return err(); + if (propModel.terrainSource != 'SRTM (30m)') return err('Invalid terrain source.'); + break; + default: + return err(); + } + + return { + isError: false, + message: undefined, + }; + }; + + /** + * Use this method when updating the entire configuration as it will also update the antenna pattern subcomponent. When updating just + * another item (like just updating the PenatrationLossModel) use setState as normal + * @param config new configuation file + */ + private updateEntireConfigState(config: AFCConfigFile) { + getDeniedRegionsCsvFile(config.regionStr!).then((res) => { + if (res.kind === 'Success') { + config.deniedRegionFile = `rat_transfer/denied_regions/${config.regionStr}_denied_regions.csv`; + } else { + config.deniedRegionFile = ''; + } + + this.getFrequencyRanges(config.regionStr!).then((rangeRes) => { + config.freqBands = rangeRes; + this.setState( + { + config: config, + antennaPatternData: { + defaultAntennaPattern: config.ulsDefaultAntennaType, + }, + }, + () => { + document.cookie = `afc-config-last-region=${config.regionStr};max-age=2592000;path='/';SameSite=strict`; + }, + ); + }); + }); + } + + /** + * Push completed form to parent if valid + */ + private submit = () => { + const isValid = this.isValid(); + if (isValid.isError) { + this.setState({ messageError: isValid.message, messageSuccess: undefined }); + return; + } + this.props.onSubmit(this.state.config).then((res) => { + if (res.kind === 'Success') { + this.setState({ messageSuccess: res.result, messageError: undefined }); + } else { + this.setState({ messageError: res.description, messageSuccess: undefined }); + } + }); + }; + + private reset = () => { + let config = getDefaultAfcConf(this.state.config.regionStr); + config.freqBands = this.props.frequencyBands.filter( + (x) => x.region == this.state.config.regionStr || (!x.region && this.state.config.regionStr == 'US'), + ); + this.updateEntireConfigState(config); + }; + + private setConfig = (newConf: string) => { + try { + var escaped = newConf.replace(/\s+/g, ' '); + const parsed = JSON.parse(escaped) as AFCConfigFile; + if (parsed.version == guiConfig.version) { + this.updateEntireConfigState(Object.assign(this.state.config, parsed)); + } else { + this.setState({ + messageError: + 'The imported configuration (version: ' + + parsed.version + + ') is not compatible with the current version (' + + guiConfig.version + + ').', + }); + } + } catch (e) { + logger.error('Pasted value was not valid JSON'); + } + }; + + private getConfig = () => JSON.stringify(this.state.config); + + private export = () => + new Blob([JSON.stringify(Object.assign({ afcConfig: this.state.config }))], { + type: 'application/json', + }); + + private import(ev) { + // @ts-ignore + const file = ev.target.files[0]; + const reader = new FileReader(); + try { + reader.onload = async () => { + try { + const value: any = JSON.parse(reader.result as string); + if (value.afcConfig) { + if (value.afcConfig.version !== guiConfig.version) { + const warning: string = + "The imported file is from a different version. It has version '" + + value.afcConfig.version + + "', and you are currently running '" + + guiConfig.version + + "'."; + logger.warn(warning); + } + const putResp = await putAfcConfigFile(value.afcConfig); + if (putResp.kind === 'Error') { + this.setState({ messageError: putResp.description, messageSuccess: undefined }); + return; + } else { + this.updateEntireConfigState(value.afcConfig); + } + } + + value.afcConfig = undefined; + + importCache(value); + + this.setState({ messageError: undefined, messageSuccess: 'Import successful!' }); + } catch (e) { + this.setState({ messageError: 'Unable to import file', messageSuccess: undefined }); + } + }; + + reader.readAsText(file); + ev.target.value = ''; + } catch (e) { + logger.error('Failed to import application state', e); + this.setState({ messageError: 'Failed to import application state', messageSuccess: undefined }); + } + } + + updateConfigFromComponent(newState: AFCConfigFile) { + this.setState({ config: newState }); + } + + updateAntennaDataFromComponent(newState: AntennaPatternState) { + this.setState({ antennaPatternData: newState }); + } + + render() { + return ( + + this.setState({ isModalOpen: false })} + actions={[ + , + ]} + > + this.setConfig(String(v).trim())} + aria-label="text area" + > + {this.getConfig()} + + + + + + + this.setUlsRegion(x)} + id="horizontal-form-uls-region" + name="horizontal-form-uls-region" + isValid={!!this.state.config.regionStr} + style={{ textAlign: 'right' }} + > + + {this.props.regions.map((option: string) => ( + + ))} + + + + + + {' '} + +

CONUS_ULS_LATEST.sqlite3 refers to the latest stable CONUS FS database .

+ + } + > + +
+ this.setUlsDatabase(x)} + id="horizontal-form-uls-db" + name="horizontal-form-uls-db" + isValid={!!this.state.config.fsDatabaseFile} + style={{ textAlign: 'right' }} + > + + {this.props.ulsFiles.map((option: string) => ( + + ))} + +
+
+ + x.region == this.state.config.regionStr)} + limit={this.props.limit} + updateConfig={(x) => this.updateConfigFromComponent(x)} + updateAntennaData={(x) => this.updateAntennaDataFromComponent(x)} + /> +
+
+ <> + {this.state.messageError !== undefined && ( + this.setState({ messageError: undefined })} />} + > + {this.state.messageError} + + )} + + <> + {this.state.messageSuccess !== undefined && ( + this.setState({ messageSuccess: undefined })} />} + > + {this.state.messageSuccess} + + )} + +
+ <> + {' '} + {' '} + + +
+
+ + this.import(ev)} + /> + +
+ this.export()} /> +
+
+
+ ); + } +} diff --git a/src/web/src/app/AFCConfig/AFCFormUSACanada.tsx b/src/web/src/app/AFCConfig/AFCFormUSACanada.tsx new file mode 100644 index 0000000..b0c541c --- /dev/null +++ b/src/web/src/app/AFCConfig/AFCFormUSACanada.tsx @@ -0,0 +1,982 @@ +import * as React from 'react'; +import { + FormGroup, + InputGroup, + TextInput, + InputGroupText, + FormSelect, + FormSelectOption, + ActionGroup, + Checkbox, + Button, + AlertActionCloseButton, + Alert, + Gallery, + GalleryItem, + Card, + CardBody, + Modal, + TextArea, + ClipboardCopy, + ClipboardCopyVariant, + Tooltip, + TooltipPosition, + Radio, + CardHead, + PageSection, +} from '@patternfly/react-core'; +import { IglooIcon, OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; +import BuildlingPenetrationLossForm from './BuildingPenetrationLossForm'; +import PolarizationMismatchLossForm from './PolarizationMismatchLossForm'; +import { ITMParametersForm } from './ITMParametersForm'; +import BodyLossForm from './BodyLossForm'; +import { APUncertaintyForm } from './APUncertaintyForm'; +import { PropogationModelForm } from './PropogationModelForm'; +import { + AFCConfigFile, + PenetrationLossModel, + PolarizationLossModel, + BodyLossModel, + AntennaPatternState, + DefaultAntennaType, + UserAntennaPattern, + RatResponse, + PropagationModel, + APUncertainty, + ITMParameters, + FSReceiverFeederLoss, + FSReceiverNoise, + FreqRange, + CustomPropagation, + ChannelResponseAlgorithm, + FSClutterModel, +} from '../Lib/RatApiTypes'; +import { Limit } from '../Lib/Admin'; +import { AllowedRangesDisplay } from './AllowedRangesForm'; +import AntennaPatternForm from './AntennaPatternForm'; + +/** + * AFCForm.tsx: form for generating afc configuration files to be used to update server + * author: Sam Smucny + */ + +/** + * Form component for AFC Config + */ +export class AFCFormUSACanada extends React.Component< + { + limit: Limit; + frequencyBands: FreqRange[]; + config: AFCConfigFile; + antennaPatterns: AntennaPatternState; + updateConfig: (x: AFCConfigFile) => void; + updateAntennaData: (x: AntennaPatternState) => void; + }, + {} +> { + private trimmedRegionStr = () => { + if ( + !!this.props.config.regionStr && + (this.props.config.regionStr?.startsWith('TEST_') || this.props.config.regionStr?.startsWith('DEMO_')) + ) { + return this.props.config.regionStr.substring(5); + } else { + return this.props.config.regionStr; + } + }; + + private hasValue = (val: any) => val !== undefined && val !== null; + private hasValueExists = (f: () => any): boolean => { + try { + return this.hasValue(f()); + } catch (error) { + return false; + } + }; + + private setMinEIRPIndoor = (n: number) => + this.props.updateConfig(Object.assign(this.props.config, { minEIRPIndoor: n })); + private setMinOutdoorEIRP = (n: number) => + this.props.updateConfig(Object.assign(this.props.config, { minEIRPOutdoor: n })); + private setMaxEIRP = (n: number) => this.props.updateConfig(Object.assign(this.props.config, { maxEIRP: n })); + private setMinPSD = (n: number) => this.props.updateConfig(Object.assign(this.props.config, { minPSD: n })); + private setFeederLoss = (f: FSReceiverFeederLoss) => + this.props.updateConfig(Object.assign(this.props.config, { receiverFeederLoss: f })); + private setReceiverNoise = (f: FSReceiverNoise) => + this.props.updateConfig(Object.assign(this.props.config, { fsReceiverNoise: f })); + private setThreshold = (n: number) => this.props.updateConfig(Object.assign(this.props.config, { threshold: n })); + private setMaxLinkDistance = (n: number) => + this.props.updateConfig(Object.assign(this.props.config, { maxLinkDistance: n })); + private setEnableMapInVirtualAp = (n: boolean) => + this.props.updateConfig(Object.assign(this.props.config, { enableMapInVirtualAp: n })); + private setRoundPSDEIRPFlag = (n: boolean) => + this.props.updateConfig(Object.assign(this.props.config, { roundPSDEIRPFlag: n })); + private setNearFieldAdjFlag = (n: boolean) => + this.props.updateConfig(Object.assign(this.props.config, { nearFieldAdjFlag: n })); + private setVisiblityThreshold = (n: number) => + this.props.updateConfig(Object.assign(this.props.config, { visibilityThreshold: n })); + private setPropogationEnv = (n: string) => { + const newConfig = { ...this.props.config }; + + newConfig.propagationEnv = n as 'NLCD Point' | 'Population Density Map' | 'Urban' | 'Suburban' | 'Rural'; + if (n != 'NLCD Point' && this.props.config.propagationEnv == 'NLCD Point') { + delete newConfig.nlcdFile; + } + if (n == 'NLCD Point' && this.props.config.propagationEnv != 'NLCD Point' && !this.props.config.nlcdFile) { + newConfig.nlcdFile = 'nlcd_production'; + } + this.props.updateConfig(newConfig); + }; + private setNlcdFile = (n: string) => this.props.updateConfig(Object.assign(this.props.config, { nlcdFile: n })); + + private setScanBelowGround = (n: string) => { + const conf = this.props.config; + switch (n) { + case 'truncate': + conf.scanPointBelowGroundMethod = 'truncate'; + break; + case 'discard': + conf.scanPointBelowGroundMethod = 'discard'; + break; + default: + break; + } + this.props.updateConfig(conf); + }; + + private setPenetrationLoss = (x: PenetrationLossModel) => { + const conf = { ...this.props.config }; + conf.buildingPenetrationLoss = x; + this.props.updateConfig(conf); + }; + + private setPolarizationLoss = (x: PolarizationLossModel) => { + const conf = { ...this.props.config }; + conf.polarizationMismatchLoss = x; + this.props.updateConfig(conf); + }; + + private setBodyLoss = (x: BodyLossModel) => { + const conf = { ...this.props.config }; + conf.bodyLoss = x; + this.props.updateConfig(conf); + }; + private setAntennaPattern = (x: AntennaPatternState) => { + const conf = { ...this.props.config }; + conf.ulsDefaultAntennaType = x.defaultAntennaPattern; + this.props.updateAntennaData(x); + this.props.updateConfig(conf); + }; + private setPropagationModel = (x: PropagationModel) => { + if (x.kind === 'Custom') { + //rlanITMTxClutterMethod is set in the CustomPropagation but stored at the top level + // so move it up if present + const conf = { ...this.props.config }; + var model = x as CustomPropagation; + var itmTxClutterMethod = model.rlanITMTxClutterMethod; + delete model.rlanITMTxClutterMethod; + if (!!itmTxClutterMethod) { + conf.rlanITMTxClutterMethod = itmTxClutterMethod; + } else { + delete conf.rlanITMTxClutterMethod; + } + switch (x.winner2LOSOption) { + case 'BLDG_DATA_REQ_TX': + break; + case 'FORCE_LOS': + delete model.win2ConfidenceNLOS; + delete model.win2ConfidenceCombined; + break; + case 'FORCE_NLOS': + delete model.win2ConfidenceLOS; + delete model.win2ConfidenceCombined; + break; + case 'UNKNOWN': + break; + } + + conf.propagationModel = model; + this.props.updateConfig(conf); + } else { + const conf = { ...this.props.config }; + if (x.kind === 'ITM with building data') { + conf.rlanITMTxClutterMethod = 'BLDG_DATA'; + } else { + conf.rlanITMTxClutterMethod = 'FORCE_TRUE'; + } + if (x.kind === 'FCC 6GHz Report & Order' && x.buildingSource === 'B-Design3D') { + delete x.win2ConfidenceLOS; + delete x.win2ConfidenceNLOS; + } + if (x.kind === 'FCC 6GHz Report & Order' && x.buildingSource === 'None') { + delete x.win2ConfidenceNLOS; + } + if (x.kind === 'Brazilian Propagation Model' && x.buildingSource === 'B-Design3D') { + delete x.win2ConfidenceLOS; + delete x.win2ConfidenceNLOS; + } + + if (x.kind === 'Brazilian Propagation Model' && x.buildingSource === 'None') { + delete x.win2ConfidenceNLOS; + } + if (x.kind !== 'ISED DBS-06') { + conf.cdsmDir = ''; + } + if (x.kind === 'ISED DBS-06') { + if (x.surfaceDataSource === 'None') { + conf.cdsmDir = ''; + } else if (conf.cdsmDir === '') { + conf.cdsmDir = 'rat_transfer/cdsm/3ov4_arcsec_wgs84'; + } + } + + conf.propagationModel = x; + this.props.updateConfig(conf); + } + }; + + private setAPUncertainty = (x: APUncertainty) => { + const conf = { ...this.props.config }; + conf.APUncertainty = x; + this.props.updateConfig(conf); + }; + + private setITMParams = (x: ITMParameters) => { + const conf = { ...this.props.config }; + conf.ITMParameters = x; + this.props.updateConfig(conf); + }; + + private setUseClutter = (x: boolean) => { + const conf = { ...this.props.config }; + //If changing to true, and was false, and there is no clutter model, use the defaults + if (x && !conf.clutterAtFS && !conf.fsClutterModel) { + conf.fsClutterModel = { maxFsAglHeight: 6, p2108Confidence: 5 }; + } + conf.clutterAtFS = x; + this.props.updateConfig(conf); + }; + + private setFsClutterConfidence = (n: number | string) => { + const val = Number(n); + const conf = this.props.config; + const newParams: FSClutterModel = { ...conf.fsClutterModel! }; + newParams.p2108Confidence = val; + conf.fsClutterModel = newParams; + this.props.updateConfig(conf); + }; + private setFsClutterMaxHeight = (n: number | string) => { + const val = Number(n); + const conf = this.props.config; + const newParams: FSClutterModel = { ...conf.fsClutterModel! }; + newParams.maxFsAglHeight = val; + conf.fsClutterModel = newParams; + this.props.updateConfig(conf); + }; + + private getPropagationModelForForm = () => { + //rlanITMTxClutterMethod is stored at the top level but set in the form + // so move it down if present + if (this.props.config.propagationModel.kind !== 'Custom') { + return { ...this.props.config.propagationModel }; + } else { + const customModel = { ...this.props.config.propagationModel } as CustomPropagation; + customModel.rlanITMTxClutterMethod = this.props.config.rlanITMTxClutterMethod; + return customModel; + } + }; + + private setChannelResponseAlgorithm = (v: ChannelResponseAlgorithm) => { + this.props.updateConfig(Object.assign(this.props.config, { channelResponseAlgorithm: v })); + }; + + private getLandCoverOptions = () => { + let trimmed = this.trimmedRegionStr(); + switch (trimmed) { + case 'CA': + return ( + <> + + + ); + case 'BR': + return ( + <> + + + ); + case 'GB': + return ( + <> + + + ); + case 'US': + default: + return ( + <> + + + + ); + } + }; + + private getDefaultLandCoverDatabase = () => { + switch (this.trimmedRegionStr()) { + case 'CA': + return 'rat_transfer/nlcd/ca/landcover-2020-classification_resampled.tif'; + case 'BR': + return 'rat_transfer/nlcd/br/landcover-for-brazil.tbd.tif'; + case 'GB': + return 'rat_transfer/nlcd/eu/U2018_CLC2012_V2020_20u1_resampled.tif'; + case 'US': + default: + return 'rat_transfer/nlcd/nlcd_production'; + } + }; + + render = () => ( + <> + + + + this.setMinEIRPIndoor(Number(x))} + type="number" + step="any" + id="horizontal-form-min-eirp" + name="horizontal-form-min-eirp" + isValid={ + this.props.limit.indoorEnforce + ? this.props.config.minEIRPIndoor <= this.props.config.maxEIRP && + this.props.config.minEIRPIndoor >= this.props.limit.indoorLimit + : this.props.config.minEIRPIndoor <= this.props.config.maxEIRP + } + style={{ textAlign: 'right' }} + /> + dBm + + + + + this.setMinOutdoorEIRP(Number(x))} + type="number" + step="any" + id="horizontal-form-min-eirp" + name="horizontal-form-min-eirp" + isValid={ + this.props.limit.outdoorEnforce + ? this.props.config.minEIRPOutdoor <= this.props.config.maxEIRP && + this.props.config.minEIRPOutdoor >= this.props.limit.outdoorLimit + : this.props.config.minEIRPOutdoor <= this.props.config.maxEIRP + } + style={{ textAlign: 'right' }} + /> + dBm + + + + + + + this.setMaxEIRP(Number(x))} + type="number" + step="any" + id="horizontal-form-max-eirp" + name="horizontal-form-max-eirp" + isValid={ + this.props.limit.indoorEnforce || this.props.limit.outdoorEnforce + ? this.props.config.minEIRPIndoor <= this.props.config.maxEIRP && + this.props.config.maxEIRP >= this.props.limit.indoorLimit && + this.props.config.minEIRPOutdoor <= this.props.config.maxEIRP && + this.props.config.maxEIRP >= this.props.limit.outdoorLimit + : this.props.config.minEIRPIndoor <= this.props.config.maxEIRP && + this.props.config.minEIRPOutdoor <= this.props.config.maxEIRP + } + style={{ textAlign: 'right' }} + /> + dBm + + + + + + + this.setMinPSD(Number(x))} + type="number" + step="any" + id="horizontal-form-max-eirp" + name="horizontal-form-max-eirp" + isValid={this.hasValue(this.props.config.minPSD)} + style={{ textAlign: 'right' }} + /> + dBm/MHz + + + + + + + + + + + + + {' '} + +

Feederloss is set to:

+
    +
  • the Feederloss in the FS Database (if present)
  • +
  • Else, the applicable value below
  • +
+ + } + > + +
+ + + this.setFeederLoss({ ...this.props.config.receiverFeederLoss, IDU: Number(x) })} + type="number" + step="any" + id="horizontal-form-receiver-feeder-loss-idu" + name="horizontal-form-receiver-feeder-loss-idu" + isValid={this.hasValue(this.props.config.receiverFeederLoss.IDU)} + style={{ textAlign: 'right' }} + /> + dB + + + + this.setFeederLoss({ ...this.props.config.receiverFeederLoss, ODU: Number(x) })} + type="number" + step="any" + id="horizontal-form-receiver-feeder-loss-7" + name="horizontal-form-receiver-feeder-loss-7" + isValid={this.hasValue(this.props.config.receiverFeederLoss.ODU)} + style={{ textAlign: 'right' }} + /> + dB + + + + this.setFeederLoss({ ...this.props.config.receiverFeederLoss, UNKNOWN: Number(x) })} + type="number" + step="any" + id="horizontal-form-receiver-feeder-loss-o" + name="horizontal-form-receiver-feeder-loss-o" + isValid={this.hasValue(this.props.config.receiverFeederLoss.UNKNOWN)} + style={{ textAlign: 'right' }} + /> + dB + +
+
+ + + {' '} + +

+ The FS receiver center frequency is compared against the value shown to choose the proper noise floor +

+ + } + > + +
+ + + this.props.config.fsReceiverNoise?.noiseFloorList[0]) + ? this.props.config.fsReceiverNoise?.noiseFloorList[0] + : -110 + } + type="number" + onChange={(x) => + this.setReceiverNoise({ + ...this.props.config.fsReceiverNoise, + noiseFloorList: [Number(x), this.props.config.fsReceiverNoise.noiseFloorList[1]], + }) + } + step="any" + id="horizontal-form-noiseFloor-0" + name="horizontal-form-noiseFloor-0" + isValid={this.hasValueExists(() => this.props.config.fsReceiverNoise?.noiseFloorList[0])} + style={{ textAlign: 'right' }} + /> + dBm/MHz + + + + + this.props.config.fsReceiverNoise?.noiseFloorList[1]) + ? this.props.config.fsReceiverNoise?.noiseFloorList[1] + : -109.5 + } + onChange={(x) => + this.setReceiverNoise({ + ...this.props.config.fsReceiverNoise, + noiseFloorList: [this.props.config.fsReceiverNoise.noiseFloorList[0], Number(x)], + }) + } + step="any" + type="number" + id="horizontal-form-noiseFloor-1" + name="horizontal-form-noiseFloor-1" + isValid={this.hasValueExists(() => this.props.config.fsReceiverNoise.noiseFloorList[1])} + style={{ textAlign: 'right' }} + /> + dBm/MHz + + +
+
+ + + + + + + this.setThreshold(Number(x))} + type="number" + step="any" + id="horizontal-form-threshold" + name="horizontal-form-threshold" + isValid={this.hasValue(this.props.config.threshold)} + style={{ textAlign: 'right' }} + /> + dB + + + + + + + this.setMaxLinkDistance(Number(x))} + id="propogation-model-max-link-distance" + name="propogation-model-max-link-distance" + style={{ textAlign: 'right' }} + isValid={this.props.config.maxLinkDistance >= 1} + /> + km + + + + + + + + + + + + + {(this.props.config.propagationModel.kind === 'ITM with no building data' || + this.props.config.propagationModel.kind == 'FCC 6GHz Report & Order' || + this.props.config.propagationModel.kind == 'ISED DBS-06' || + this.props.config.propagationModel.kind == 'Brazilian Propagation Model' || + this.props.config.propagationModel.kind == 'Ofcom Propagation Model') && ( + + + {' '} + +

AP Propagation Environment:

+
    +
  • + - "Land Cover Point" assigns propagation environment based on single Land Cover tile class the + RLAN resides in: Urban if Land Cover tile = 23 or 24, Suburban if Land Cover tile = 22, Rural-D + (Deciduous trees) if Land Cover tile = 41, 43 or 90, Rural-C (Coniferous trees) if Land Cover tile + = 42, and Rural-V (Village Center) otherwise. The various Rural types correspond to the P.452 + clutter category. +
  • +
  • + - If “Land Cover Point” is not selected, Village center is assumed for the Rural clutter category. +
  • +
+ + } + > + +
+ this.setPropogationEnv(x)} + id="propogation-env" + name="propogation-env" + style={{ textAlign: 'right' }} + isValid={this.props.config.propagationEnv !== undefined} + > + + + + + + + {this.props.config.propagationEnv == 'NLCD Point' ? ( + + this.setNlcdFile(x)} + id="nlcd-database" + name="nlcd-database" + style={{ textAlign: 'right' }} + > + {this.getLandCoverOptions()} + + + ) : ( + <> + )} +
+
+ )} + {this.props.config.propagationModel.kind != 'FSPL' ? ( + + + + ) : ( + false + )} + + + When distance > 1km and FS Receiver (Rx) is in Urban/Suburban, P.2108 clutter loss is added at FS Rx + when FS Receiver AGL height < Max FS AGL Height +

+ } + > + + + + + + + +
+ {this.props.config.clutterAtFS == true ? ( + + + + % + + + ) : ( + <> + )} + {this.props.config.clutterAtFS == true ? ( + + + + m + + + ) : ( + <> + )} +
+ + + + + + {' '} + +

Min allowable AGL height = 1.5 m

+

+ Note that this is meant to mainly prevent the portion of uncertainty region to be underground due to + height uncertainty and terrain variation. +

+

+ If the AGL height of all scan points is below ground (without height uncertainty), an error will be + reported. +

+ + } + > + +
+ this.setScanBelowGround(x)} + id="horizontal-form-uls-scanBelowGround" + name="horizontal-form-uls-scanBelowGround" + isValid={!!this.props.config.scanPointBelowGroundMethod} + style={{ textAlign: 'right' }} + > + + + +
+
+ + + If enabled, the Virtual AP page will add map information via the Vendor extension and present a Google Map + on the page +

+ } + > + + + + + + + +
+
+ + If enabled, the EIRP and PSD values will be rounded down to one decimal place

} + > + + + + + + + +
+
+ + + + b && this.setChannelResponseAlgorithm('pwr')} + id="horizontal-form-channelResponse-pwr" + name="horizontal-form-channelResponse-pwr" + style={{ textAlign: 'left' }} + value="pwr" + /> + + + b && this.setChannelResponseAlgorithm('psd')} + id="horizontal-form-channelResponse-psd" + name="horizontal-form-channelResponse-psd" + style={{ textAlign: 'left' }} + value="psd" + /> + + + + + + + this.setVisiblityThreshold(Number(x))} + id="visiblity-threshold" + name="visiblity-threshold" + style={{ textAlign: 'right' }} + isValid={!!this.props.config.visibilityThreshold || this.props.config.visibilityThreshold === 0} + /> + dB I/N + + + + + {/* If enabled, the Virtual AP page will add map information via the Vendor extension and present a Google Map on the page

+ } + > */} + + + + + {/* */} + + + {/*
*/} +
+ + ); +} diff --git a/src/web/src/app/AFCConfig/APUncertaintyForm.tsx b/src/web/src/app/AFCConfig/APUncertaintyForm.tsx new file mode 100644 index 0000000..a841f0d --- /dev/null +++ b/src/web/src/app/AFCConfig/APUncertaintyForm.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; +import { + FormGroup, + FormSelect, + FormSelectOption, + TextInput, + InputGroup, + InputGroupText, + Tooltip, + TooltipPosition, + TextArea, +} from '@patternfly/react-core'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; +import { APUncertainty } from '../Lib/RatApiTypes'; + +/** + * APUncertaintyForm.tsx: sub form of afc config form + */ + +/** + * Sub for for propogation model + */ +export class APUncertaintyForm extends React.PureComponent<{ + data: APUncertainty; + onChange: (x: APUncertainty) => void; +}> { + private setPointsPerDegree = (s: string) => + // @ts-ignore + this.props.onChange({ points_per_degree: Number(s), height: this.props.data.height }); + + private setHeight = (s: string) => + // @ts-ignore + this.props.onChange({ points_per_degree: this.props.data.points_per_degree, height: Number(s) }); + /* + + */ + + render = () => ( + <> + + {' '} + +

Orientation of the AP uncertainty region is defined as follows:

+
    +
  • + - The direction of "UP" is defined by a 3D vector drawn from the center of the earth towards the AP + position. +
  • +
  • + - The "Horizontal plane" is then defined as the plane passing through the AP position that is + orthogonal to "UP". +
  • +
  • + - Scan points are placed on a rectangular X-Y grid in the "Horizontal plane" where the direction of Y + is North and the direction of X is east. +
  • +
  • + - The spacing in the Horizonal Plane is specified in points per degree. For example, 3600 points per + degree corresponds to 1arcsecond which is 30m at the equator. +
  • +
  • - The "Height" is the scanning resolution in the direction of "UP".
  • +
+ + } + > + +
+ + + + 0} + value={this.props.data.points_per_degree} + onChange={this.setPointsPerDegree} + style={{ textAlign: 'right' }} + /> + ppd + + + + 0} + value={this.props.data.height} + onChange={this.setHeight} + style={{ textAlign: 'right' }} + /> + m + +
+ + ); +} diff --git a/src/web/src/app/AFCConfig/AllowedRangesForm.tsx b/src/web/src/app/AFCConfig/AllowedRangesForm.tsx new file mode 100644 index 0000000..6b40691 --- /dev/null +++ b/src/web/src/app/AFCConfig/AllowedRangesForm.tsx @@ -0,0 +1,120 @@ +import * as React from 'react'; +import { + Alert, + FormGroup, + InputGroup, + TextInput, + InputGroupText, + AlertActionCloseButton, +} from '@patternfly/react-core'; +import { Table, TableHeader, TableBody, TableVariant } from '@patternfly/react-table'; +import { FreqRange } from '../Lib/RatApiTypes'; + +/** + * AllowedRangesForm.tsx: Displays admin defined allowed frequency ranges as readonly values + */ + +const cols = ['Name', 'Low Frequency', 'High Frequency']; + +const freqBandToRow = (f: FreqRange, index) => ({ + id: index, + cells: [f.name, f.startFreqMHz, f.stopFreqMHz], +}); + +export const getDefaultRangesByRegion = (region: string) => { + if (region.endsWith('CA')) { + return [ + { + region: 'CA', + name: 'Canada', + startFreqMHz: 5925, + stopFreqMHz: 6875, + }, + ]; + } else if (region.endsWith('BR')) { + return [ + { + region: 'BR', + name: 'Brazil', + startFreqMHz: 5925, + stopFreqMHz: 6875, + }, + ]; + } else if (region.endsWith('GB')) { + return [ + { + region: 'GB', + name: 'United Kingdom', + startFreqMHz: 5925, + stopFreqMHz: 7125, + }, + ]; + } else { + return [ + { + region: 'US', + name: 'UNII-5', + startFreqMHz: 5925, + stopFreqMHz: 6425, + }, + { + region: 'US', + name: 'UNII-7', + startFreqMHz: 6525, + stopFreqMHz: 6875, + }, + ]; + } +}; + +/** + * Sub form component for allowed freq ranges + */ +export class AllowedRangesDisplay extends React.PureComponent< + { data: FreqRange[]; region: string }, + { showWarn: boolean } +> { + constructor(props) { + super(props); + this.state = { + showWarn: !this.props.data || this.props.data.length === 0, + }; + } + + private renderTable = (datasource: FreqRange[]) => { + return ( + + + +
+ ); + }; + + render() { + let dataSource = + !this.props.data || this.props.data.length === 0 ? getDefaultRangesByRegion(this.props.region) : this.props.data; + return ( + <> + {this.state.showWarn ? ( + this.setState({ showWarn: false })} />} + > +
Falling back to default UNII-5 and UNII-7 ranges
+
+ ) : ( + false + )} + + {this.renderTable(dataSource)} + + + ); + } +} diff --git a/src/web/src/app/AFCConfig/AntennaPatternForm.tsx b/src/web/src/app/AFCConfig/AntennaPatternForm.tsx new file mode 100644 index 0000000..09de2cc --- /dev/null +++ b/src/web/src/app/AFCConfig/AntennaPatternForm.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { FormGroup, FormSelect, FormSelectOption, TextInput, Tooltip, TooltipPosition } from '@patternfly/react-core'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; +import { AntennaPatternState } from '../Lib/RatApiTypes'; + +/** + * AntennaPatternForm.tsx: sub form of afc config form + * author: Sam Smucny + */ + +/** + * Sub form component for Antenna patterns + */ +export default class AntennaPatternForm extends React.PureComponent<{ + data: AntennaPatternState; + region: string; + onChange: (x: AntennaPatternState) => void; +}> { + private setKind = (s: string) => { + switch (s) { + case 'F.1245': + this.props.onChange({ defaultAntennaPattern: 'F.1245', userUpload: this.props.data.userUpload }); + break; + case 'F.699': + this.props.onChange({ defaultAntennaPattern: 'F.699', userUpload: this.props.data.userUpload }); + break; + case 'WINNF-AIP-07': + this.props.onChange({ defaultAntennaPattern: 'WINNF-AIP-07', userUpload: this.props.data.userUpload }); + break; + case 'WINNF-AIP-07-CAN': + this.props.onChange({ defaultAntennaPattern: 'WINNF-AIP-07-CAN', userUpload: this.props.data.userUpload }); + break; + + default: + // Do nothing + } + }; + + setUserUpload = (s: string) => + this.props.onChange({ + defaultAntennaPattern: this.props.data.defaultAntennaPattern, + userUpload: s != 'None' ? { kind: 'User Upload', value: s } : { kind: s, value: '' }, + }); + + /** + * Enumeration of antenna pattern types + */ + private getPatternOptionsByRegion = (region: string) => { + if (region.endsWith('CA')) { + return ['WINNF-AIP-07-CAN', 'F.699', 'F.1245']; + } else if (region.endsWith('BR')) { + return ['WINNF-AIP-07', 'F.699', 'F.1245']; + } else { + return ['WINNF-AIP-07', 'F.699', 'F.1245']; + } + }; + + render = () => ( + + {' '} + this.setKind(x)} + id="horzontal-form-antenna" + name="horizontal-form-antenna" + isValid={this.props.data.defaultAntennaPattern !== undefined} + style={{ textAlign: 'right' }} + > + {this.getPatternOptionsByRegion(this.props.region).map((option) => ( + + ))} + + + ); +} diff --git a/src/web/src/app/AFCConfig/BodyLossForm.tsx b/src/web/src/app/AFCConfig/BodyLossForm.tsx new file mode 100644 index 0000000..3509bd6 --- /dev/null +++ b/src/web/src/app/AFCConfig/BodyLossForm.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; +import { FormGroup, InputGroup, FormSelect, FormSelectOption, TextInput, InputGroupText } from '@patternfly/react-core'; +import { BodyLossModel } from '../Lib/RatApiTypes'; + +/** + * BodyLossForm.tsx: sub form of afc config form + * author: Sam Smucny + */ + +/** + * Enumeration of penetration loss models + */ +const penetrationLossModels = ['Fixed Value']; + +/** + * Sub form component for body loss model + */ +export default class BodyLossForm extends React.PureComponent<{ + data: BodyLossModel; + onChange: (x: BodyLossModel) => void; +}> { + private setKind = (s: string) => { + switch (s) { + case 'EU': + this.props.onChange({ kind: s }); + break; + case 'Fixed Value': + this.props.onChange({ kind: s, valueIndoor: 0, valueOutdoor: 0 }); + break; + default: + // Do nothing + } + }; + + private setFixedValueIndoor = (s: string) => + // @ts-ignore + this.props.onChange({ kind: 'Fixed Value', valueIndoor: Number(s), valueOutdoor: this.props.data.valueOutdoor }); + + private setFixedValueOutdoor = (s: string) => + // @ts-ignore + this.props.onChange({ kind: 'Fixed Value', valueIndoor: this.props.data.valueIndoor, valueOutdoor: Number(s) }); + + private getExpansion = () => { + switch (this.props.data.kind) { + case 'EU': + return <>; + + case 'Fixed Value': + return ( + <> + + + 0} + value={this.props.data.valueIndoor} + onChange={this.setFixedValueIndoor} + style={{ textAlign: 'right' }} + /> + dB + + + + + 0} + value={this.props.data.valueOutdoor} + onChange={this.setFixedValueOutdoor} + style={{ textAlign: 'right' }} + /> + dB + + + + ); + + default: + return <>; + } + }; + + render() { + return ( + + {/* + this.setKind(x)} + id="horzontal-form-body-loss" + name="horizontal-form-body-loss" + isValid={this.props.data.kind !== undefined} + style={{ textAlign: "right" }} + > + {penetrationLossModels.map((option) => ( + + ))} + + */} + {this.getExpansion()} + + ); + } +} diff --git a/src/web/src/app/AFCConfig/BuildingPenetrationLossForm.tsx b/src/web/src/app/AFCConfig/BuildingPenetrationLossForm.tsx new file mode 100644 index 0000000..893bfd3 --- /dev/null +++ b/src/web/src/app/AFCConfig/BuildingPenetrationLossForm.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { + FormGroup, + InputGroup, + FormSelect, + FormSelectOption, + TextInput, + Form, + InputGroupText, +} from '@patternfly/react-core'; +import { PenetrationLossModel, P2109 } from '../Lib/RatApiTypes'; + +/** + * BuildingPenetrationLossForm.tsx: sub form of afc config form + * author: Sam Smucny + */ + +/** + * Enumeration of penetration loss types + */ +const penetrationLossModels = ['ITU-R Rec. P.2109', 'Fixed Value']; + +/** + * Sub form for building penetration loss model + */ +export default class BuildlingPenetrationLossForm extends React.PureComponent<{ + data: PenetrationLossModel; + onChange: (x: PenetrationLossModel) => void; +}> { + change: (x: PenetrationLossModel) => void; + + constructor(props) { + super(props); + + this.change = props.onChange; + } + + private setKind(s: string) { + switch (s) { + case 'ITU-R Rec. P.2109': + this.change({ kind: s, buildingType: 'Traditional', confidence: 50 }); + break; + case 'Fixed Value': + this.change({ kind: s, value: 0 }); + break; + default: + // Do nothing + } + } + + setBuildingType = (s: string) => { + if (s === 'Traditional' || s === 'Efficient') { + this.change({ kind: 'ITU-R Rec. P.2109', buildingType: s, confidence: (this.props.data as P2109).confidence }); + } + }; + + setPercentile = (s: string) => { + this.change({ + kind: 'ITU-R Rec. P.2109', + confidence: Number(s), + buildingType: (this.props.data as P2109).buildingType, + }); + }; + + setFixedValue = (s: string) => { + this.change({ kind: 'Fixed Value', value: Number(s) }); + }; + + getExpansion = () => { + switch (this.props.data.kind) { + case 'ITU-R Rec. P.2109': + return ( + <> + + + + + + + + + = 0 && this.props.data.confidence <= 100} + /> + % + + + + ); + + case 'Fixed Value': + return ( + + + + dB + + + ); + + default: + return <>; + } + }; + + render() { + return ( + + + this.setKind(x)} + id="horzontal-form-penetration-loss" + name="horizontal-form-penetration-loss" + isValid={this.props.data.kind !== undefined} + style={{ textAlign: 'right' }} + > + {penetrationLossModels.map((option) => ( + + ))} + + + {this.getExpansion()} + + ); + } +} diff --git a/src/web/src/app/AFCConfig/ITMParametersForm.tsx b/src/web/src/app/AFCConfig/ITMParametersForm.tsx new file mode 100644 index 0000000..b98947c --- /dev/null +++ b/src/web/src/app/AFCConfig/ITMParametersForm.tsx @@ -0,0 +1,225 @@ +import * as React from 'react'; +import { + FormGroup, + FormSelect, + FormSelectOption, + TextInput, + InputGroup, + InputGroupText, + Tooltip, + TooltipPosition, + TextArea, +} from '@patternfly/react-core'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; +import { ITMParameters, GroundType } from '../Lib/RatApiTypes'; + +/** + * ITMParametersForm.tsx: sub form of afc config form + */ + +/** + * Sub form for itm parameters + */ +export class ITMParametersForm extends React.PureComponent<{ + data: ITMParameters; + onChange: (x: ITMParameters) => void; +}> { + constructor(props) { + super(props); + } + + private lookUpConstants = (ground: GroundType) => { + let dielectric = -1; + let conductivity = -1; + switch (ground) { + case 'Average Ground': + dielectric = 15; + conductivity = 0.005; + break; + case 'Poor Ground': + dielectric = 4; + conductivity = 0.001; + break; + case 'Good Ground': + dielectric = 25; + conductivity = 0.02; + break; + case 'Sea Water': + dielectric = 81; + conductivity = 5.0; + break; + case 'Fresh Water': + dielectric = 81; + conductivity = 0.01; + break; + } + return { dielectricConst: dielectric, conductivity: conductivity }; + }; + + private setPolarization = (s: string) => + // @ts-ignore + this.props.onChange(Object.assign(this.props.data, { polarization: s })); + + private setGroundType = (s: string) => { + let constants = this.lookUpConstants(s as GroundType); + // @ts-ignore + this.props.onChange( + Object.assign(this.props.data, { + ground: s, + dielectricConst: constants.dielectricConst, + conductivity: constants.conductivity, + }), + ); + }; + + private setMinSpacing = (s: string) => { + // @ts-ignore + this.props.onChange(Object.assign(this.props.data, { minSpacing: Number(s) })); + }; + + private setMaxPoints = (s: string) => { + // @ts-ignore + this.props.onChange(Object.assign(this.props.data, { maxPoints: Number(s) })); + }; + + render = () => ( + <> + + {' '} + +

+ ITM Path Profile “Min Spacing” and “Max Points” are used to define the points and their uniform spacing + within the RLAN to FS RX path. The spacing is at the minimum equal to “Min Spacing” (for shorter + distances) and for larger distances when “Max Points” is reached, it will be larger. +

+

Constraints:

+
  • - "Path Profile Min Spacing" must be between 1m and 30m.​
  • +
  • - "Path Profile Max Points" must be between 100 and 10,000.
  • + + } + > + +
    + + + this.setPolarization(x)} + id="polarization-drop" + name="polarization-drop" + style={{ textAlign: 'right' }} + > + + + + + + + this.setGroundType(x)} + id="ground-type-drop" + name="ground-type-drop" + style={{ textAlign: 'right' }} + > + + + + + + + + + + + + + + + S/m + + + + this.setMinSpacing(x)} + step="any" + type="number" + isValid={this.props.data.minSpacing >= 1 && this.props.data.minSpacing <= 30} + value={this.props.data.minSpacing} + style={{ textAlign: 'right' }} + /> + m + + + + this.setMaxPoints(x)} + step="any" + type="number" + isValid={this.props.data.maxPoints >= 100 && this.props.data.maxPoints <= 10000} + value={this.props.data.maxPoints} + style={{ textAlign: 'right' }} + /> + +
    + + ); +} diff --git a/src/web/src/app/AFCConfig/PolarizationMismatchLossForm.tsx b/src/web/src/app/AFCConfig/PolarizationMismatchLossForm.tsx new file mode 100644 index 0000000..74331c0 --- /dev/null +++ b/src/web/src/app/AFCConfig/PolarizationMismatchLossForm.tsx @@ -0,0 +1,101 @@ +import * as React from 'react'; +import { FormGroup, TextInput, InputGroup, FormSelect, FormSelectOption, InputGroupText } from '@patternfly/react-core'; +import { PolarizationLossModel } from '../Lib/RatApiTypes'; + +/** + * PolarizationMismatchLossForm.tsx: sub form of afc config form + * author: Sam Smucny + */ + +/** + * Enumeration of polarization loss models + */ +const polarizationLossModels = ['Fixed Value']; + +/** + * Sub form for Polarization mismatch model + */ +export default class PolarizationMismatchLossForm extends React.PureComponent<{ + data: PolarizationLossModel; + onChange: (x: PolarizationLossModel) => void; +}> { + private change: (x: PolarizationLossModel) => void; + + constructor(props: Readonly<{ data: PolarizationLossModel; onChange: (x: PolarizationLossModel) => void }>) { + super(props); + + this.change = props.onChange; + } + + private setKind(s: string) { + switch (s) { + case 'EU': + this.change({ kind: s }); + break; + case 'Fixed Value': + this.change({ kind: s, value: 0 }); + break; + default: + // Do nothing + } + } + + setFixedValue = (s: string) => { + this.change({ kind: 'Fixed Value', value: Number(s) }); + }; + + private getExpansion(model: PolarizationLossModel) { + switch (model.kind) { + case 'EU': + return <>; + + case 'Fixed Value': + return ( + + + + dB + + + ); + + default: + return <>; + } + } + + render() { + const model = this.props.data; + + return ( + + {/* + this.setKind(x)} + id="horzontal-form-polarization-loss" + name="horizontal-form-polarization-loss" + isValid={model.kind !== undefined} + style={{ textAlign: "right" }} + > + {polarizationLossModels.map((option) => ( + + ))} + + */} + {this.getExpansion(model)} + + ); + } +} diff --git a/src/web/src/app/AFCConfig/PropogationModelForm.tsx b/src/web/src/app/AFCConfig/PropogationModelForm.tsx new file mode 100644 index 0000000..f1683a0 --- /dev/null +++ b/src/web/src/app/AFCConfig/PropogationModelForm.tsx @@ -0,0 +1,1481 @@ +import * as React from 'react'; +import { + FormGroup, + FormSelect, + FormSelectOption, + TextInput, + InputGroup, + InputGroupText, + Tooltip, + TooltipPosition, +} from '@patternfly/react-core'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; +import { + BuildingSourceValues, + CustomPropagation, + CustomPropagationLOSOptions, + FCC6GHz, + FSPL, + IsedDbs06, + PropagationModel, + Win2ItmClutter, + Win2ItmDb, + BrazilPropModel, + OfcomPropModel, +} from '../Lib/RatApiTypes'; + +/** + * PropogationModelForm.tsx: sub form of afc config form + * author: Sam Smucny + */ + +/** + * Sub for for propogation model + */ +export class PropogationModelForm extends React.PureComponent<{ + data: PropagationModel; + region: string; + onChange: (x: PropagationModel) => void; +}> { + private getModelOptionsByRegion = (region: string) => { + if (region.endsWith('CA')) { + return [ + 'FSPL', + 'ITM with building data', + //"ITM with no building data", + 'ISED DBS-06', + 'Ray Tracing', + 'Custom', + ]; + } else if (region.endsWith('BR')) { + return [ + 'FSPL', + 'ITM with building data', + //"ITM with no building data", + 'Brazilian Propagation Model', + 'Ray Tracing', + 'Custom', + ]; + } else if (region.endsWith('GB')) { + return [ + 'FSPL', + 'ITM with building data', + //"ITM with no building data", + 'Ofcom Propagation Model', + 'Ray Tracing', + 'Custom', + ]; + } else { + return [ + 'FSPL', + 'ITM with building data', + //"ITM with no building data", + 'FCC 6GHz Report & Order', + 'Ray Tracing', + 'Custom', + ]; + } + }; + + private setKind = (s: string) => { + switch (s) { + case 'FSPL': + this.props.onChange({ kind: s, fsplUseGroundDistance: false } as FSPL); + break; + case 'ITM with building data': + this.props.onChange({ + kind: s, + win2ProbLosThreshold: 10, + win2Confidence: 50, + itmConfidence: 50, + itmReliability: 50, + p2108Confidence: 50, + buildingSource: 'LiDAR', + } as Win2ItmDb); + break; + case 'ITM with no building data': + this.props.onChange({ + kind: s, + win2ProbLosThreshold: 10, + win2Confidence: 50, + itmConfidence: 50, + itmReliability: 50, + p2108Confidence: 50, + terrainSource: 'SRTM (90m)', + } as Win2ItmClutter); + break; + case 'FCC 6GHz Report & Order': + this.props.onChange({ + kind: s, + win2ConfidenceCombined: 16, + win2ConfidenceLOS: 16, + winner2LOSOption: 'BLDG_DATA_REQ_TX', + win2UseGroundDistance: false, + fsplUseGroundDistance: false, + winner2HgtFlag: false, + winner2HgtLOS: 15, + itmConfidence: 5, + itmReliability: 20, + p2108Confidence: 25, + buildingSource: 'None', + terrainSource: '3DEP (30m)', + } as FCC6GHz); + break; + case 'Ray Tracing': + break; // do nothing + case 'Custom': + this.props.onChange({ + kind: s, + winner2LOSOption: 'UNKNOWN', + win2ConfidenceCombined: 50, + itmConfidence: 50, + itmReliability: 50, + p2108Confidence: 50, + rlanITMTxClutterMethod: 'FORCE_TRUE', + buildingSource: 'None', + terrainSource: '3DEP (30m)', + win2ConfidenceLOS: 50, + win2UseGroundDistance: false, + fsplUseGroundDistance: false, + winner2HgtFlag: false, + winner2HgtLOS: 15, + } as CustomPropagation); + break; + case 'ISED DBS-06': + this.props.onChange({ + kind: s, + win2ConfidenceCombined: 16, + win2ConfidenceLOS: 50, + win2ConfidenceNLOS: 50, + winner2LOSOption: 'CDSM', + win2UseGroundDistance: false, + fsplUseGroundDistance: false, + winner2HgtFlag: false, + winner2HgtLOS: 15, + itmConfidence: 5, + itmReliability: 20, + p2108Confidence: 10, + surfaceDataSource: 'Canada DSM (2000)', + terrainSource: '3DEP (30m)', + rlanITMTxClutterMethod: 'FORCE_TRUE', + } as IsedDbs06); + break; + case 'Brazilian Propagation Model': + this.props.onChange({ + kind: s, + win2ConfidenceCombined: 50, + win2ConfidenceLOS: 50, + winner2LOSOption: 'BLDG_DATA_REQ_TX', + win2UseGroundDistance: false, + fsplUseGroundDistance: false, + winner2HgtFlag: false, + winner2HgtLOS: 50, + itmConfidence: 50, + itmReliability: 50, + p2108Confidence: 50, + buildingSource: 'None', + terrainSource: 'SRTM (30m)', + } as BrazilPropModel); + break; + case 'Ofcom Propagation Model': + this.props.onChange({ + kind: s, + win2ConfidenceCombined: 50, + win2ConfidenceLOS: 50, + winner2LOSOption: 'BLDG_DATA_REQ_TX', + win2UseGroundDistance: false, + fsplUseGroundDistance: false, + winner2HgtFlag: false, + winner2HgtLOS: 50, + itmConfidence: 50, + itmReliability: 50, + p2108Confidence: 50, + buildingSource: 'None', + terrainSource: 'SRTM (30m)', + } as OfcomPropModel); + break; + } + }; + + setWin2ConfidenceCombined = (s: string) => { + const n: number = Number(s); + this.props.onChange(Object.assign(this.props.data, { win2ConfidenceCombined: n })); + }; + + setWin2ConfidenceLOS = (s: string) => { + const n: number = Number(s); + this.props.onChange(Object.assign(this.props.data, { win2ConfidenceLOS: n })); + }; + + setWinConfidenceNLOS = (s: string) => { + const n: number = Number(s); + this.props.onChange(Object.assign(this.props.data, { win2ConfidenceNLOS: n })); + }; + + setWin2ConfidenceLOS_NLOS = (s: string) => { + const n: number = Number(s); + this.props.onChange(Object.assign(this.props.data, { win2ConfidenceLOS: n, win2ConfidenceNLOS: n })); + }; + + setItmConfidence = (s: string) => { + const n: number = Number(s); + this.props.onChange(Object.assign(this.props.data, { itmConfidence: n })); + }; + + setItmReliability = (s: string) => { + const n: number = Number(s); + this.props.onChange(Object.assign(this.props.data, { itmReliability: n })); + }; + + setP2108Confidence = (s: string) => { + const n: number = Number(s); + this.props.onChange(Object.assign(this.props.data, { p2108Confidence: n })); + }; + + setProbLOS = (s: string) => { + const n: number = Number(s); + this.props.onChange(Object.assign(this.props.data, { win2ProbLosThreshold: n })); + }; + + setBuildingSource = (s: BuildingSourceValues) => { + const model = this.props.data; + if (model.hasOwnProperty('buildingSource')) { + // Ensure that there is terrain source is set to default when there is building data + + if (model.kind === 'FCC 6GHz Report & Order') { + let newData: Partial = { buildingSource: s }; + if (model.buildingSource === 'None' && s !== 'LiDAR') { + newData.terrainSource = '3DEP (30m)'; + } + + if (s == 'LiDAR') { + if (!model.win2ConfidenceLOS) { + newData.win2ConfidenceLOS = 50; + } + if (!model.win2ConfidenceNLOS) { + newData.win2ConfidenceNLOS = 50; + } + } + if (s === 'None') { + newData.winner2LOSOption = 'UNKNOWN'; + newData.win2ConfidenceLOS = 50; + } else { + newData.winner2LOSOption = 'BLDG_DATA_REQ_TX'; + } + + this.props.onChange(Object.assign(this.props.data, newData)); + } else if (model.kind === 'Brazilian Propagation Model' || model.kind === 'Ofcom Propagation Model') { + let newData: Partial = { buildingSource: s }; + if (model.buildingSource === 'None' && s !== 'LiDAR') { + newData.terrainSource = 'SRTM (30m)'; + } + if (s == 'LiDAR') { + if (!model.win2ConfidenceLOS) { + newData.win2ConfidenceLOS = 50; + } + if (!model.win2ConfidenceNLOS) { + newData.win2ConfidenceNLOS = 50; + } + } + if (s === 'None') { + newData.winner2LOSOption = 'UNKNOWN'; + newData.win2ConfidenceLOS = 50; + } else { + newData.winner2LOSOption = 'BLDG_DATA_REQ_TX'; + } + this.props.onChange(Object.assign(this.props.data, newData)); + } else { + if ((model as Win2ItmDb | CustomPropagation).buildingSource === 'None' && s !== 'None') { + this.props.onChange(Object.assign(this.props.data, { buildingSource: s, terrainSource: '3DEP (30m)' })); + } else { + this.props.onChange(Object.assign(this.props.data, { buildingSource: s })); + } + } + } + }; + + setTerrainSource = (s: string) => { + this.props.onChange(Object.assign(this.props.data, { terrainSource: s })); + }; + + setSurfaceDataSource = (s: string) => { + this.props.onChange(Object.assign(this.props.data, { surfaceDataSource: s })); + }; + + setLosOption = (s: string) => { + let newLos = s as CustomPropagationLOSOptions; + + const model = this.props.data as CustomPropagation; + let newModel: Partial = { winner2LOSOption: newLos }; + + switch (newLos) { + case 'BLDG_DATA_REQ_TX': + if (!model.win2ConfidenceLOS) { + newModel.win2ConfidenceLOS = 50; + } + if (!model.win2ConfidenceNLOS) { + newModel.win2ConfidenceNLOS = 50; + } + + case 'FORCE_LOS': + if (model.winner2LOSOption === 'BLDG_DATA_REQ_TX') { + newModel.buildingSource = 'None'; + newModel.terrainSource = '3DEP (30m)'; + } + if (!model.win2ConfidenceLOS) { + newModel.win2ConfidenceLOS = 50; + } + + case 'FORCE_NLOS': + if (model.winner2LOSOption === 'BLDG_DATA_REQ_TX') { + newModel.buildingSource = 'None'; + newModel.terrainSource = '3DEP (30m)'; + } + if (!model.win2ConfidenceNLOS) { + newModel.win2ConfidenceNLOS = 50; + } + + case 'UNKNOWN': + if (!model.win2ConfidenceCombined) { + newModel.win2ConfidenceCombined = 50; + } + if (!model.win2ConfidenceLOS) { + newModel.win2ConfidenceLOS = 50; + } + if (model.winner2LOSOption === 'BLDG_DATA_REQ_TX') { + newModel.buildingSource = 'None'; + newModel.terrainSource = '3DEP (30m)'; + } + } + + this.props.onChange(Object.assign(this.props.data, newModel)); + }; + + setItmClutterMethod = (s: string) => { + const model = this.props.data as CustomPropagation; + if (model.rlanITMTxClutterMethod === 'BLDG_DATA' && s !== 'BLDG_DATA') { + this.props.onChange( + Object.assign(this.props.data, { + rlanITMTxClutterMethod: s, + buildingSource: 'None', + terrainSource: '3DEP (30m)', + }), + ); + } else { + this.props.onChange(Object.assign(this.props.data, { rlanITMTxClutterMethod: s })); + } + }; + renderCustomLosNlosConfidence(model: CustomPropagation) { + switch (model.winner2LOSOption) { + case 'UNKNOWN': + //Combined, show nothing + return ( + <> + + + = 0 && model.win2ConfidenceCombined <= 100} + /> + % + + + + + { + this.setWin2ConfidenceLOS(v); + }} + id="propogation-model-win-los-confidence" + name="propogation-model-win-los-confidence" + style={{ textAlign: 'right' }} + isValid={model.win2ConfidenceLOS >= 0 && model.win2ConfidenceLOS <= 100} + /> + % + + + + ); + case 'FORCE_LOS': + return ( + <> + + + { + this.setWin2ConfidenceLOS(v); + }} + id="propogation-model-win-los-confidence" + name="propogation-model-win-los-confidence" + style={{ textAlign: 'right' }} + isValid={model.win2ConfidenceLOS >= 0 && model.win2ConfidenceLOS <= 100} + /> + % + + + + ); + case 'FORCE_NLOS': + return ( + <> + + + { + this.setWinConfidenceNLOS(v); + }} + id="propogation-model-win-nlos-confidence" + name="propogation-model-win-nlos-confidence" + style={{ textAlign: 'right' }} + isValid={model.win2ConfidenceNLOS >= 0 && model.win2ConfidenceNLOS <= 100} + /> + % + + + + ); + case 'BLDG_DATA_REQ_TX': + return ( + <> + + + = 0 && model.win2ConfidenceCombined <= 100} + /> + % + + + + + + { + this.setWin2ConfidenceLOS(v); + this.setWinConfidenceNLOS(v); + }} + id="propogation-model-win-los-nlos-confidence" + name="propogation-model-win-los-nlos-confidence" + style={{ textAlign: 'right' }} + isValid={model.win2ConfidenceLOS >= 0 && model.win2ConfidenceLOS <= 100} + /> + % + + + + ); + } + } + + getExpansion = () => { + const model = this.props.data; + switch (model.kind) { + case 'FSPL': + return <>; + case 'ITM with building data': + return ( + <> + + + = 0 && model.itmConfidence <= 100} + /> + % + + + + + = 0 && model.itmReliability <= 100} + /> + % + + + + this.setBuildingSource(v as BuildingSourceValues)} + id="propogation-model-data-source" + name="propogation-model-data-source" + style={{ textAlign: 'right' }} + isValid={model.buildingSource === 'LiDAR' || model.buildingSource === 'B-Design3D'} + > + + + + + + ); + /* case "ITM with no building data": + return <> + = 0 && model.win2ProbLosThreshold <= 100} /> + % + + = 0 && model.win2Confidence <= 100} /> + % + + = 0 && model.itmConfidence <= 100} /> + % + + + = 0 && model.itmReliability <= 100} /> + % + + = 0 && model.p2108Confidence <= 100} /> + % + + + + + + + + */ + case 'FCC 6GHz Report & Order': + return ( + <> + + + = 0 && model.win2ConfidenceCombined <= 100} + /> + % + + + {model.buildingSource === 'LiDAR' ? ( + <> + + + { + this.setWin2ConfidenceLOS_NLOS(v); + }} + id="propogation-model-win-los-nlos-confidence" + name="propogation-model-win-los-nlos-confidence" + style={{ textAlign: 'right' }} + isValid={model.win2ConfidenceLOS >= 0 && model.win2ConfidenceLOS <= 100} + /> + % + + + + ) : ( + <> + )} + {model.buildingSource === 'None' ? ( + <> + + + { + this.setWin2ConfidenceLOS(v); + }} + id="propogation-model-win-los-confidence" + name="propogation-model-win-los-confidence" + style={{ textAlign: 'right' }} + isValid={model.win2ConfidenceLOS >= 0 && model.win2ConfidenceLOS <= 100} + /> + % + + + + ) : ( + <> + )} + + + = 0 && model.itmConfidence <= 100} + /> + % + + + + + = 0 && model.itmReliability <= 100} + /> + % + + + + + = 0 && model.p2108Confidence <= 100} + /> + % + + + + this.setBuildingSource(v as BuildingSourceValues)} + id="propogation-model-data-source" + name="propogation-model-data-source" + style={{ textAlign: 'right' }} + isValid={ + model.buildingSource === 'LiDAR' || + model.buildingSource === 'B-Design3D' || + model.buildingSource === 'None' + } + > + + + + + + {model.buildingSource === 'None' ? ( + + + + + + + ) : ( + false + )} + + ); + case 'Ray Tracing': + return <>; + case 'Custom': + return ( + <> + + {' '} + +

    + If LoS/NLoS per building data is selected, and if the RLAN is in a region where + there is building database, then terrain plus building database is used to determine whether the + path is LoS or not. Otherwise, the Combined LoS/NLoS model is used. By the same logic, if the{' '} + Building Data Source is None, the Combined LoS/NLoS model is always used.{' '} +

    + + } + > + +
    + + + + + + +
    + + {this.renderCustomLosNlosConfidence(model)} + + {' '} + +

    + If Clutter/No clutter per building is selected, the path is determined to be LoS + or NLoS based on terrain and/or building database (if available). By the same logic, if the{' '} + Building Data Source is None, blockage is determined based solely on terrain + database. +

    + + } + > + +
    + + + + + +
    + + + = 0 && model.itmConfidence <= 100} + /> + % + + + + + = 0 && model.itmReliability <= 100} + /> + % + + + + + = 0 && model.p2108Confidence <= 100} + /> + % + + + + {model.rlanITMTxClutterMethod === 'BLDG_DATA' || model.winner2LOSOption === 'BLDG_DATA_REQ_TX' ? ( + <> + + this.setBuildingSource(v as BuildingSourceValues)} + id="propogation-model-data-source" + name="propogation-model-data-source" + style={{ textAlign: 'right' }} + isValid={ + model.buildingSource === 'LiDAR' || + model.buildingSource === 'B-Design3D' || + model.buildingSource === 'None' + } + > + + + + + + + + {' '} + +

    + The higher terrain height resolution (that goes with the building database) is used instead + within the first 1 km (when WinnerII building data is chosen) and greater than 1 km (when ITM + with building data is chosen). +

    + + } + > + +
    + + + + +
    + + ) : ( + false + )} + + ); + case 'ISED DBS-06': + return ( + <> + + {' '} + +

    + If LoS/NLoS per surface data is selected, and if the RLAN is in a region where + there is surface database, then terrain plus surface database is used to determine whether the + path is LoS or not. Otherwise, the Combined LoS/NLoS model is used. By the same logic, if the{' '} + Surface Data Source is None, the Combined LoS/NLoS model is always used.{' '} +

    + + } + > + +
    + + + +
    + + + + = 0 && model.win2ConfidenceCombined <= 100} + /> + % + + + + + + { + this.setWin2ConfidenceLOS(v); + this.setWinConfidenceNLOS(v); + }} + id="propogation-model-win-los-nlos-confidence" + name="propogation-model-win-los-nlos-confidence" + style={{ textAlign: 'right' }} + isValid={model.win2ConfidenceLOS >= 0 && model.win2ConfidenceLOS <= 100} + /> + % + + + + + {' '} + +

    + If Clutter/No clutter per building is selected, the path is determined to be LoS + or NLoS based on terrain and/or building database (if available). By the same logic, if the{' '} + Building Data Source is None, blockage is determined based solely on terrain + database. +

    + + } + > + +
    + + + {/* + */} + +
    + + + = 0 && model.itmConfidence <= 100} + /> + % + + + + + = 0 && model.itmReliability <= 100} + /> + % + + + + + = 0 && model.p2108Confidence <= 100} + /> + % + + + + + {/* {" "} +

    The higher terrain height resolution (that goes with the building database) + is used instead within the first 1 km (when WinnerII building data is chosen) + and greater than 1 km (when ITM with building data is chosen).

    + + } + > + +
    */} + + + + +
    + + + {' '} + +

    + The higher terrain height resolution (that goes with the building database) is used instead within + the first 1 km (when WinnerII building data is chosen) and greater than 1 km (when ITM with + building data is chosen). +

    + + } + > + +
    + + + + +
    + + ); + case 'Brazilian Propagation Model': + return ( + <> + + + = 0 && model.win2ConfidenceCombined <= 100} + /> + % + + + {model.buildingSource === 'LiDAR' ? ( + <> + + + { + this.setWin2ConfidenceLOS_NLOS(v); + }} + id="propogation-model-win-los-nlos-confidence" + name="propogation-model-win-los-nlos-confidence" + style={{ textAlign: 'right' }} + isValid={model.win2ConfidenceLOS >= 0 && model.win2ConfidenceLOS <= 100} + /> + % + + + + ) : ( + <> + )} + {model.buildingSource === 'None' ? ( + <> + + + { + this.setWin2ConfidenceLOS(v); + }} + id="propogation-model-win-los-confidence" + name="propogation-model-win-los-confidence" + style={{ textAlign: 'right' }} + isValid={model.win2ConfidenceLOS >= 0 && model.win2ConfidenceLOS <= 100} + /> + % + + + + ) : ( + <> + )} + + + = 0 && model.itmConfidence <= 100} + /> + % + + + + + = 0 && model.itmReliability <= 100} + /> + % + + + + + = 0 && model.p2108Confidence <= 100} + /> + % + + + + this.setBuildingSource(v as BuildingSourceValues)} + id="propogation-model-data-source" + name="propogation-model-data-source" + style={{ textAlign: 'right' }} + isValid={model.buildingSource === 'None'} + > + + + + {model.buildingSource === 'None' ? ( + + + + + + ) : ( + false + )} + + ); + case 'Ofcom Propagation Model': + return ( + <> + + + = 0 && model.win2ConfidenceCombined <= 100} + /> + % + + + {model.buildingSource === 'LiDAR' ? ( + <> + + + { + this.setWin2ConfidenceLOS_NLOS(v); + }} + id="propogation-model-win-los-nlos-confidence" + name="propogation-model-win-los-nlos-confidence" + style={{ textAlign: 'right' }} + isValid={model.win2ConfidenceLOS >= 0 && model.win2ConfidenceLOS <= 100} + /> + % + + + + ) : ( + <> + )} + {model.buildingSource === 'None' ? ( + <> + + + { + this.setWin2ConfidenceLOS(v); + }} + id="propogation-model-win-los-confidence" + name="propogation-model-win-los-confidence" + style={{ textAlign: 'right' }} + isValid={model.win2ConfidenceLOS >= 0 && model.win2ConfidenceLOS <= 100} + /> + % + + + + ) : ( + <> + )} + + + = 0 && model.itmConfidence <= 100} + /> + % + + + + + = 0 && model.itmReliability <= 100} + /> + % + + + + + = 0 && model.p2108Confidence <= 100} + /> + % + + + + this.setBuildingSource(v as BuildingSourceValues)} + id="propogation-model-data-source" + name="propogation-model-data-source" + style={{ textAlign: 'right' }} + isValid={model.buildingSource === 'None'} + > + + + + {model.buildingSource === 'None' ? ( + + + + + + ) : ( + false + )} + + ); + + default: + throw 'Invalid propogation model'; + } + }; + + render = () => ( + + {' '} + +

    For "ITM with no building data," the following model is used:

    +

    Urban/Suburban RLAN:

    +
      +
    • - Distance from FS < 1 km: Winner II
    • +
    • - Distance from FS > 1 km: ITM + P.2108 Clutter
    • +
    +

    Rural/Barren RLAN: ITM + P.456 Clutter

    +
    +

    For "FCC 6 GHz Report & Order", the following model is used:

    +
      +
    • - Path loss < FSPL is clamped to FSPL
    • +
    • - Distance < 30m: FSPL
    • +
    • - 30m <= Distance <= 50m: WinnerII LOS Urban/Suburban/Rural
    • +
    • + - 50m < Distance < 1km: Combined WinnerII (in absence of building database); WinnerII LOS or NLOS + (in presence of building database) +
    • +
    • + - Distance > 1km: ITM + [P.2108 Clutter (Urban/Suburban) or P.452 Clutter (Rural) (in absence of + building database)]; in presence of building database, if no obstruction is found in the path, no + clutter is applied; otherwise, clutter is applied per above. +
    • +
    • + - For the terrain source, the highest resolution selected terrain source is used (1m LiDAR -> 30m + 3DEP -> 90m SRTM -> 1km Globe) +
    • +
    + + } + > + +
    + this.setKind(x)} + id="horzontal-form-propogation-model" + name="horizontal-form-propogation-model" + isValid={this.props.data.kind !== undefined} + style={{ textAlign: 'right' }} + > + + {this.getModelOptionsByRegion(this.props.region).map((option) => ( + + ))} + + {this.getExpansion()} +
    + ); +} diff --git a/src/web/src/app/About/About.tsx b/src/web/src/app/About/About.tsx new file mode 100644 index 0000000..9a1e4ee --- /dev/null +++ b/src/web/src/app/About/About.tsx @@ -0,0 +1,126 @@ +import * as React from 'react'; +import { + Title, + Card, + CardHead, + CardHeader, + CardBody, + PageSection, + InputGroup, + TextInput, + FormGroup, + Button, + Alert, + AlertActionCloseButton, +} from '@patternfly/react-core'; +import { logger } from '../Lib/Logger'; +import ReCAPTCHA from 'react-google-recaptcha'; +import { setAboutAfc } from '../Lib/RatApi'; +import { PlusCircleIcon } from '@patternfly/react-icons'; + +export class About extends React.Component { + constructor(props: Readonly<{ content: RatResponse; sitekey: string }>) { + super(); + this.state = { content: props.content.result, name: '', email: '', org: '', token: '', sitekey: props.sitekey }; + logger.info('ABOUT state: ' + this.state.content); + } + + onChange = (value) => { + this.setState({ token: value }); + }; + + private submit() { + if (!this.state.token && this.state.sitekey) { + this.setState({ messageType: 'danger', messageValue: 'Captcha completion required ' }); + return; + } + if (!this.state.name || !this.state.email || !this.state.org) { + this.setState({ messageType: 'danger', messageValue: 'User information is required' }); + return; + // + } + + setAboutAfc(this.state.name, this.state.email, this.state.org, this.state.token).then((res) => { + if (res.kind === 'Success') { + this.setState({ + messageType: 'success', + messageValue: 'Request for ' + this.state.email + ' has been submitted', + }); + } else { + this.setState({ messageType: 'danger', messageValue: res.description }); + } + }); + } + + private hideAlert = () => this.setState({ messageType: undefined }); + + render() { + const nameChange = (s?: string) => this.setState({ name: s }); + const emailChange = (s?: string) => this.setState({ email: s }); + const orgChange = (s?: string) => this.setState({ org: s }); + + return ( + +
    + Request Access to the AFC Website + + {' '} + + + + + + + + + + + + + + + + + + <> + {this.state.messageType && ( + } + /> + )} + +
    + + + + {this.state.sitekey && } +
    +
    + ); + } +} diff --git a/src/web/src/app/Admin/Admin.tsx b/src/web/src/app/Admin/Admin.tsx new file mode 100644 index 0000000..82a81b7 --- /dev/null +++ b/src/web/src/app/Admin/Admin.tsx @@ -0,0 +1,546 @@ +import * as React from 'react'; +import { + PageSection, + Card, + CardHead, + CardBody, + CardHeader, + Modal, + Button, + FormGroup, + FormSelect, + FormSelectOption, + Alert, + AlertActionCloseButton, + InputGroup, + Gallery, + GalleryItem, +} from '@patternfly/react-core'; +import { UserTable } from './UserList'; +import { Role, UserState } from '../Lib/User'; +import { getUsers, addUserRole, deleteUser, removeUserRole, setMinimumEIRP, Limit } from '../Lib/Admin'; +import { logger } from '../Lib/Logger'; +import { UserAccount } from '../UserAccount/UserAccount'; +import { UserModel, RatResponse, FreqRange } from '../Lib/RatApiTypes'; +import { Table, TableHeader, TableBody, TableVariant } from '@patternfly/react-table'; +import { FrequencyRangeInput } from './FrequencyRangeInput'; +import { UserContext, hasRole } from '../Lib/User'; +import { updateAllAllowedRanges } from '../Lib/RatApi'; + +/** + * Admin.tsx: Administration page for managing users + * author: Sam Smucny + */ + +/** + * Possible roles enumerated + */ +const sroles: Role[] = ['AP', 'Analysis', 'Admin', 'Super', 'Trial']; +const roles: Role[] = ['AP', 'Analysis', 'Admin', 'Trial']; + +const cols = ['Region', 'Name', 'Low Frequency (MHz)', 'High Frequency (MHz)']; + +const freqBandToRow = (f: FreqRange, index) => ({ + id: index, + cells: [f.region, f.name, f.startFreqMHz, f.stopFreqMHz], +}); + +/** + * Administrator tab for managing users + */ +export class Admin extends React.Component< + { users: RatResponse; regions: RatResponse }, + { + users: UserModel[]; + userId?: number; + addRoleModalOpen: boolean; + newRole?: Role; + removeRoleModalOpen: boolean; + removeRole?: Role; + deleteModalOpen: boolean; + userModalOpen: boolean; + limit: Limit; + ulsLastSuccessRuntime: string; + parseInProgress: boolean; + frequencyBands: FreqRange[]; + editingFrequency: boolean; + frequencyEditIndex?: number; + messageSuccess?: string; + messageError?: string; + messageUlsSuccess?: string; + messageUlsError?: string; + messageUls?: string; + userEnteredUpdateTime?: string; + regionsList: string[]; + frequencyBandsNeedSaving: boolean; + } +> { + constructor( + props: Readonly<{ + users: RatResponse; + limit: RatResponse; + frequencyBands: RatResponse; + regions: RatResponse; + }>, + ) { + super(props); + + if (props.users.kind === 'Error') logger.error(props.users); + + const userList = props.users.kind === 'Success' ? props.users.result : []; + const apiLimit = props.limit.kind === 'Success' ? props.limit.result : new Limit(false, false, 18, -102); + const apiFreqBands = props.frequencyBands.kind === 'Success' ? props.frequencyBands.result : []; + const regionsList = props.regions.kind === 'Success' ? props.regions.result : ['US']; + + this.state = { + users: userList, + addRoleModalOpen: false, + removeRoleModalOpen: false, + deleteModalOpen: false, + userModalOpen: false, + limit: apiLimit, + parseInProgress: false, + ulsLastSuccessRuntime: '', + messageSuccess: undefined, + messageError: undefined, + userEnteredUpdateTime: '00:00', + editingFrequency: false, + frequencyEditIndex: undefined, + frequencyBands: apiFreqBands, + regionsList: regionsList, + frequencyBandsNeedSaving: false, + }; + this.init = this.init.bind(this); + this.init(); + } + + /** + * Function used to cancel a long running task + */ + cancelTask?: () => void; + + init() {} + + /** + * Delete the user with `this.state.userId` + */ + private deleteUser() { + logger.info('Deleting user: ', this.state.userId); + if (!this.state.userId) return; + deleteUser(this.state.userId).then(async (res) => { + if (res.kind === 'Success') { + const users = await getUsers(); + if (users.kind === 'Error') { + logger.error(users); + return; + } + this.handleDeleteModalToggle(); + this.setState({ users: users.result, deleteModalOpen: false }); + } else { + logger.error(res); + } + }); + } + + /** + * Add `this.state.newRole` to `this.state.userId` + */ + private addRole() { + logger.info('Adding role: ', this.state.newRole); + if (!this.state.userId || !this.state.newRole) return; + addUserRole(this.state.userId, this.state.newRole).then(async (res) => { + if (res.kind === 'Success') { + const users = await getUsers(); + if (users.kind === 'Error') { + logger.error(users); + return; + } + this.setState({ users: users.result }); + this.handleAddRoleModalToggle(); + } else { + logger.error(res); + } + }); + } + + /** + * Remove `this.state.removeRole` from `this.state.userId` + */ + private removeRole() { + logger.info('Removing role: ', this.state.removeRole); + if (!this.state.userId || !this.state.removeRole) return; + removeUserRole(this.state.userId, this.state.removeRole).then(async (res) => { + if (res.kind === 'Success') { + const users = await getUsers(); + if (users.kind === 'Error') { + logger.error(users); + return; + } + this.handleRemoveRoleModalToggle(); + this.setState({ users: users.result }); + } else { + logger.error(res); + } + }); + } + + /** + * Updates user data after an edit + */ + private async userEdited() { + const users = await getUsers(); + if (users.kind === 'Error') { + logger.error(users); + return; + } + this.setState({ users: users.result }); + this.handleUserModalToggle(); + } + + private handleAddRoleModalToggle = (id?: number) => + this.setState((s) => ({ userId: id, addRoleModalOpen: !s.addRoleModalOpen })); + private handleRemoveRoleModalToggle = (id?: number) => + this.setState((s) => ({ userId: id, removeRoleModalOpen: !s.removeRoleModalOpen })); + private handleDeleteModalToggle = (id?: number) => + this.setState((s) => ({ userId: id, deleteModalOpen: !s.deleteModalOpen })); + private handleUserModalToggle = (id?: number) => + this.setState((s) => ({ userId: id, userModalOpen: !s.userModalOpen })); + private updateEnforceLimit = (value: boolean, isIndoor: boolean) => { + let newState = { ...this.state.limit }; + if (isIndoor) { + newState.indoorEnforce = value; + } else { + newState.outdoorEnforce = value; + } + this.setState({ limit: newState }); + }; + private updateEirpLimit = (value: number, isIndoor: boolean) => { + let newState = { ...this.state.limit }; + if (isIndoor) { + newState.indoorLimit = value; + } else { + newState.outdoorLimit = value; + } + this.setState({ limit: newState }); + }; + private submitMinEIRP = () => { + setMinimumEIRP(this.state.limit).then((res) => { + let successMessage = 'Updated minimum EIRP values'; + if (res.kind == 'Success') { + this.setState({ messageSuccess: successMessage, messageError: undefined }); + } else { + this.setState({ messageError: 'Unable to update limits.', messageSuccess: undefined }); + } + }); + }; + + private removeFreqBand = (index: number) => { + const { frequencyBands } = this.state; + frequencyBands.splice(index, 1); + this.setState({ frequencyBands: frequencyBands, frequencyBandsNeedSaving: true }); + }; + + private addNewBand = () => { + const { frequencyBands } = this.state; + let newBand = { name: '', startFreqMHz: 5925, stopFreqMHz: 6425, region: 'US' } as FreqRange; + frequencyBands.push(newBand); + this.setState({ frequencyBands: frequencyBands }); + }; + + private mapBandsToAllAllowedRanges = (bands: FreqRange[]) => { + let m = new Map(); + let regions = new Set(bands.map((x) => x.region)); + regions.forEach((x) => { + if (x !== undefined) { + m.set(x, []); + } + }); + bands.forEach((f) => { + if (f.region !== undefined) { + m.get(f.region)?.push(f); + } + }); + return m; + }; + + private putFrequencyBands = () => { + updateAllAllowedRanges(this.state.frequencyBands).then((res) => { + if (res.kind == 'Success') { + this.setState({ + messageSuccess: 'Updated allowed frequency', + messageError: undefined, + frequencyBandsNeedSaving: false, + }); + } else { + this.setState({ messageError: 'Unable to get current frequency ranges.', messageSuccess: undefined }); + } + }); + }; + + actionResolver(data: any, extraData: any) { + return [ + { + title: 'Edit Range', + onClick: (event: any, rowId: number, rowData: any, extra: any) => + this.setState({ editingFrequency: true, frequencyEditIndex: rowId }), + }, + { + title: 'Delete', + onClick: (event: any, rowId: any, rowData: any, extra: any) => this.removeFreqBand(rowId), + }, + ]; + } + + private updateTableEntry = (freq: FreqRange) => { + const { frequencyEditIndex, frequencyBands } = this.state; + frequencyBands[frequencyEditIndex] = freq; + this.setState({ frequencyBands: frequencyBands }); + }; + + private renderFrequencyTable = () => { + return ( + this.actionResolver(a, b)} + variant={TableVariant.compact} + cells={cols as any} + rows={this.state.frequencyBands.map((fr, idx) => freqBandToRow(fr, idx))} + > + + +
    + ); + }; + + render() { + return ( + + + + Limits + + + + + {this.state.limit.indoorEnforce ? ( + <> + this.updateEnforceLimit(e.target.checked, true)} + /> + +
    + +
    + this.updateEirpLimit(Number(event.target.value), true)} + id="min__Indoor_EIRP" + type="number" + /> + dBm + + ) : ( + <> + this.updateEnforceLimit(e.target.checked, true)} + /> + +
    + + )} +
    + + {this.state.limit.outdoorEnforce ? ( + <> + this.updateEnforceLimit(e.target.checked, false)} + /> + +
    + +
    + this.updateEirpLimit(Number(event.target.value), false)} + id="min__outdoor_EIRP" + type="number" + /> + dBm + + ) : ( + <> + this.updateEnforceLimit(e.target.checked, false)} + /> + +
    + + )} +
    +
    + +
    +
    +
    + + + Allowed Frequency band(s) + + + + {this.state.frequencyBands ? this.renderFrequencyTable() : false} + + + + + + + + this.setState({ editingFrequency: false, frequencyEditIndex: undefined })} + > + { + this.updateTableEntry(freq); + this.setState({ + editingFrequency: false, + frequencyEditIndex: undefined, + frequencyBandsNeedSaving: true, + }); + }} + /> + + +
    + <> + {this.state.messageError !== undefined && ( + this.setState({ messageError: undefined })} />} + > + {this.state.messageError} + + )} + + <> + {this.state.messageSuccess !== undefined && ( + this.setState({ messageSuccess: undefined })} />} + > + {this.state.messageSuccess} + + )} + +
    + + + Users + + + + + this.addRole()}> + Confirm + , + ]} + > + + this.setState({ newRole: x as Role })} + id="new-role" + name="new-role" + > + ( + {hasRole('Super') + ? sroles.map((r) => ) + : roles.map((r) => )} + ) + + + + this.removeRole()}> + Confirm + , + ]} + > + + this.setState({ removeRole: x as Role })} + id="remove-role" + name="remove-role" + > + + {roles.map((r) => ( + + ))} + + + + this.deleteUser()}> + Confirm + , + ]} + > + Are you sure you want to delete{' '} + {this.state.deleteModalOpen && this.state.users.find((x) => x.id === this.state.userId)?.email}? + + + this.userEdited()} /> + + +
    +
    + ); + } +} diff --git a/src/web/src/app/Admin/FrequencyRangeInput.tsx b/src/web/src/app/Admin/FrequencyRangeInput.tsx new file mode 100644 index 0000000..2a17867 --- /dev/null +++ b/src/web/src/app/Admin/FrequencyRangeInput.tsx @@ -0,0 +1,157 @@ +import * as React from 'react'; +import { + Card, + CardHead, + CardHeader, + CardBody, + PageSection, + FormGroup, + TextInput, + Title, + ActionGroup, + Button, + Alert, + AlertActionCloseButton, + InputGroupText, + InputGroup, + Checkbox, + FormSelect, + FormSelectOption, +} from '@patternfly/react-core'; +import { FreqRange } from '../Lib/RatApiTypes'; +import { mapRegionCodeToName } from '../Lib/Utils'; + +// FrequencyRangeInput.tsx: Page where frequency bands can be modified + +interface FrequencyRangeProps { + frequencyRange: FreqRange; + regions: string[]; + onSuccess: (freq: FreqRange) => void; +} + +interface FrequencyRangeState { + frequencyRange: FreqRange; +} + +export class FrequencyRangeInput extends React.Component { + constructor(props: FrequencyRangeProps) { + super(props); + + this.state = { + frequencyRange: { ...props.frequencyRange }, + }; + } + + private updateField = (newData: string, fieldToUpdate: string) => { + let freq = { ...this.state.frequencyRange }; + if (fieldToUpdate == 'name' || fieldToUpdate == 'region') { + freq[fieldToUpdate] = newData; + } else { + // the only other fields are all numbers + freq[fieldToUpdate] = Number(newData); + } + + this.setState({ frequencyRange: freq }); + }; + + private submitBand = () => { + const isValid = + this.state.frequencyRange.name.length > 0 && + this.state.frequencyRange.startFreqMHz > 0 && + this.state.frequencyRange.stopFreqMHz > this.state.frequencyRange.startFreqMHz; + if (isValid) { + this.props.onSuccess(this.state.frequencyRange); + } else { + alert('One or more fields invalid.'); + } + }; + + render() { + return ( + + + + + + 0} + onChange={(data) => this.updateField(data, 'region')} + > + {this.props.regions.map((option: string) => ( + + ))} + + + + + + 0} + onChange={(data) => this.updateField(data, 'name')} + /> + + + + 0} + onChange={(data) => this.updateField(data, 'startFreqMHz')} + /> + MHz + + + + this.state.frequencyRange.startFreqMHz} + onChange={(data) => this.updateField(data, 'stopFreqMHz')} + /> + MHz + + + + + + ); + } +} diff --git a/src/web/src/app/Admin/UserList.tsx b/src/web/src/app/Admin/UserList.tsx new file mode 100644 index 0000000..7cd6243 --- /dev/null +++ b/src/web/src/app/Admin/UserList.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { UserModel } from '../Lib/RatApiTypes'; +import { Table, TableHeader, TableBody, headerCol, TableVariant } from '@patternfly/react-table'; + +/** + * UserList.tsx: Table of users with actions + * author: Sam Smucny + */ + +/** + * mock data + */ +export const testUsers: UserModel[] = [ + { + id: 1, + email: 'a@domain.com', + firstName: 'Bob', + active: true, + roles: ['Analysis', 'AP'], + }, +]; + +interface UserTableProps { + onDelete: (id: number) => void; + onRoleAdd: (id: number) => void; + onRoleRemove: (id: number) => void; + onUserEdit: (id: number) => void; + users: UserModel[]; +} + +const userToRow = (u: UserModel) => ({ + id: u.id, + cells: [u.email, u.org, u.active ? 'Y' : 'N', u.roles.join(', ')], +}); + +/** + * Table component to show users + */ +export class UserTable extends React.Component { + private columns = [ + { title: 'Email', cellTransforms: [headerCol()] }, + { title: 'Org' }, + { title: 'Active' }, + { title: 'Roles' }, + ]; + + constructor(props: UserTableProps) { + super(props); + this.state = { + rows: [], + }; + } + + actionResolver(data: any, extraData: any) { + return [ + { + title: 'Edit User', + onClick: (event: any, rowId: number, rowData: any, extra: any) => this.props.onUserEdit(rowData.id), + }, + { + title: 'Add Role', + onClick: (event: any, rowId: number, rowData: any, extra: any) => this.props.onRoleAdd(rowData.id), + }, + { + title: 'Remove Role', + onClick: (event: any, rowId: number, rowData: any, extra: any) => this.props.onRoleRemove(rowData.id), + }, + { + title: 'Delete', + onClick: (event: any, rowId: any, rowData: any, extra: any) => this.props.onDelete(rowData.id), + }, + ]; + } + + render() { + return ( + this.actionResolver(a, b)} + // areActionsDisabled={this.areActionsDisabled} + variant={TableVariant.compact} + > + + +
    + ); + } +} diff --git a/src/web/src/app/AppLayout/AppInfo.tsx b/src/web/src/app/AppLayout/AppInfo.tsx new file mode 100644 index 0000000..621f424 --- /dev/null +++ b/src/web/src/app/AppLayout/AppInfo.tsx @@ -0,0 +1,180 @@ +import React, { useState } from 'react'; +import { AboutModal, Button, TextContent, TextList, TextListItem } from '@patternfly/react-core'; +import { Accordion, AccordionItem, AccordionContent, AccordionToggle } from '@patternfly/react-core'; +import { guiConfig } from '../Lib/RatApi'; +import { InfoIcon } from '@patternfly/react-icons'; + +/** + * AppInfo.tsx: Application about modal with version information + * author: Sam Smucny + */ + +/** + * Modal component that provides information about the application + */ +class AppInfo extends React.Component<{}, { isModalOpen: boolean; expanded: string[] }> { + setExpanded(newExpanded: string[]) { + this.setState(Object.assign({ expanded: newExpanded })); + } + handleModalToggle: () => void; + + constructor(props: any) { + super(props); + this.state = { + isModalOpen: false, + expanded: [], + }; + this.handleModalToggle = () => { + this.setState(({ isModalOpen }) => ({ + isModalOpen: !isModalOpen, + })); + }; + } + + toggle = (id) => { + const index = this.state.expanded.indexOf(id); + const newExpanded: string[] = + index >= 0 + ? [...this.state.expanded.slice(0, index), ...this.state.expanded.slice(index + 1, this.state.expanded.length)] + : [...this.state.expanded, id]; + this.setExpanded(newExpanded); + }; + + render() { + const { isModalOpen } = this.state; + + return ( + + + + + + Version + {guiConfig.version} + + +

    Database Licences and Source Citations

    + + + this.toggle('ex2-toggle1')} + isExpanded={this.state.expanded.includes('ex2-toggle1')} + id="ex2-toggle1" + > + NLCD + + +

    Public domain

    +

    References:

    +

    + Dewitz, J., and U.S. Geological Survey, 2021, National Land Cover Database (NLCD) 2019 Products (ver. + 2.0, June 2021): U.S. Geological Survey data release, https://doi.org/10.5066/P9KZCM54 +

    +

    + Wickham, J., Stehman, S.V., Sorenson, D.G., Gass, L., and Dewitz, J.A., 2021, Thematic accuracy + assessment of the NLCD 2016 land cover for the conterminous United States: Remote Sensing of + Environment, v. 257, art. no. 112357, at https://doi.org/10.1016/j.rse.2021.112357 +

    +

    + Homer, Collin G., Dewitz, Jon A., Jin, Suming, Xian, George, Costello, C., Danielson, Patrick, Gass, + L., Funk, M., Wickham, J., Stehman, S., Auch, Roger F., Riitters, K. H., Conterminous United States + land cover change patterns 2001–2016 from the 2016 National Land Cover Database: ISPRS Journal of + Photogrammetry and Remote Sensing, v. 162, p. 184–199, at + https://doi.org/10.1016/j.isprsjprs.2020.02.019 +

    +

    + Jin, Suming, Homer, Collin, Yang, Limin, Danielson, Patrick, Dewitz, Jon, Li, Congcong, Zhu, Z., Xian, + George, Howard, Danny, Overall methodology design for the United States National Land Cover Database + 2016 products: Remote Sensing, v. 11, no. 24, at https://doi.org/10.3390/rs11242971 +

    +

    + Yang, L., Jin, S., Danielson, P., Homer, C., Gass, L., Case, A., Costello, C., Dewitz, J., Fry, J., + Funk, M., Grannemann, B., Rigge, M. and G. Xian. 2018. A New Generation of the United States National + Land Cover Database: Requirements, Research Priorities, Design, and Implementation Strategies, ISPRS + Journal of Photogrammetry and Remote Sensing, 146, pp.108-123. +

    +
    +
    + + this.toggle('ex2-toggle2')} + isExpanded={this.state.expanded.includes('ex2-toggle2')} + id="ex2-toggle2" + > + proc_lidar_2019 + + +

    Available for public use with no restrictions

    +

    + Disclaimer and quality information is at + https://rockyweb.usgs.gov/vdelivery/Datasets/Staged/Elevation/Non_Standard_Contributed/NGA_US_Cities/00_NGA%20133%20US%20Cities%20Data%20Disclaimer%20and%20Explanation%20Readme.pdf +

    +
    +
    + + this.toggle('ex2-toggle3')} + isExpanded={this.state.expanded.includes('ex2-toggle3')} + id="ex2-toggle3" + > + 3DEP + + +

    Public domain

    +

    Data available from U.S. Geological Survey, National Geospatial Program.

    +
    +
    + + this.toggle('ex2-toggle4')} + isExpanded={this.state.expanded.includes('ex2-toggle4')} + id="ex2-toggle4" + > + Globe + + +

    Public domain

    +

    + NOAA National Geophysical Data Center. 1999: Global Land One-kilometer Base Elevation (GLOBE) v.1. + NOAA National Centers for Environmental Information. https://doi.org/10.7289/V52R3PMS. Accessed TBD +

    +
    +
    + + this.toggle('ex2-toggle5')} + isExpanded={this.state.expanded.includes('ex2-toggle5')} + id="ex2-toggle5" + > + population + + +

    + Creative Commons Attribution 4.0 International (CC BY) License + (https://creativecommons.org/licenses/by/4.0) +

    +

    + Center for International Earth Science Information Network - CIESIN - Columbia University. 2018. + Gridded Population of the World, Version 4 (GPWv4): Population Density, Revision 11. Palisades, New + York: NASA Socioeconomic Data and Applications Center (SEDAC). https://doi.org/10.7927/H49C6VHW +

    +
    +
    +
    +
    +
    + ); + } +} + +export { AppInfo }; diff --git a/src/web/src/app/AppLayout/AppLayout.tsx b/src/web/src/app/AppLayout/AppLayout.tsx new file mode 100644 index 0000000..a67b45c --- /dev/null +++ b/src/web/src/app/AppLayout/AppLayout.tsx @@ -0,0 +1,242 @@ +import * as React from 'react'; +import '@patternfly/react-core/dist/styles/base.css'; +import { NavLink, withRouter } from 'react-router-dom'; +import { + Nav, + NavList, + NavItem, + NavItemSeparator, + NavVariants, + Page, + PageHeader, + PageSidebar, + SkipToContent, + Button, + Avatar, +} from '@patternfly/react-core'; +import '@app/app.css'; +import { guiConfig } from '../Lib/RatApi'; +import { AppInfo } from './AppInfo'; +import { LoginAvatar } from './LoginAvatar'; +import { UserContext, isAdmin, UserState, hasRole, isLoggedIn } from '../Lib/User'; + +/** + * AppLayout.tsx: defines navigation regions and buttons on app (sidebar/ header) + * author: Sam Smucny + */ + +/** + * Interface definition common to app layouts + */ +interface IAppLayout { + children: React.ReactNode; +} + +/** + * Wrapper component used to render navigation + * @param children Interior components to render + */ +const AppLayout: React.FunctionComponent = ({ children }) => { + const logoProps = { + href: '/', + target: '_self', + }; + const [isNavOpen, setIsNavOpen] = React.useState(true); + const [isMobileView, setIsMobileView] = React.useState(true); + const [isNavOpenMobile, setIsNavOpenMobile] = React.useState(false); + const onNavToggleMobile = () => { + setIsNavOpenMobile(!isNavOpenMobile); + }; + const onNavToggle = () => { + setIsNavOpen(!isNavOpen); + }; + const onPageResize = (props: { mobileView: boolean; windowSize: number }) => { + setIsMobileView(props.mobileView); + }; + + const topNav = ( + + ); + + const Header = ( + } + showNavToggle={true} + isNavOpen={isNavOpen} + onNavToggle={isMobileView ? onNavToggleMobile : onNavToggle} + avatar={} + /> + ); + + // @ts-ignore + const uls: any = ( + + ULS Databases + + ); + // @ts-ignore + const antenna: any = ( + + Antenna Patterns + + ); + // @ts-ignore + const history: any = ( + + Debug Files + + ); + // @ts-ignore + + const showAbout = () => guiConfig.about_url; + + const Navigation = ( + + {(user) => ( + + )} + + ); + const Sidebar = ; + const PageSkipToContent = Skip to Content; + return ( + + {children} + + ); +}; + +export { AppLayout }; diff --git a/src/web/src/app/AppLayout/AppLogin.tsx b/src/web/src/app/AppLayout/AppLogin.tsx new file mode 100644 index 0000000..3292747 --- /dev/null +++ b/src/web/src/app/AppLayout/AppLogin.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import * as React from 'react'; +import { + Title, + Card, + CardHead, + CardHeader, + CardBody, + PageSection, + InputGroup, + TextInput, + FormGroup, + Button, + Alert, + AlertActionCloseButton, +} from '@patternfly/react-core'; +import { guiConfig, getAboutLoginAfc } from '../Lib/RatApi'; + +/** + * AppLogin.ts: Login page for web site + * author: patternfly, Sam Smucny + */ + +// Currently there is no logo/background on app pages, so if we want to brand the site this is one place to do it +// Note: When using background-filter.svg, you must also include #image_overlay as the fragment identifier + +/** + * Application login page + */ +class AppLoginPage extends React.Component { + constructor(props: any) { + super(props); + this.state = { content: '' }; + getAboutLoginAfc().then((res) => + res.kind === 'Success' + ? this.setState({ content: res.result }) + : this.setState({ + messageType: 'danger', + messageValue: res.description, + }), + ); + } + + private hideAlert = () => this.setState({ messageType: undefined }); + + render() { + // the commented lines are not being used. If we want to add functionality for them at a later date then uncomment + return ( + +
    + AFC Login + + {' '} + + + <> + {this.state.messageType && ( + } + /> + )} + +
    +
    + ); + } +} + +export default AppLoginPage; diff --git a/src/web/src/app/AppLayout/LoginAvatar.tsx b/src/web/src/app/AppLayout/LoginAvatar.tsx new file mode 100644 index 0000000..78286a9 --- /dev/null +++ b/src/web/src/app/AppLayout/LoginAvatar.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { Button } from '@patternfly/react-core'; +import { Route } from 'react-router'; +import { isLoggedIn, logout, login, UserContext } from '../Lib/User'; +import { UserIcon } from '@patternfly/react-icons'; +import { UnknownIcon } from '@patternfly/react-icons'; +import { guiConfig } from '../Lib/RatApi'; + +/** + * LoginAvatar.ts: component for top right of page to login/logout + * author: Sam Smucny + */ + +/** + * Icon that indicates login status. If clicked, it either + * redirects to login page or logs out current user. + */ +export class LoginAvatar extends React.Component { + private LoginUrl = () => { + if (!guiConfig.about_url) { + return ( + + + + ); + } else { + return ( + ( + + )} + /> + ); + } + }; + + render() { + const showLogin = this.LoginUrl(); + + const showLogout = ( + + + + ); + + return {(user) => (isLoggedIn() ? showLogout : showLogin)}; + } +} diff --git a/src/web/src/app/Components/AnalysisProgress.tsx b/src/web/src/app/Components/AnalysisProgress.tsx new file mode 100644 index 0000000..631e252 --- /dev/null +++ b/src/web/src/app/Components/AnalysisProgress.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { Progress, ProgressMeasureLocation } from '@patternfly/react-core'; + +/** + * AnalysisProgress.tsx: Progress bar with updates for heat map task + * author: Sam Smucny + */ + +/** + * Progress bar properties + */ +interface ProgressProps { + percent: number; + message: string; +} + +/** + * Progress bar + * @param props `ProgressProps` + */ +export const AnalysisProgress: React.FunctionComponent = (props: ProgressProps) => ( + +); diff --git a/src/web/src/app/Components/ChannelDisplay.tsx b/src/web/src/app/Components/ChannelDisplay.tsx new file mode 100644 index 0000000..d3d991d --- /dev/null +++ b/src/web/src/app/Components/ChannelDisplay.tsx @@ -0,0 +1,279 @@ +import * as React from 'react'; +import { Stage, Layer, Text, Shape, Rect, Line } from 'react-konva'; +import { ChannelData } from '../Lib/RatApiTypes'; +import { KonvaEventObject } from 'konva/types/Node'; + +/** + * ChannelDisplay.tsx: Draws channel shapes on page and colors/labels them according to props + * author: Sam Smucny + */ + +/** + * Channel set up data + * If changes are made here they need to be made in SpectrumDisplay.tsx as well + */ +const emptyChannels: ChannelData[] = [ + { + channelWidth: 20, + startFrequency: 5945, + channels: Array(59) + .fill(0) + .map((_, i) => ({ name: String(1 + 4 * i), color: 'grey', maxEIRP: 0 })), + }, + { + channelWidth: 40, + startFrequency: 5945, + channels: Array(29) + .fill(0) + .map((_, i) => ({ name: String(3 + 8 * i), color: 'grey', maxEIRP: 0 })), + }, + { + channelWidth: 80, + startFrequency: 5945, + channels: Array(14) + .fill(0) + .map((_, i) => ({ name: String(7 + 16 * i), color: 'grey', maxEIRP: 0 })), + }, + { + channelWidth: 160, + startFrequency: 5945, + channels: Array(7) + .fill(0) + .map((_, i) => ({ name: String(15 + 32 * i), color: 'grey', maxEIRP: 0 })), + }, + { + channelWidth: 20, + startFrequency: 5925, + channels: Array(1) + .fill(0) + .map((_, i) => ({ name: String(2 + 4 * i), color: 'grey', maxEIRP: 0 })), + }, + { + channelWidth: 320, + startFrequency: 5945, + channels: Array(3) + .fill(0) + .map((_, i) => ({ name: String(31 + 64 * i), color: 'grey', maxEIRP: 0 })), + }, + { + channelWidth: 320, + startFrequency: 6105, + channels: Array(3) + .fill(0) + .map((_, i) => ({ name: String(63 + 64 * i), color: 'grey', maxEIRP: 0 })), + }, +]; + +interface ChannelProps { + start: number; + vertical: number; + height: number; + key: number; + end: number; + color: string; + name: string; + maxEIRP?: number; + stageSize: { width: number; height: number }; +} + +/** + * Fundamental component that is used as building block + */ +class Channel extends React.Component { + constructor(props: ChannelProps) { + super(props); + this.state = { showToolTip: false }; + } + + private handleMouseEnter = (evt: KonvaEventObject) => { + this.setState({ showToolTip: true }); + }; + + private handleMouseOut = (evt: KonvaEventObject) => { + this.setState({ showToolTip: false }); + }; + + render = () => ( + <> + { + context.beginPath(); + context.moveTo(this.props.start, this.props.vertical + this.props.height); + context.lineTo(this.props.start + 5, this.props.vertical); + context.lineTo(this.props.end - 5, this.props.vertical); + context.lineTo(this.props.end, this.props.vertical + this.props.height); + context.closePath(); + context.fillStrokeShape(shape); + }} + fill={this.props.color} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseOut} + stroke="black" + strokeWidth={2} + /> + {this.state.showToolTip ? ( + <> + + + + ) : ( + <> + )} + + ); +} + +interface ChannelArrayProps { + topLeft: { x: number; y: number }; + height: number; + channelWidth: number; + startOffset: number; + channels: { name: string; color: string; maxEIRP?: number }[]; + stageSize: { width: number; height: number }; +} + +/** + * One row of channels + * @param props `ChannelArrayProps` + */ +const ChannelArray: React.FunctionComponent = (props: ChannelArrayProps) => ( + <> + {props.channels.map((chan, i) => ( + + ))} + +); + +interface ChannelDisplayProps { + channels?: ChannelData[]; + topLeft: { x: number; y: number }; + channelHeight: number; + totalWidth: number; +} + +/** + * helper method to determine size of components + * @returns value to scale drawing by + */ +const calcScaleFactor = (props: ChannelDisplayProps): number => { + const maxWidth = props + .channels! // will only be called after props.channels !== undefined + .map((row) => row.channelWidth * row.channels.length + (row.startFrequency - startFreq)) // get total size + .reduce((a, b) => (a > b ? a : b)); // maximum + const f = (0.98 * props.totalWidth) / maxWidth; + return f; +}; + +const startFreq = 5925; +const lines = Array(15).fill(0); + +/** + * top level component that renders multiple rows of channels + */ +const ChannelDisplay: React.FunctionComponent = (props) => { + if (props.channels === undefined) return <>; + + const scaleFactor = calcScaleFactor(props); + return ( + + + {lines + .map((_, i) => ( + + )) + .concat([ + , + ])} + {lines + .map((_, i) => ( + + )) + .concat([ + , + ])} + {props.channels.map((row, i) => ( + + ))} + + + ); +}; + +export { ChannelDisplay, emptyChannels }; diff --git a/src/web/src/app/Components/DownloadContents.tsx b/src/web/src/app/Components/DownloadContents.tsx new file mode 100644 index 0000000..fb518f2 --- /dev/null +++ b/src/web/src/app/Components/DownloadContents.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; + +/** + * React component that renders a button which can be used to download a file from the server + * @param contents Binary data to download + * @param fileName Human readable name file will be given when downloaded + */ +class DownloadContents extends React.Component<{ contents: () => Blob; fileName: string }> { + constructor(props: Readonly<{ contents: () => Blob; fileName: string }>) { + super(props); + } + + downloadFile = () => { + var data = this.props.contents(); + const url = window.URL.createObjectURL(new Blob([data], { type: 'application/vnd' })); + const a = document.createElement('a'); + a.href = url; + a.download = this.props.fileName; + a.click(); + }; + + render() { + return ( + + ); + } +} + +export default DownloadContents; diff --git a/src/web/src/app/Components/DownloadFile.tsx b/src/web/src/app/Components/DownloadFile.tsx new file mode 100644 index 0000000..80546fb --- /dev/null +++ b/src/web/src/app/Components/DownloadFile.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; + +/** + * React component that renders a button which can be used to download a file from the server + * @param url URL to resource to download + * @param fileName Human readable name file will be given when downloaded + */ +class DownloadFile extends React.Component<{ url: string; fileName: string }> { + constructor(props: Readonly<{ url: string; fileName: string }>) { + super(props); + } + + downloadFile = () => { + fetch(this.props.url).then((response) => { + response.blob().then((blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = this.props.fileName; + a.click(); + }); + }); + }; + + render() { + return ( + + ); + } +} + +export default DownloadFile; diff --git a/src/web/src/app/Components/JsonRawDisp.tsx b/src/web/src/app/Components/JsonRawDisp.tsx new file mode 100644 index 0000000..fc8a543 --- /dev/null +++ b/src/web/src/app/Components/JsonRawDisp.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { Card, CardHeader, CardBody, ClipboardCopy, ClipboardCopyVariant } from '@patternfly/react-core'; + +/** + * JsonRawDisp.tsx: Simple raw JSON display that can be used for debugging + * author: Sam Smucny + */ + +/** + * Displays JSON object in an expandable text area that can be coppied from. + * @param props JSON value to display + */ +export const JsonRawDisp: React.FunctionComponent<{ value?: any }> = (props) => + props.value === undefined || props.value === null ? ( + + ) : ( + + Raw JSON + + + {!!props.value && typeof props.value === 'string' ? props.value : JSON.stringify(props.value, undefined, 2)} + + + + ); diff --git a/src/web/src/app/Components/LoadLidarBounds.tsx b/src/web/src/app/Components/LoadLidarBounds.tsx new file mode 100644 index 0000000..0d45e3e --- /dev/null +++ b/src/web/src/app/Components/LoadLidarBounds.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { GeoJson } from '../Lib/RatApiTypes'; +import { guiConfig } from '../Lib/RatApi'; + +/** + * React component that renders a button which loads LiDAR Bounds onto Google Maps feature list + */ +class LoadLidarBounds extends React.Component<{ currentGeoJson: GeoJson; onLoad: (data: GeoJson) => void }> { + constructor(props: Readonly<{ currentGeoJson: GeoJson; onLoad: (data: GeoJson) => void }>) { + super(props); + } + + downloadFile = () => { + fetch(guiConfig.lidar_bounds).then(async (response) => { + const data: GeoJson = await response.json(); + let newJson: GeoJson = JSON.parse(JSON.stringify(this.props.currentGeoJson)); + newJson.features.push(...data.features); + this.props.onLoad(newJson); + }); + }; + + render() { + return ( + + ); + } +} + +export default LoadLidarBounds; diff --git a/src/web/src/app/Components/LoadRasBounds.tsx b/src/web/src/app/Components/LoadRasBounds.tsx new file mode 100644 index 0000000..7ed2487 --- /dev/null +++ b/src/web/src/app/Components/LoadRasBounds.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { GeoJson } from '../Lib/RatApiTypes'; +import { guiConfig } from '../Lib/RatApi'; + +/** + * React component that renders a button which loads RAS Bounds onto Google Maps feature list + */ +class LoadRasBounds extends React.Component<{ currentGeoJson: GeoJson; onLoad: (data: GeoJson) => void }> { + constructor(props: Readonly<{ currentGeoJson: GeoJson; onLoad: (data: GeoJson) => void }>) { + super(props); + } + + downloadFile = () => { + fetch(guiConfig.ras_bounds).then(async (response) => { + const data: GeoJson = await response.json(); + let newJson: GeoJson = JSON.parse(JSON.stringify(this.props.currentGeoJson)); + newJson.features.push(...data.features); + this.props.onLoad(newJson); + }); + }; + + render() { + return ( + + ); + } +} + +export default LoadRasBounds; diff --git a/src/web/src/app/Components/MapContainer.tsx b/src/web/src/app/Components/MapContainer.tsx new file mode 100644 index 0000000..cd254a7 --- /dev/null +++ b/src/web/src/app/Components/MapContainer.tsx @@ -0,0 +1,629 @@ +import React, { CSSProperties } from 'react'; +import GoogleMapReact from 'google-map-react'; +import { guiConfig } from '../Lib/RatApi'; +import { GeoJson } from '../Lib/RatApiTypes'; +import { logger } from '../Lib/Logger'; + +/** + * MapContainer.tsx: Wrapper for google map component so that our app can communicate with geoJson + * author: Sam Smucny + */ + +/** + * Test data + */ +const rectTest: GeoJson = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { + ItoN: -43.34, + kind: 'HMAP', + indoor: 'Y', + }, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-82.84790039062499, 38.09133660751176], + [-80.74951171875, 38.09133660751176], + [-80.74951171875, 39.51251701659638], + [-82.84790039062499, 39.51251701659638], + [-82.84790039062499, 38.09133660751176], + ], + ], + }, + }, + ], +}; + +/** + * Properties to be passed to `MapContainer` + */ +interface MapProps { + /** + * geographic data to render on map. To rerender + * assign a new object to the property. Do not edit + * the existing object. + */ + geoJson: GeoJson; + + /** + * Bounds of heat map region + */ + selectionRectangle?: { + north: number; + south: number; + east: number; + west: number; + }; + + /** + * Location of RLAN in Point Analysis + */ + markerPosition?: { + lat: number; + lng: number; + }; + + /** + * Color of marker + */ + markerColor?: string; + + /** + * Callback when user changes `selectionRectangle` from the + * Google maps drawer + */ + onRectUpdate?: (rect: any) => void; + + /** + * Callback when user changes `markerPosition` from the + * Google maps drawer + */ + onMarkerUpdate?: (lat: number, lon: number) => void; + + /** + * Initial center of map + */ + center: { + lat: number; + lng: number; + }; + + /** + * The specified mode enables/disables certain features + */ + mode: 'Point' | 'Exclusion' | 'Heatmap' | 'Mobile'; + + /** + * Initial zoom level + */ + zoom: number; + + /** + * Styles to apply to map items. + * The key specifies the kind of the `GeoJson`. + * The value is either a dictionary of CSS classes + * or a function that produces such a dictionary + * given a Google Map feature object + */ + styles?: Map CSSProperties)>; + + /** + * URL to a kml file to display on the map + */ + kmlUrl?: string; + + /** + * Version counter used by component to indicate when it should update. + */ + versionId: number; +} + +/** + * URL keys interface that specifies which Google libraries to use + */ +interface URLKeys { + key: string; + libraries?: string; +} + +/** + * Wrapper around Google Map React component. + * Adds additional functionality by working directly with Google Maps API + */ +class MapContainer extends React.Component { + /** + * Google maps map object + */ + private map: any; + + /** + * Google maps API object + */ + private maps: any; + + /** + * FS markers + */ + private markers: any[]; + + /** + * Special RLAN marker + */ + private rlanMarker: any; + + /** + * Heat map bounding rectangle + */ + private rectangle: any; + + /** + * parameters to pass to Google Maps API + */ + private urlParams: URLKeys; + + /** + * Current center of map + */ + private center?: { lat: number; lng: number }; + + /** + * Current zoom of map + */ + private zoom?: number; + + /** + * Tool tip popup over heat map tiles + */ + private infoWindow?: any; + + /** + * Reference to the GeoJson that is being displayed. + * Used to check if GeoJson should be updated which + * is a costly redraw procedure. + */ + private currentGeoJson?: GeoJson; + + /** + * Stores map KML layer + */ + private kmlLayer?: any; + + constructor(props: any) { + super(props); + this.map = undefined; + this.maps = undefined; + this.markers = []; + this.rectangle = undefined; + this.center = undefined; + this.zoom = undefined; + this.currentGeoJson = undefined; + + this.urlParams = { key: guiConfig.google_apikey }; + this.urlParams.libraries = 'drawing'; + } + + /** + * Handler for when google map is loaded. + * This is when we can call geoJson functions + * and do a bunch of initialization of the map + */ + private apiIsLoaded = (map: any, maps: any) => { + this.map = map; + this.maps = maps; + this.map.setMapTypeId('satellite'); + this.map.data.setStyle( + this.props.styles + ? (feature: any) => { + const kind = feature.getProperty('kind'); + // @ts-ignore + if (this.props.styles.has(kind)) + // @ts-ignore + const style = this.props.styles.get(kind); + return style instanceof Function ? style(feature) : style; + } + : {}, + ); + + // update Marker, Rectangle, GeoJson, and set center/zoom + this.componentDidUpdate(); + + // Point analysis specific initialization + if (this.props.mode === 'Point') { + const drawingManager = new this.maps.drawing.DrawingManager({ + drawingMode: this.maps.drawing.OverlayType.MARKER, + drawingControl: true, + drawingControlOptions: { + position: this.maps.ControlPosition.TOP_CENTER, + drawingModes: ['marker'], + }, + markerOptions: { + clickable: false, + label: 'R', + title: 'RLAN', + zIndex: 100, + }, + }); + drawingManager.setMap(map); + + this.maps.event.addListener(drawingManager, 'markercomplete', (marker: any) => { + // remove existing marker + if (this.rlanMarker) this.rlanMarker.setMap(null); + + // add new marker + this.rlanMarker = marker; + if (this.props.onMarkerUpdate) + this.props.onMarkerUpdate(this.rlanMarker.getPosition().lat(), this.rlanMarker.getPosition().lng()); + }); + } + + // Heat map specific initalization + if (this.props.mode === 'Heatmap') { + const drawingManager = new this.maps.drawing.DrawingManager({ + drawingMode: this.maps.drawing.OverlayType.RECTANGLE, + drawingControl: true, + drawingControlOptions: { + position: this.maps.ControlPosition.TOP_CENTER, + drawingModes: ['rectangle'], + }, + rectangleOptions: { + editable: false, + draggable: false, + color: 'yellow', + fillOpacity: 0.1, + map: this.map, + }, + }); + drawingManager.setMap(map); + this.maps.event.addListener(drawingManager, 'rectanglecomplete', (rect: any) => { + if (this.props.onRectUpdate) { + // remove existing rectangle if it exists + if (this.rectangle) this.rectangle.setMap(null); + + // set new rectangle + this.rectangle = rect; + this.props.onRectUpdate(this.rectangle); + } + }); + + // listeners for showing tile info on hover + this.map.data.addListener('mouseover', (a: any) => { + if (a.feature.getProperty('kind') !== 'HMAP') return; + logger.info('Showing heat map info: ', a); + const points: { lat: number; lng: number }[] = a.feature + .getGeometry() + .getArray()[0] + .getArray() + .map((x: any) => x.toJSON()); + const lat = points.map((x) => x.lat).reduce((x, y) => x + y) / points.length; + const lng = points.map((x) => x.lng).reduce((x, y) => x + y) / points.length; + let content = '

    < minEIRP

    '; + if (!!a.feature.getProperty('ItoN')) { + content = + '

    I/N: ' + + a.feature.getProperty('ItoN').toFixed(2) + + '

    ' + + (a.feature.getProperty('indoor') === 'Y' ? 'Indoors' : 'Outdoors') + + '

    '; + } + if (!!a.feature.getProperty('eirpLimit')) { + content = + '

    EIRP Limit: ' + + a.feature.getProperty('eirpLimit') + + '

    ' + + (a.feature.getProperty('indoor') === 'Y' ? 'Indoors' : 'Outdoors') + + '

    '; + } + const infoAnchor = { lat: lat, lng: lng }; + if (this.infoWindow) { + this.infoWindow.setContent(content); + this.infoWindow.setPosition(infoAnchor); + } else { + this.infoWindow = new this.maps.InfoWindow({ + content: content, + map: this.map, + position: infoAnchor, + disableAutoPan: true, + }); + } + this.infoWindow.open(this.map); + }); + this.map.data.addListener('mouseout', (a: any) => { + if (a.feature.getProperty('kind') !== 'HMAP') return; + logger.info('Closing heat map info: ', a); + if (this.infoWindow) this.infoWindow.close(); + }); + } + }; + + /** + * Called when Google API is finished loading. Triggers initialization. + */ + private onLoad = ({ map, maps }: { map: any; maps: any }) => this.apiIsLoaded(map, maps); + + /** + * Update the `GeoJson` if it has changed + */ + private updateGeoJson() { + if (this.map && this.map !== null) { + if (this.rectangle) { + this.rectangle.setVisible(false); + this.rectangle.setMap(null); + this.rectangle = undefined; + } else if (this.props.selectionRectangle) { + const rect = new this.maps.Rectangle({ + strokeColor: '#000000', + fillColor: '#000000', + bounds: this.props.selectionRectangle, + clickable: false, + fillOpacity: 0, + map: this.map, + }); + this.rectangle = rect; + } + + // short circuit update function to avoid costly redraw if no change in object + // props.geoJson object should not be mutated. If it is changed the value is + // set to a new object (result of HTTP response) + if (this.currentGeoJson === this.props.geoJson) return; + + this.currentGeoJson = this.props.geoJson; + + // remove current geographical data from map + this.map.data.forEach((f: any) => this.map.data.remove(f)); + this.markers.forEach((m) => { + m.setMap(null); + }); + this.markers.length = 0; // empty array + + // add new features + this.map.data.addGeoJson(this.props.geoJson); // add polygons + + // render additional features + this.props.geoJson.features.forEach((poly) => { + if (poly.properties.kind === 'FS') { + // for each fs, add marker + const existingMarker = this.markers.find( + (x) => + x.getPosition().lat() === poly.geometry.coordinates[0][0][1] && + x.getPosition().lng() === poly.geometry.coordinates[0][0][0], + ); + if (existingMarker) { + const newTitle = + [ + 'FSID: ' + poly.properties.FSID, + 'Start Freq: ' + poly.properties.startFreq.toFixed(2) + ' MHz', + 'Stop Freq: ' + poly.properties.stopFreq.toFixed(2) + ' MHz', + ].join('\n') + + '\n\n' + + existingMarker.getTitle(); + existingMarker.setTitle(newTitle); + } else { + this.markers.push( + new this.maps.Marker({ + map: this.map, + position: { + // use the first coordinate of polygon as FS location + // @ts-ignore + lat: poly.geometry.coordinates[0][0][1], + // @ts-ignore + lng: poly.geometry.coordinates[0][0][0], + }, + title: [ + 'FSID: ' + poly.properties.FSID, + 'Start Freq: ' + poly.properties.startFreq.toFixed(2) + ' MHz', + 'Stop Freq: ' + poly.properties.stopFreq.toFixed(2) + ' MHz', + ].join('\n'), + }), + ); + } + } else if (poly.properties.kind === 'ZONE') { + // add marker for the FS at center of exclusion zone + const zone = new this.maps.Marker({ + map: this.map, + position: { + lat: poly.properties.lat, + lng: poly.properties.lon, + }, + title: [ + 'FSID: ' + poly.properties.FSID, + 'Terrain height: ' + poly.properties.terrainHeight + ' m', + 'Height (AGL): ' + poly.properties.height + ' m', + ].join('\n'), + zIndex: 100, + }); + this.markers.push(zone); + this.center = zone.getPosition(); + this.zoom = 16; + } + }); + } + } + + /** + * Update the Heat Map boundary + */ + private updateRect() { + if (this.props.selectionRectangle && this.maps) { + if (this.rectangle) { + this.rectangle.setBounds(this.props.selectionRectangle); + } else { + this.rectangle = new this.maps.Rectangle({ + bounds: this.props.selectionRectangle, + editable: false, + map: this.map, + visible: true, + zIndex: 3, + }); + } + if (this.map) { + this.map.fitBounds(this.rectangle.getBounds()); + this.center = this.map.getCenter(); + this.zoom = this.map.getZoom(); + } + logger.info('Updating rect bounds: ', this.rectangle.getBounds().toJSON()); + } + } + + /** + * Update the Point Analysis marker + */ + private updateMarker() { + if ( + this.props.markerPosition === undefined || + !Number.isFinite(this.props.markerPosition.lng) || + !Number.isFinite(this.props.markerPosition.lat) || + !this.maps + ) + return; + if (this.props.onMarkerUpdate) { + const same = + this.rlanMarker && + this.rlanMarker.getPosition().lat() === this.props.markerPosition.lat && + this.rlanMarker.getPosition().lng() === this.props.markerPosition.lng; + if (this.rlanMarker && !same) { + this.rlanMarker.setPosition(this.props.markerPosition); + } else if (!this.rlanMarker) { + this.rlanMarker = new this.maps.Marker({ + clickable: false, + map: this.map, + visible: true, + zIndex: 100, + label: 'R', + title: 'RLAN', + position: this.props.markerPosition, + }); + this.center = this.props.markerPosition; + this.zoom = 17; + } + } else if (this.props.markerPosition) { + // called for mobile AP + if (this.rlanMarker) { + if (this.props.markerColor) { + const circleRLAN = { + path: this.maps.SymbolPath.CIRCLE, + scale: 5, + fillOpacity: 1, + fillColor: this.props.markerColor, + strokeColor: this.props.markerColor, + }; + this.rlanMarker.setIcon(circleRLAN); + } + this.rlanMarker.setPosition(this.props.markerPosition); + this.center = this.props.markerPosition; + this.zoom = this.map.getZoom(); + } else { + const circleRLAN = { + path: this.maps.SymbolPath.CIRCLE, + scale: 5, + fillOpacity: 1, + fillColor: this.props.markerColor || 'blue', + strokeColor: this.props.markerColor || 'blue', + }; + this.rlanMarker = new this.maps.Marker({ + clickable: false, + map: this.map, + visible: true, + zIndex: 100, + title: 'MOBILE AP', + icon: circleRLAN, + position: this.props.markerPosition, + }); + this.center = this.props.markerPosition; + this.zoom = 17; + } + } + } + + /** + * Update google maps with new KML layer + */ + private updateKml(newKml: string) { + if (this.kmlLayer && this.props.kmlUrl !== newKml) { + // KML layer already exists, update + this.kmlLayer.setMap(null); + this.kmlLayer = new this.maps.KmlLayer(newKml, { + suppressInfoWindows: true, + preserveViewport: false, + map: this.map, + }); + } else if (!this.kmlLayer) { + // make new layer + this.kmlLayer = new this.maps.KmlLayer(newKml, { + suppressInfoWindows: true, + preserveViewport: false, + map: this.map, + }); + } + } + + /** + * @override + * @param nextProps If `nextProps.versionId` is different then update + */ + shouldComponentUpdate(nextProps: MapProps) { + const update = nextProps.versionId !== this.props.versionId; + if (nextProps.markerPosition && nextProps.markerPosition !== this.props.markerPosition) { + this.updateMarker(); + this.map && this.center && this.map.setCenter(this.center); + this.map && this.zoom && this.map.setZoom(this.zoom); + if ( + nextProps.onMarkerUpdate && + nextProps.markerPosition && + nextProps.markerPosition.lat !== undefined && + nextProps.markerPosition.lng !== undefined + ) + this.map && this.map.panTo(nextProps.markerPosition); + } + if (update && this.maps && this.map && this.map.getCenter() && this.map.getZoom()) { + // update center so that map doesn't move back + this.center = this.map.getCenter().toJSON(); + this.zoom = this.map.getZoom(); + } + if (this.maps && nextProps.kmlUrl) { + this.updateKml(nextProps.kmlUrl); + } + return update; + } + + /** + * Update map elements + * @override + */ + componentDidUpdate() { + if (this.maps && this.map) { + this.updateGeoJson(); + this.updateRect(); + this.updateMarker(); + this.props.kmlUrl && this.updateKml(this.props.kmlUrl); + + // update map center an zoom to they match the previous values + // unless one of the three previous methods changed them + if (this.center) this.map.setCenter(this.center); + if (this.zoom) this.map.setZoom(this.zoom); + } + } + + render() { + return ( +
    + +
    + ); + } +} + +export { MapContainer, MapProps }; diff --git a/src/web/src/app/Components/OperatingClassForm.tsx b/src/web/src/app/Components/OperatingClassForm.tsx new file mode 100644 index 0000000..18aa3a4 --- /dev/null +++ b/src/web/src/app/Components/OperatingClassForm.tsx @@ -0,0 +1,229 @@ +import * as React from 'react'; +// import ReactTooltip from 'react-tooltip'; +import { + GalleryItem, + FormGroup, + InputGroup, + Radio, + TextInput, + InputGroupText, + Select, + SelectOption, + SelectVariant, + CheckboxSelectGroup, + Checkbox, +} from '@patternfly/react-core'; +import { OperatingClass, OperatingClassIncludeType } from '../Lib/RatAfcTypes'; +/** OperatingClassForm.tsx - Form component to display and create the OperatingClass object + * + * mgelman 2022-01-04 + */ + +export interface OperatingClassFormParams { + operatingClass: { + num: number; + include: OperatingClassIncludeType; + channels?: number[]; + }; + onChange: (val: { num: number; include: OperatingClassIncludeType; channels?: number[] }) => void; + allowOnlyOneChannel?: boolean; +} + +export interface OperatingClassFormState { + num?: number; + include?: OperatingClassIncludeType; + channels?: number[]; + allChannels: number[]; + allowOnlyOneChannel: boolean; +} + +export class OperatingClassForm extends React.PureComponent { + constructor(props: OperatingClassFormParams) { + super(props); + this.state = { + allChannels: this.generateChannelIndicesForOperatingClassNum(props.operatingClass.num), + num: this.props.operatingClass.num, + allowOnlyOneChannel: this.props.allowOnlyOneChannel ?? false, + }; + } + + readonly humanNames = { + 131: 'Operating Class 131 (20 MHz)', + 132: 'Operating Class 132 (40 MHz)', + 133: 'Operating Class 133 (80 MHz)', + 134: 'Operating Class 134 (160 MHz)', + 136: 'Operating Class 136 (20 MHz)', + 137: 'Operating Class 137 (320 MHz)', + }; + + private setInclude(n: OperatingClassIncludeType) { + this.props.onChange({ + include: n, + channels: this.props.operatingClass.channels, + num: this.props.operatingClass.num, + }); + } + + private setChannels(n: number[]) { + this.props.onChange({ + include: this.props.operatingClass.include, + channels: n, + num: this.props.operatingClass.num, + }); + } + + private AddIdToString(s: string) { + return s + '_' + this.props.operatingClass.num; + } + + private generateChannelIndicesForOperatingClassNum(oc: number) { + var start = 1; + var interval = 1; + var count = 1; + + switch (oc) { + case 131: + interval = 4; + count = 59; + break; + case 132: + start = 3; + interval = 8; + count = 29; + break; + case 133: + start = 7; + interval = 16; + count = 14; + break; + case 134: + start = 15; + interval = 32; + count = 7; + break; + case 136: + start = 2; + interval = 4; + count = 1; + break; + case 137: + start = 31; + interval = 32; + count = 6; + break; + + default: + return []; + } + + var n = start; + var result = []; + for (let index = 0; index < count; index++) { + result.push(n); + n += interval; + } + + return result; + } + + private onChannelSelected(isChecked, selection) { + var selectedChannel = Number(selection); + + if (this.state.allowOnlyOneChannel) { + this.setChannels([selectedChannel]); + } else { + if (isChecked && !this.props.operatingClass.channels) { + this.setChannels([selectedChannel]); + } else { + var prevSelections = this.props.operatingClass.channels!.slice(); + + if (isChecked) { + if (prevSelections.includes(selectedChannel)) { + return; // already there do nothing + } else { + prevSelections.push(selectedChannel); + this.setChannels(prevSelections.sort((a, b) => a - b)); + } + } else { + //remove if present + if (prevSelections.includes(selectedChannel)) { + this.setChannels(prevSelections.filter((x) => x != selection)); + } + } + } + } + } + + private isChannelSelected(c: number) { + if (this.props.operatingClass.channels) { + return this.props.operatingClass.channels.includes(c); + } else return false; + } + + render() { + return ( + <> + + + {!this.state.allowOnlyOneChannel && ( + + isChecked && this.setInclude(OperatingClassIncludeType.None)} + /> + isChecked && this.setInclude(OperatingClassIncludeType.Some)} + /> + isChecked && this.setInclude(OperatingClassIncludeType.All)} + /> + + )} + + {this.props.operatingClass.include == OperatingClassIncludeType.Some && + this.state.allChannels.map((v, _) => { + return !this.state.allowOnlyOneChannel ? ( + this.onChannelSelected(isChecked, v)} + /> + ) : ( + this.onChannelSelected(isChecked, v)} + /> + ); + })} + + + + + ); + } +} diff --git a/src/web/src/app/Components/SpectrumDisplay.tsx b/src/web/src/app/Components/SpectrumDisplay.tsx new file mode 100644 index 0000000..866ad88 --- /dev/null +++ b/src/web/src/app/Components/SpectrumDisplay.tsx @@ -0,0 +1,481 @@ +import * as React from 'react'; +import { + LineChart, + ScatterChart, + CartesianGrid, + XAxis, + YAxis, + Tooltip, + Scatter, + Line, + Legend, + ResponsiveContainer, + ReferenceArea, + TooltipProps, +} from 'recharts'; +import { Title, CardHeader, CardBody, Card, TextArea } from '@patternfly/react-core'; +import { SpectrumProfile, PAWSResponse } from '../Lib/RatApiTypes'; +import { AvailableSpectrumInquiryResponse, AvailableSpectrumInfo, AvailableChannelInfo } from '@app/Lib/RatAfcTypes'; + +import ReactDOMServer from 'react-dom/server'; + +/** + * SpectrumDisplay.tsx: display PAWS response spectrums on graphs + * author: Sam Smucny + */ + +/** + * Enumeration of possible colors + */ +const colors = ['black', 'blue', 'orange', 'green', 'red', 'purple', 'gold']; + +/*** + * Things to know to draw the Spectrum line chart correctly + * If changes are made here they need to be made in ChannelDisplay.tsx as well + */ +const channelDescription = [ + { + channelWidth: 20, + startFrequency: 5945, + OperatingClass: 131, + OCDisplayname: '131', + channels: Array(59) + .fill(0) + .map((_, i) => ({ cfi: Number(1 + 4 * i) })), + }, + { + channelWidth: 40, + startFrequency: 5945, + OperatingClass: 132, + OCDisplayname: '132', + channels: Array(29) + .fill(0) + .map((_, i) => ({ cfi: Number(3 + 8 * i) })), + }, + { + channelWidth: 80, + startFrequency: 5945, + OperatingClass: 133, + OCDisplayname: '133', + channels: Array(14) + .fill(0) + .map((_, i) => ({ cfi: Number(7 + 16 * i) })), + }, + { + channelWidth: 160, + startFrequency: 5945, + OperatingClass: 134, + OCDisplayname: '134', + channels: Array(7) + .fill(0) + .map((_, i) => ({ cfi: Number(15 + 32 * i) })), + }, + { + channelWidth: 20, + startFrequency: 5925, + OperatingClass: 136, + OCDisplayname: '136', + channels: Array(1) + .fill(0) + .map((_, i) => ({ cfi: Number(2 + 4 * i) })), + }, + { + channelWidth: 320, + startFrequency: 5945, + OperatingClass: 137, + OCDisplayname: '137-1', + channels: Array(3) + .fill(0) + .map((_, i) => ({ cfi: Number(31 + 64 * i) })), + }, + { + channelWidth: 320, + startFrequency: 6105, + OperatingClass: 137, + OCDisplayname: '137-2', + channels: Array(3) + .fill(0) + .map((_, i) => ({ cfi: Number(63 + 64 * i) })), + }, +]; + +/** + * Add null points between disjoint spectral envelopes to give displayed line discontinuities + * also convert Hz to MHz. + * @param spectra Spectrum profile according to PAWS + * @returns adjusted spectrum values that properly display on graph + */ +const insertNullPoints = (spectra: SpectrumProfile[][]): { hz: number; dbm?: number | null }[] => + spectra + .reduce((prev, curr) => prev.concat([{ hz: (prev[prev.length - 1].hz + curr[0].hz) / 2, dbm: -Infinity }], curr)) + .map((point) => ({ hz: point.hz / 1e6, dbm: point.dbm === -Infinity ? undefined : point.dbm })); + +/** + * Displays spectrum from a PAWS response on a graph to show spectral envelope + * @param spectrum `PAWSResponse` object to graph + */ +export const SpectrumDisplayPAWS: React.FunctionComponent<{ spectrum?: PAWSResponse; greyOutUnii: boolean }> = ( + props, +) => + !props.spectrum || props.spectrum.spectrumSpecs.length === 0 ? ( + + There is no data to display. Try sending a request above. + + ) : ( + <> + {props.spectrum.spectrumSpecs.map((spec) => + spec.spectrumSchedules.map((schedule, sIndex) => ( + + + + {spec.rulesetInfo.rulesetId + + ': ' + + schedule.eventTime.startTime + + ' to ' + + schedule.eventTime.stopTime} + + + + + + + 5925 + i * 80) + .concat([7125])} + /> + + + {schedule.spectra.map((spectrum, i) => ( + + ))} + + {props.greyOutUnii && } + {props.greyOutUnii && } + + + + + )), + )} + + ); + +/** + * Converts AP-AFC spectrum info into a format that is able to be displayed on the plot + * @param sections spectrum infos in AP-AFC format + */ +const makeSteps = (sections: AvailableSpectrumInfo[]): { hz: number; dbm?: number }[] => { + let prev = sections[0]; + let points: { hz: number; dbm?: number }[] = [ + { hz: prev.frequencyRange.lowFrequency, dbm: prev.maxPsd }, + { hz: prev.frequencyRange.highFrequency, dbm: prev.maxPsd }, + ]; + for (let i = 1; i < sections.length; i++) { + let curr = sections[i]; + + if (curr.frequencyRange.lowFrequency !== prev.frequencyRange.highFrequency) { + points.push({ hz: (curr.frequencyRange.lowFrequency + prev.frequencyRange.highFrequency) / 2, dbm: undefined }); + } + points.push( + { hz: curr.frequencyRange.lowFrequency, dbm: curr.maxPsd }, + { hz: curr.frequencyRange.highFrequency, dbm: curr.maxPsd }, + ); + + prev = curr; + } + return points; +}; + +/** + * Displays spectrum from a AP-AFC request + * @param spectrum `AvailableSpectrumInquiryResponse` object to graph + */ +export const SpectrumDisplayAFC: React.FunctionComponent<{ spectrum?: AvailableSpectrumInquiryResponse }> = (props) => + !props.spectrum || !props.spectrum.availableFrequencyInfo || props.spectrum.availableFrequencyInfo.length === 0 ? ( + + There is no spectrum data to display. + + ) : ( + <> + + + + {'Request ' + + props.spectrum?.requestId + + ': expires at ' + + (props.spectrum?.availabilityExpireTime || 'No expiration')} + + + + + + + 5925 + i * 80) + .concat([7125])} + /> + + + + + + + + + + ); + +/** + * Converts the channel data to frequencies + * @param spectra Channel info + * @returns Spectrum profile for plotting + */ +const convertOpClassData = (spectra: AvailableChannelInfo): { hz: number; dbm: number | undefined; cfi: number }[] => { + var desc = channelDescription.find((x) => x.OperatingClass == spectra.globalOperatingClass); + if (!!desc) { + return desc.channels + .map((e, i) => { + let cfiIndex = spectra.channelCfi.indexOf(e.cfi); + return [ + { + hz: desc!.startFrequency + desc!.channelWidth * i, + dbm: cfiIndex >= 0 ? spectra.maxEirp[cfiIndex] : undefined, + cfi: e.cfi, + }, + { + hz: desc!.startFrequency + desc!.channelWidth * i + desc!.channelWidth, + dbm: cfiIndex >= 0 ? spectra.maxEirp[cfiIndex] : undefined, + cfi: e.cfi, + }, + ]; + }) + .reduce((r, x) => r.concat(x)); + } + return []; +}; + +// converts the data for 137 depending on which line it gets drawn on +const convert137OpClassData = ( + spectra: AvailableChannelInfo, + channelIndex: number, +): { hz: number; dbm: number | undefined; cfi: number }[] => { + var desc = channelDescription.find( + (x) => x.OperatingClass == spectra.globalOperatingClass && x.OCDisplayname.endsWith(String(channelIndex)), + ); + if (!!desc) { + return desc.channels + .map((e, i) => { + let cfiIndex = spectra.channelCfi.indexOf(e.cfi); + return [ + { + hz: desc!.startFrequency + desc!.channelWidth * i, + dbm: cfiIndex >= 0 ? spectra.maxEirp[cfiIndex] : undefined, + cfi: e.cfi, + }, + { + hz: desc!.startFrequency + desc!.channelWidth * i + desc!.channelWidth, + dbm: cfiIndex >= 0 ? spectra.maxEirp[cfiIndex] : undefined, + cfi: e.cfi, + }, + ]; + }) + .reduce((r, x) => r.concat(x)); + } + return []; +}; + +// Op Class 137 is two lines in the graph, but sent as a single set of channel info. This breaks it up +// into the two lines +const split137IntoSubchannels = (spectra: AvailableChannelInfo) => { + var chan1 = channelDescription.find((x) => x.OCDisplayname == '137-1')?.channels.map((x) => x.cfi); + + var spectra1: AvailableChannelInfo = { globalOperatingClass: 137, channelCfi: [], maxEirp: [] }; + var spectra2: AvailableChannelInfo = { globalOperatingClass: 137, channelCfi: [], maxEirp: [] }; + + for (let index = 0; index < spectra.channelCfi.length; index++) { + if (chan1?.includes(spectra.channelCfi[index])) { + spectra1.channelCfi.push(spectra.channelCfi[index]); + spectra1.maxEirp.push(spectra.maxEirp[index]); + } else { + spectra2.channelCfi.push(spectra.channelCfi[index]); + spectra2.maxEirp.push(spectra.maxEirp[index]); + } + } + return [spectra1, spectra2]; +}; + +// Current operating class for custom tooltip +var tooltipLabel: string = ''; + +/** + * Custom tooltip to display Operating class, channel Cfi, frequency and EIRP + */ +const CustomTooltip = (pr: TooltipProps) => { + if (pr.active && pr.payload && pr.payload.length) { + var res = ( + <> + {' '} +
    + {pr.payload.map((v) => { + return ( +

    + OC {tooltipLabel} Ch {v.payload.cfi} {v.name}:{v.value} +

    + ); + })} +
    + + ); + return res; + } + + return null; +}; + +const generateScatterPlot = (data: AvailableChannelInfo[]) => { + return data + .map((spectrum, i) => { + if (spectrum.globalOperatingClass == 137) { + return split137IntoSubchannels(spectrum).map((s137, j) => { + return ( + (tooltipLabel = String(spectrum.globalOperatingClass) + '-' + (j + 1))} + /> + ); + }); + } else + return ( + (tooltipLabel = String(spectrum.globalOperatingClass))} + /> + ); + }) + .flat(); +}; + +/** + * Displays the channel information from a virtual AP request as a line chart + * @param props + * @returns + */ +export const SpectrumDisplayLineAFC: React.FunctionComponent<{ spectrum?: AvailableSpectrumInquiryResponse }> = ( + props, +) => + !props.spectrum || !props.spectrum.availableChannelInfo || props.spectrum.availableChannelInfo.length === 0 ? ( + + There is no spectrum data to display. + + ) : ( + + + + {'Request ' + + props.spectrum.requestId + + ': expires at ' + + (props.spectrum.availabilityExpireTime || 'No expiration')} + + + + + + + 5925 + i * 80) + .concat([7125])} + /> + + } active={true} /> + {generateScatterPlot(props.spectrum.availableChannelInfo)} + + + + {/* */} + + + ); diff --git a/src/web/src/app/Components/Timer.tsx b/src/web/src/app/Components/Timer.tsx new file mode 100644 index 0000000..a310b5d --- /dev/null +++ b/src/web/src/app/Components/Timer.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { logger } from '../Lib/Logger'; + +/** + * Timer.tsx: simple timer element that counts the seconds since being rendered and stops when unmounted + * author: Sam Smucny + */ + +/** + * Timer component that counts how many seconds it has been alive. + */ +export class Timer extends React.Component<{}, { count: number; timer: any }> { + constructor(props: Readonly<{}>) { + super(props); + this.state = { + count: 0, + timer: setInterval(() => this.setState((st, pr) => ({ count: st.count + 1 })), 1000), + }; + } + + componentWillUnmount() { + logger.info('Timer stopped at ' + this.state.count + ' seconds. Timer: ', this.state.timer); + clearInterval(this.state.timer); + } + + render = () =>
    Seconds elapsed: {this.state.count}
    ; +} diff --git a/src/web/src/app/Convert/Convert.tsx b/src/web/src/app/Convert/Convert.tsx new file mode 100644 index 0000000..5fb4109 --- /dev/null +++ b/src/web/src/app/Convert/Convert.tsx @@ -0,0 +1,147 @@ +import * as React from 'react'; +import { + PageSection, + Title, + Card, + CardHeader, + CardBody, + FormGroup, + FormSelect, + FormSelectOption, + Button, + Alert, + AlertActionCloseButton, +} from '@patternfly/react-core'; +import { RatResponse } from '../Lib/RatApiTypes'; +import { logger } from '../Lib/Logger'; +import { ulsFileConvert } from '../Lib/RatApi'; +import { Timer } from '../Components/Timer'; + +/** + * Convert.tsx: Page where use can convert files on server + * author: Sam Smucny + */ + +/** + * Interface definition of `Convert` state + */ +interface ConvertState { + ulsSelect?: string; + ulsFilesCsv: string[]; + ulsMessageType?: 'info' | 'warning' | 'danger' | 'success'; + ulsMessage?: string; +} + +/** + * Page level component that allows a user to convert file formats + */ +class Convert extends React.Component< + { + /** + * List of csv files that can be converted + */ + ulsFilesCsv: RatResponse; + }, + ConvertState +> { + constructor(props: Readonly<{ ulsFilesCsv: RatResponse }>) { + super(props); + + if (props.ulsFilesCsv.kind === 'Error') { + throw new Error('Uls files could not be retreived'); + } + + this.state = { + ulsFilesCsv: props.ulsFilesCsv.result, + }; + } + + private updateUls(s: string | undefined) { + if (s) { + this.setState({ ulsSelect: s }); + } + } + + private convertUls() { + if (this.state.ulsSelect) { + logger.info('converting uls file', this.state.ulsSelect); + this.setState({ ulsMessageType: 'info', ulsMessage: 'processing...' }); + ulsFileConvert(this.state.ulsSelect).then((res) => { + if (res.kind === 'Success') { + if (res.result.invalidRows > 0) { + this.setState({ + ulsMessageType: 'warning', + ulsMessage: + String(res.result.invalidRows) + ' invalid rows in processing:\n\n' + res.result.errors.join('\n'), + }); + } else { + this.setState({ ulsMessage: 'Conversion successful', ulsMessageType: 'success' }); + } + } else { + this.setState({ ulsMessage: res.description, ulsMessageType: 'danger' }); + } + }); + } else { + this.setState({ ulsMessage: 'Select a file', ulsMessageType: 'danger' }); + } + } + + render() { + return ( + + File Conversion + + ULS Conversion + + + this.updateUls(s)} + id="horizontal-form-uls-db" + name="horizontal-form-uls-db" + style={{ textAlign: 'right' }} + > + + {this.state.ulsFilesCsv.map((option: string) => ( + + ))} + + +
    + <> + {this.state.ulsMessageType && ( + this.setState({ ulsMessageType: undefined })} /> + ) + } + > + {this.state.ulsMessage} + {this.state.ulsMessageType === 'info' && } + + )} + +
    + +
    +
    +
    + ); + } +} + +export { Convert }; diff --git a/src/web/src/app/Dashboard/Dashboard.tsx b/src/web/src/app/Dashboard/Dashboard.tsx new file mode 100644 index 0000000..a71d4a8 --- /dev/null +++ b/src/web/src/app/Dashboard/Dashboard.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { PageSection, Title, Card, CardBody } from '@patternfly/react-core'; +import { guiConfig } from '../Lib/RatApi'; + +/** + * Dashboard.tsx: application splash page. Currently empty + * author: Sam Smucny + */ + +/** + * Page level component for dashboard. Nothing interesting now. + */ +const Dashboard: React.FunctionComponent = () => { + return ( + + {guiConfig.app_name} + + Navigate between pages using the sidebar menu. + + + ); +}; + +export { Dashboard }; diff --git a/src/web/src/app/DeniedRules/DRList.tsx b/src/web/src/app/DeniedRules/DRList.tsx new file mode 100644 index 0000000..a06226f --- /dev/null +++ b/src/web/src/app/DeniedRules/DRList.tsx @@ -0,0 +1,456 @@ +import * as React from 'react'; +import { DeniedRegion, error, success, RatResponse } from '../Lib/RatApiTypes'; +import { + getDeniedRegions, + getDeniedRegionsCsvFile, + updateDeniedRegions, + putAccessPointDenyList, + getAccessPointsDeny, + addAccessPointDeny, +} from '../Lib/Admin'; +import { + Card, + CardHead, + CardHeader, + CardBody, + PageSection, + InputGroup, + Select, + SelectOption, + FormSelect, + FormSelectOption, + Button, + Modal, + Alert, + AlertActionCloseButton, + GalleryItem, + FormGroup, +} from '@patternfly/react-core'; +import { NewDR } from './NewDR'; +import { DRTable } from './DRTable'; +import { NewAPDeny } from './NewAPDeny'; +import { logger } from '../Lib/Logger'; +import { UserContext, UserState, isLoggedIn, hasRole } from '../Lib/User'; +import { mapRegionCodeToName } from '../Lib/Utils'; +import { exportCache, getRulesetIds } from '../Lib/RatApi'; + +/** + * DRList.tsx: List of denied rules by regionStr and by AP cert id and/or serial number + * to add/remove + * author: mgelman@rkf-eng.com + */ + +/** + * Interface definition of `DRList` properties + */ +export interface DRListProps { + regions: string[]; + userId: number; + org: string; +} + +interface DRListState { + deniedRegions: DeniedRegion[]; + regionStr: string; + allRegions: string[]; + deniedRegionsNeedSaving: boolean; + showingSaveWarning: boolean; + newRegionString?: string; + messageSuccess?: string; + messageError?: string; + isEditorOpen: boolean; + rulesetIds: []; + apMessageSuccess?: string; + apMessageError?: string; + drForEditing?: DeniedRegion; +} + +/** + * Page level component to list a user's registerd access points. Users use these + * credentials to utilize the PAWS interface. + */ +export class DRList extends React.Component { + constructor(props: DRListProps) { + super(props); + + this.state = { + deniedRegions: [], + regionStr: 'US', + deniedRegionsNeedSaving: false, + showingSaveWarning: false, + allRegions: this.props.regions, + isEditorOpen: false, + rulesetIds: [], + drForEditing: undefined, + }; + this.loadCurrentDeniedRegions(); + + getRulesetIds().then((res) => { + if (res.kind === 'Success') { + this.setState({ rulesetIds: res.result }); + } else { + alert(res.description); + } + }); + } + + loadCurrentDeniedRegions = () => { + getDeniedRegions(this.state.regionStr).then((res) => { + if (res.kind === 'Success') { + this.setState({ deniedRegions: res.result, deniedRegionsNeedSaving: false }); + } + }); + }; + + onAdd = (dr: DeniedRegion) => { + this.setState({ + deniedRegions: this.state.deniedRegions.concat(dr), + deniedRegionsNeedSaving: true, + isEditorOpen: false, + }); + }; + + onOpenEdit = (id: string) => { + let drToEdit = this.state.deniedRegions.find( + (x) => x.regionStr == this.state.regionStr && x.name + '===' + x.zoneType == id, + ); + this.setState({ drForEditing: drToEdit, isEditorOpen: true }); + }; + + onCloseEdit = (dr: DeniedRegion, prevName: string, prevZoneType: string) => { + if (!!dr) { + let oldId = prevName + '===' + prevZoneType; + let drToEdit = this.state.deniedRegions.findIndex( + (x) => x.regionStr == this.state.regionStr && x.name + '===' + x.zoneType == oldId, + ); + if (drToEdit >= 0) { + //Need to copy the array so that the updated state will trigger + let newDrs = Array.from(this.state.deniedRegions); + newDrs[drToEdit] = dr; + this.setState({ + deniedRegions: newDrs, + deniedRegionsNeedSaving: true, + isEditorOpen: false, + drForEditing: undefined, + }); + } + } else { + this.setState({ isEditorOpen: false, drForEditing: undefined }); + } + }; + + deleteDR = (id: string) => { + this.setState({ + deniedRegions: this.state.deniedRegions.filter( + (x) => !(x.regionStr == this.state.regionStr && x.name + '===' + x.zoneType == id), + ), + deniedRegionsNeedSaving: true, + }); + }; + + setUlsRegion = (newRegion: string): void => { + if (newRegion != this.state.regionStr) { + if (this.state.deniedRegionsNeedSaving) { + this.setState({ showingSaveWarning: true, newRegionString: newRegion }); + } else { + this.setState({ regionStr: newRegion, deniedRegionsNeedSaving: false }, () => { + this.loadCurrentDeniedRegions(); + }); + } + } + }; + + closeSaveWarning = () => { + this.setState({ showingSaveWarning: false }); + }; + + forceChangeRegion = () => { + this.setState( + { + showingSaveWarning: false, + regionStr: this.state.newRegionString!, + newRegionString: undefined, + deniedRegionsNeedSaving: false, + }, + () => { + this.loadCurrentDeniedRegions(); + }, + ); + }; + + putDeniedRegions = () => { + updateDeniedRegions( + this.state.deniedRegions.filter((x) => x.regionStr == this.state.regionStr), + this.state.regionStr, + ).then((res) => { + if (res.kind === 'Success') { + this.setState({ messageSuccess: 'Changes saved', deniedRegionsNeedSaving: false }); + } else { + logger.error('Could not save denied regions', 'error code: ', res.errorCode, 'description: ', res.description); + this.setState({ messageError: 'Error saving ' + res.description }); + } + }); + }; + + downloadBlob = (b: Blob, filename: string) => { + const element = document.createElement('a'); + element.href = URL.createObjectURL(b); + element.download = filename; + document.body.appendChild(element); // Required for this to work in FireFox + element.click(); + }; + + downloadCSVFile = () => { + getDeniedRegionsCsvFile(this.state.regionStr).then((res) => { + if (res.kind === 'Success') { + let file = new Blob([res.result], { + type: 'text/csv', + }); + this.downloadBlob(file, this.state.regionStr + '_denied_regions.csv'); + } + }); + }; + + closeEditor = () => { + this.setState({ isEditorOpen: false, drForEditing: undefined }); + }; + + openEditor = () => { + this.setState({ isEditorOpen: true }); + }; + + importList(ev) { + // @ts-ignore + const file = ev.target.files[0]; + const reader = new FileReader(); + try { + reader.onload = async () => { + try { + const value: AccessPointListModel = { accessPoints: reader.result as string }; + const putResp = await putAccessPointDenyList(value, this.props.userId); + if (putResp.kind === 'Error') { + this.setState({ apMessageError: putResp.description, apMessageSuccess: undefined }); + return; + } else { + this.setState({ apMessageError: undefined, apMessageSuccess: 'Import successful!' }); + } + } catch (e) { + this.setState({ apMessageError: 'Unable to read file', apMessageSuccess: undefined }); + } + }; + + reader.readAsText(file); + ev.target.value = ''; + } catch (e) { + logger.error('Failed read file ', e); + this.setState({ apMessageError: 'Failed to read read file', apMessageSuccess: undefined }); + } + } + + async onAddAPDeny(ap: AccessPointModel) { + const res = await addAccessPointDeny(ap, this.props.userId); + if (res.kind === 'Success') { + return success('Added'); + } else { + return res; + } + } + + async exportList() { + const res = await getAccessPointsDeny(this.props.userId); + if (res.kind == 'Success') { + let b = new Blob([res.result], { + type: 'text/csv', + }); + this.downloadBlob(b, 'denied_aps.json'); + } + } + + render() { + return ( + + + + Denied Region + + + this.closeSaveWarning()} + actions={[ + , + , + ]} + > + You have unsaved changes to the denied regions for the current country. Proceed and lose changes? + + {this.state.messageError !== undefined && ( + this.setState({ messageError: undefined })} />} + > + {this.state.messageError} + + )} + + {this.state.messageSuccess !== undefined && ( + this.setState({ messageSuccess: undefined })} />} + > + {this.state.messageSuccess} + + )} + this.closeEditor()} + actions={[ + , + ]} + > + this.onAdd(dr)} + onCloseEdit={(dr, prevName, prevZone) => this.onCloseEdit(dr, prevName, prevZone)} + currentRegionStr={this.state.regionStr} + drToEdit={this.state.drForEditing} + /> + + + + + this.setUlsRegion(x)} + id="horizontal-form-uls-region" + name="horizontal-form-uls-region" + isValid={!!this.state.regionStr} + style={{ textAlign: 'right' }} + > + + {this.state.allRegions.map((option: string) => ( + + ))} + + + +
    + this.deleteDR(id)} + currentRegionStr={this.state.regionStr} + onOpenEdit={(id: string) => this.onOpenEdit(id)} + /> +
    + {hasRole('Super') && ( + + )} +
    + {hasRole('Super') && ( + + )} +
    + {hasRole('Super') && } +
    +
    + + + + Denied Access Points + + + {hasRole('Admin') && ( + this.onAddAPDeny(ap)} + rulesetIds={this.state.rulesetIds} + org={this.props.org} + /> + )} + +
    + {this.state.apMessageError !== undefined && ( + this.setState({ apMessageError: undefined })} />} + > + {this.state.apMessageError} + + )} + + {this.state.apMessageSuccess !== undefined && ( + this.setState({ apMessageSuccess: undefined })} />} + > + {this.state.apMessageSuccess} + + )} + + + this.importList(ev)} + /> + + +
    + {hasRole('Admin') && } +
    +
    +
    +
    + ); + } +} +/** + * wrapper for ap list when it is not embedded in another page + */ +export class DRListPage extends React.Component<{ regions: RatResponse }, { regions: string[] }> { + constructor(props) { + super(props); + + this.state = { regions: [] }; + if (props.regions.kind === 'Success') { + Object.assign(this.state, { regions: props.regions.result }); + } else { + logger.error( + 'Could not load regions', + 'error code: ', + props.regions.errorCode, + 'description: ', + props.regions.description, + ); + Object.assign(this.state, { regions: [] }); + } + } + + render() { + return ( + + + {(u: UserState) => + u.data.loggedIn && + } + + + ); + } +} diff --git a/src/web/src/app/DeniedRules/DRTable.tsx b/src/web/src/app/DeniedRules/DRTable.tsx new file mode 100644 index 0000000..4a8394e --- /dev/null +++ b/src/web/src/app/DeniedRules/DRTable.tsx @@ -0,0 +1,99 @@ +import * as React from 'react'; +import { headerCol, Table, TableVariant, TableHeader, TableBody } from '@patternfly/react-table'; +import { + AccessPointModel, + DeniedRegion, + ExclusionCircle, + ExclusionHorizon, + ExclusionRect, + ExclusionTwoRect, + UserModel, +} from '../Lib/RatApiTypes'; + +/** + * DRTable.tsx: Table that displays access points. Shows org column if admin specifies filterId is 0 + * author: Sam Smucny + */ + +/** + * Interface definition of `DRTable` properties + */ +interface DRTableProps { + deniedRegions: DeniedRegion[]; + currentRegionStr: string; + onDelete: (id: string) => void; + onOpenEdit: (id: string) => void; +} + +/** + * Table component to display access points. + */ +export class DRTable extends React.Component { + private columns = ['Location', 'Start Freq (MHz)', 'Stop Freq (MHz)', 'Exclusion Zone', 'Coordinates']; + + constructor(props: DRTableProps) { + super(props); + this.state = { + rows: [], + }; + } + + private toRow = (dr: DeniedRegion) => ({ + id: dr.name + '===' + dr.zoneType, + cells: [dr.name, dr.startFreq, dr.endFreq, dr.zoneType, this.zoneToText(dr)], + }); + + private zoneToText(dr: DeniedRegion) { + switch (dr.zoneType) { + case 'Circle': + let c = dr.exclusionZone as ExclusionCircle; + return `Center: (${c.latitude}, ${c.longitude}) Rad: ${c.radiusKm} km`; + case 'One Rectangle': + let o = dr.exclusionZone as ExclusionRect; + return `(${o.topLat}, ${o.leftLong}), (${o.bottomLat}, ${o.rightLong}) `; + case 'Two Rectangles': + let t = dr.exclusionZone as ExclusionTwoRect; + return ( + `Rectangle 1: (${t.rectangleOne.topLat}, ${t.rectangleOne.leftLong}), (${t.rectangleOne.bottomLat}, ${t.rectangleOne.rightLong}) ` + + `Rectangle 2:(${t.rectangleTwo.topLat}, ${t.rectangleTwo.leftLong}), (${t.rectangleTwo.bottomLat}, ${t.rectangleTwo.rightLong})` + ); + case 'Horizon Distance': + let h = dr.exclusionZone as ExclusionHorizon; + return `Center: (${h.latitude}, ${h.longitude}) Height AGL: ${h.aglHeightM} m`; + default: + return ''; + } + } + + actionResolver(data: any, extraData: any) { + return [ + { + title: 'Edit', + onClick: (event: any, rowId: number, rowData: any, extra: any) => this.props.onOpenEdit(rowData.id), + }, + { + title: 'Remove', + onClick: (event: any, rowId: number, rowData: any, extra: any) => this.props.onDelete(rowData.id), + }, + ]; + } + + render() { + return ( + x.regionStr == this.props.currentRegionStr).map(this.toRow) + : [] + } + variant={TableVariant.compact} + actionResolver={(a, b) => this.actionResolver(a, b)} + > + + +
    + ); + } +} diff --git a/src/web/src/app/DeniedRules/NewAPDeny.tsx b/src/web/src/app/DeniedRules/NewAPDeny.tsx new file mode 100644 index 0000000..ca5ec22 --- /dev/null +++ b/src/web/src/app/DeniedRules/NewAPDeny.tsx @@ -0,0 +1,190 @@ +import * as React from 'react'; +import { AccessPointModel, RatResponse } from '../Lib/RatApiTypes'; +import { + Gallery, + GalleryItem, + FormGroup, + TextInput, + Button, + Alert, + AlertActionCloseButton, + FormSelect, + FormSelectOption, + InputGroup, +} from '@patternfly/react-core'; +import { PlusCircleIcon } from '@patternfly/react-icons'; +import { UserModel } from '../Lib/RatApiTypes'; +import { hasRole } from '../Lib/User'; + +/** + * NewAPDeny.tsx: Form for creating a new access point + * author: Sam Smucny + */ + +/** + * Interface definition of `NewAPDeny` properties + */ +interface NewAPDenyProps { + org: string; + rulesetIds: string[]; + onAdd: (ap: AccessPointModel) => Promise>; +} + +interface NewAPDenyState { + serialNumber?: string; + certificationId?: string; + messageType?: 'danger' | 'success'; + messageValue: string; + org: string; + rulesetId: string; +} + +/** + * Component with form for user to register a new access point. + * + */ +export class NewAPDeny extends React.Component { + constructor(props: NewAPDenyProps) { + super(props); + this.state = { + messageValue: '', + serialNumber: '', + certificationId: '', + org: this.props.org, + rulesetId: '', + }; + } + + private setRulesetId = (n: string) => this.setState({ rulesetId: n }); + + private submit() { + if (!this.state.serialNumber) { + this.setState({ messageType: 'danger', messageValue: 'Enter * for serial number wildcard' }); + return; + } + + if (!this.state.org) { + this.setState({ messageType: 'danger', messageValue: 'Org must be specified' }); + return; + } + + if (!this.state.certificationId) { + this.setState({ messageType: 'danger', messageValue: 'certification id must be specified' }); + return; + } + + if (!this.state.rulesetId) { + this.setState({ messageType: 'danger', messageValue: 'rulesetId must be specified' }); + return; + } + + this.props + .onAdd({ + id: 0, + serialNumber: this.state.serialNumber, + certificationId: this.state.certificationId, + org: this.state.org, + rulesetId: this.state.rulesetId, + }) + .then((res) => { + if (res.kind === 'Success') { + this.setState((s: NewAPDenyState) => ({ + messageType: 'success', + messageValue: 'Added Deny AP serial ' + s.serialNumber + ' Cert ID ' + s.certificationId, + })); + } else { + this.setState({ messageType: 'danger', messageValue: res.description }); + } + }); + } + + private hideAlert = () => this.setState({ messageType: undefined }); + + render() { + const serialNumberChange = (s?: string) => this.setState({ serialNumber: s }); + const certificationIdChange = (s?: string) => this.setState({ certificationId: s }); + const orgChange = (s?: string) => this.setState({ org: s }); + + return ( + <> + {this.state.messageType && ( + } + /> + )} +
    + + + + this.setRulesetId(x)} + id="horizontal-form-ruleset" + name="horizontal-form-ruleset" + isValid={!!this.state.rulesetId} + style={{ textAlign: 'right' }} + > + + {this.props.rulesetIds.map((option: string) => ( + + ))} + + + + + + + + + + +
    + +
    +
    +
    + + {hasRole('Super') && ( + + + + + + )} + + + + +
    + + ); + } +} diff --git a/src/web/src/app/DeniedRules/NewDR.tsx b/src/web/src/app/DeniedRules/NewDR.tsx new file mode 100644 index 0000000..27fc3c5 --- /dev/null +++ b/src/web/src/app/DeniedRules/NewDR.tsx @@ -0,0 +1,648 @@ +import * as React from 'react'; +import { + AccessPointModel, + DeniedRegion, + ExclusionCircle, + ExclusionHorizon, + ExclusionRect, + ExclusionTwoRect, + RatResponse, +} from '../Lib/RatApiTypes'; +import { + Gallery, + GalleryItem, + FormGroup, + TextInput, + Button, + Alert, + AlertActionCloseButton, + FormSelect, + FormSelectOption, + InputGroup, +} from '@patternfly/react-core'; +import { PlusCircleIcon } from '@patternfly/react-icons'; +import { UserModel } from '../Lib/RatApiTypes'; +import { hasRole } from '../Lib/User'; +import { BlankDeniedRegion } from '../Lib/Admin'; +import { number } from 'prop-types'; + +/** + * NewDR.tsx: Form for creating a new access point + * author: Sam Smucny + */ + +const zoneTypes = ['Circle', 'One Rectangle', 'Two Rectangles', 'Horizon Distance']; + +/** + * Interface definition of `NewDR` properties + */ +interface NewDRProps { + currentRegionStr: string; + drToEdit: DeniedRegion | undefined; + onAdd: (dr: DeniedRegion) => void; + onCloseEdit: (dr: DeniedRegion, prevName: string, prevZoneType: string) => void; +} + +interface NewDRState { + isEdit: boolean; + prevName: string; + prevZoneType: string; + needsSave: boolean; + + regionStr?: string; + name?: string; + startFreq?: number; + endFreq?: number; + zoneType?: 'Circle' | 'One Rectangle' | 'Two Rectangles' | 'Horizon Distance'; + circleLat?: number; + circleLong?: number; + radiusHeight?: number; + rect1topLat?: number; + rect1leftLong?: number; + rect1bottomLat?: number; + rect1rightLong?: number; + rect2topLat?: number; + rect2leftLong?: number; + rect2bottomLat?: number; + rect2rightLong?: number; +} + +/** + * Component with form for user to register a new access point. + * + */ +export class NewDR extends React.Component { + constructor(props: NewDRProps) { + super(props); + if (props.drToEdit !== undefined) { + let preState: NewDRState = { + isEdit: true, + prevName: props.drToEdit.name, + prevZoneType: props.drToEdit.zoneType, + needsSave: false, + regionStr: props.currentRegionStr, + name: props.drToEdit.name, + startFreq: props.drToEdit.startFreq, + endFreq: props.drToEdit.endFreq, + zoneType: props.drToEdit.zoneType, + }; + switch (props.drToEdit.zoneType) { + case 'Circle': + let circ = props.drToEdit.exclusionZone as ExclusionCircle; + preState.circleLat = circ.latitude; + preState.circleLong = circ.longitude; + preState.radiusHeight = circ.radiusKm; + break; + case 'One Rectangle': + let rect = props.drToEdit.exclusionZone as ExclusionRect; + preState.rect1topLat = rect.topLat; + preState.rect1leftLong = rect.leftLong; + preState.rect1bottomLat = rect.bottomLat; + preState.rect1rightLong = rect.rightLong; + break; + case 'Two Rectangles': + let rect2 = props.drToEdit.exclusionZone as ExclusionTwoRect; + preState.rect1topLat = rect2.rectangleOne.topLat; + preState.rect1leftLong = rect2.rectangleOne.leftLong; + preState.rect1bottomLat = rect2.rectangleOne.bottomLat; + preState.rect1rightLong = rect2.rectangleOne.rightLong; + preState.rect2topLat = rect2.rectangleTwo.topLat; + preState.rect2leftLong = rect2.rectangleTwo.leftLong; + preState.rect2bottomLat = rect2.rectangleTwo.bottomLat; + preState.rect2rightLong = rect2.rectangleTwo.rightLong; + break; + case 'Horizon Distance': + let horz = props.drToEdit.exclusionZone as ExclusionHorizon; + preState.circleLat = horz.latitude; + preState.circleLong = horz.longitude; + preState.radiusHeight = horz.aglHeightM; + break; + default: + break; + } + this.state = preState; + } else { + this.state = { + isEdit: false, + regionStr: props.currentRegionStr, + prevName: 'Placeholder', + prevZoneType: 'Circle', + needsSave: false, + name: undefined, + startFreq: undefined, + endFreq: undefined, + zoneType: 'Circle', + circleLat: undefined, + circleLong: undefined, + radiusHeight: undefined, + rect1topLat: undefined, + rect1leftLong: undefined, + rect1bottomLat: undefined, + rect1rightLong: undefined, + rect2topLat: undefined, + rect2leftLong: undefined, + rect2bottomLat: undefined, + rect2rightLong: undefined, + }; + } + } + + private stateToDeniedRegion(): DeniedRegion { + let ez; + switch (this.state.zoneType) { + case 'Circle': + let rc: ExclusionCircle = { + latitude: this.state.circleLat!, + longitude: this.state.circleLong!, + radiusKm: this.state.radiusHeight!, + }; + ez = rc; + break; + case 'One Rectangle': + let er: ExclusionRect = { + topLat: this.state.rect1topLat!, + leftLong: this.state.rect1leftLong!, + bottomLat: this.state.rect1bottomLat!, + rightLong: this.state.rect1rightLong!, + }; + ez = er; + break; + case 'Two Rectangles': + let er2: ExclusionTwoRect = { + rectangleOne: { + topLat: this.state.rect1topLat!, + leftLong: this.state.rect1leftLong!, + bottomLat: this.state.rect1bottomLat!, + rightLong: this.state.rect1rightLong!, + }, + rectangleTwo: { + topLat: this.state.rect2topLat!, + leftLong: this.state.rect2leftLong!, + bottomLat: this.state.rect2bottomLat!, + rightLong: this.state.rect2rightLong!, + }, + }; + ez = er2; + break; + case 'Horizon Distance': + let eh: ExclusionHorizon = { + latitude: this.state.circleLat!, + longitude: this.state.circleLong!, + aglHeightM: this.state.radiusHeight!, + }; + ez = eh; + break; + default: + break; + } + + let dr: DeniedRegion = { + regionStr: this.state.regionStr!, + name: this.state.name!, + endFreq: this.state.endFreq!, + startFreq: this.state.startFreq!, + zoneType: this.state.zoneType!, + exclusionZone: ez, + }; + + return dr; + } + + private submit() { + let newDr = this.stateToDeniedRegion(); + if (this.state.isEdit) { + this.props.onCloseEdit(newDr, this.state.prevName, this.state.prevZoneType); + } else { + this.props.onAdd(newDr); + } + } + + private setName(n: string) { + if (n != this.state.name) { + this.setState({ name: n, needsSave: true }); + } + } + + private setStartFreq(n: number) { + if (n != this.state.startFreq) { + this.setState({ startFreq: n, needsSave: true }); + } + } + private setEndFreq(n: number) { + if (n != this.state.endFreq) { + this.setState({ endFreq: n, needsSave: true }); + } + } + + private setZoneType(n: string) { + if (n != this.state.zoneType && zoneTypes.includes(n)) { + this.setState({ zoneType: n, needsSave: true }); + } + } + + private setCircleLat(n: number) { + if (n != this.state.circleLat) { + this.setState({ circleLat: n, needsSave: true }); + } + } + + private setCircleLong(n: number) { + if (n != this.state.circleLong) { + this.setState({ circleLong: n, needsSave: true }); + } + } + + private setCircleRadius(n: number) { + if (n != this.state.radiusHeight) { + this.setState({ radiusHeight: n, needsSave: true }); + } + } + + private setRectTopLat(n: number, rectNumber: number): void { + if (rectNumber == 1) { + if (n != this.state.rect1topLat) { + this.setState({ rect1topLat: n, needsSave: true }); + } + } else if (rectNumber == 2) { + if (n != this.state.rect2topLat) { + this.setState({ rect2topLat: n, needsSave: true }); + } + } + } + + private setRectLeftLong(n: number, rectNumber: number): void { + if (rectNumber == 1) { + if (n != this.state.rect1leftLong) { + this.setState({ rect1leftLong: n, needsSave: true }); + } + } else if (rectNumber == 2) { + if (n != this.state.rect2leftLong) { + this.setState({ rect2leftLong: n, needsSave: true }); + } + } + } + + private setRectBottomLat(n: number, rectNumber: number): void { + if (rectNumber == 1) { + if (n != this.state.rect1bottomLat) { + this.setState({ rect1bottomLat: n, needsSave: true }); + } + } else if (rectNumber == 2) { + if (n != this.state.rect2bottomLat) { + this.setState({ rect2bottomLat: n, needsSave: true }); + } + } + } + private setRectRightLong(n: number, rectNumber: number): void { + if (rectNumber == 1) { + if (n != this.state.rect1rightLong) { + this.setState({ rect1rightLong: n, needsSave: true }); + } + } else if (rectNumber == 2) { + if (n != this.state.rect2rightLong) { + this.setState({ rect2rightLong: n, needsSave: true }); + } + } + } + + render() { + return ( + <> +
    + + + + this.setName(x)} + isValid={!!this.state.name && this.state.name.length > 0} + /> + + + + + this.setStartFreq(Number(x))} + isValid={!!this.state.startFreq && this.state.startFreq > 0} + /> + + + + + this.setEndFreq(Number(x))} + isValid={ + !!this.state.endFreq && this.state.endFreq > 0 && (this.state.startFreq ?? 0) <= this.state.endFreq + } + /> + + + + + + this.setZoneType(x)} + isValid={!!this.state.zoneType} + > + + + + + + + + + {this.state.zoneType == 'Circle' && ( + <> + + + + this.setCircleLat(Number(x))} + isValid={this.state.circleLat !== undefined} + /> + + + + + this.setCircleLong(Number(x))} + isValid={this.state.circleLong !== undefined} + /> + + + + + this.setCircleRadius(Number(x))} + isValid={this.state.radiusHeight !== undefined} + /> + + + + + )} + {this.state.zoneType == 'One Rectangle' && ( + <> + + + + this.setRectTopLat(Number(x), 1)} + isValid={this.state.circleLat !== undefined} + /> + + + + + this.setRectLeftLong(Number(x), 1)} + isValid={this.state.rect1leftLong !== undefined} + /> + + + + + this.setRectBottomLat(Number(x), 1)} + isValid={this.state.rect1bottomLat !== undefined} + /> + + + + + this.setRectRightLong(Number(x), 1)} + isValid={this.state.rect1rightLong !== undefined} + /> + + + + + )} + {this.state.zoneType == 'Two Rectangles' && ( + <> + + + + this.setRectTopLat(Number(x), 1)} + isValid={this.state.rect1topLat !== undefined} + /> + + + + + this.setRectLeftLong(Number(x), 1)} + isValid={this.state.rect1leftLong !== undefined} + /> + + + + + this.setRectBottomLat(Number(x), 1)} + isValid={this.state.rect1bottomLat !== undefined} + /> + + + + + this.setRectRightLong(Number(x), 1)} + isValid={this.state.rect1rightLong !== undefined} + /> + + + + + + + this.setRectTopLat(Number(x), 2)} + isValid={this.state.rect2topLat !== undefined} + /> + + + + + this.setRectLeftLong(Number(x), 2)} + isValid={this.state.rect2leftLong !== undefined} + /> + + + + + this.setRectBottomLat(Number(x), 2)} + isValid={this.state.rect2bottomLat !== undefined} + /> + + + + + this.setRectRightLong(Number(x), 2)} + isValid={this.state.rect2rightLong !== undefined} + /> + + + + + )} + {this.state.zoneType == 'Horizon Distance' && ( + <> + + + + this.setCircleLat(Number(x))} + isValid={this.state.circleLat !== undefined} + /> + + + + + this.setCircleLong(Number(x))} + isValid={this.state.circleLong !== undefined} + /> + + + + + this.setCircleRadius(Number(x))} + isValid={this.state.radiusHeight !== undefined} + /> + + + + + )} + + + + + + ); + } +} diff --git a/src/web/src/app/DynamicImport.tsx b/src/web/src/app/DynamicImport.tsx new file mode 100644 index 0000000..f1f8c57 --- /dev/null +++ b/src/web/src/app/DynamicImport.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; + +/** + * DynamicImport.tsx: component which loads app modules from the server on demand + * author: Sam Smucny + */ + +/** + * Interface definition for `IDynamicImport` + * @member load promise whose result is the page bundle with code needed to render children. Result is passed to the children rendering function + * @member children interior elements to render + * @member resolve promise object that is resoved before children are loaded. Once loaded the result of the promise can be accessed to construct the children + */ +interface IDynamicImport { + load: () => Promise; + children: (component: any, resolved: T) => JSX.Element; + resolve?: Promise; +} + +/** + * Class to dynamically load bundles from server + * this decreases the transfer size of individual parts + * also supports general resolves (can be used for API calls) when certain resources need to be + * loaded before a component is mounted + * @typeparam T (optional) type of resolve object + */ +class DynamicImport extends React.Component> { + public state = { + component: null, + resolve: undefined, + }; + public componentDidMount() { + (this.props.resolve !== undefined ? this.props.resolve : Promise.resolve(undefined as unknown as T)).then( + (resolve) => + this.props.load().then((component) => { + this.setState({ + component: component.default ? component.default : component, + resolve: resolve, + }); + }), + ); + } + public render() { + return this.props.children(this.state.component, this.state.resolve); + } +} + +export { DynamicImport }; diff --git a/src/web/src/app/ExclusionZone/ExclusionZone.tsx b/src/web/src/app/ExclusionZone/ExclusionZone.tsx new file mode 100644 index 0000000..94261e6 --- /dev/null +++ b/src/web/src/app/ExclusionZone/ExclusionZone.tsx @@ -0,0 +1,399 @@ +import * as React from 'react'; +import { + PageSection, + CardHead, + CardBody, + Card, + Title, + Expandable, + Alert, + Modal, + Button, + ClipboardCopy, + ClipboardCopyVariant, + AlertActionCloseButton, +} from '@patternfly/react-core'; +import 'react-measure'; +import { MapContainer, MapProps } from '../Components/MapContainer'; +import { ExclusionZoneResult, GeoJson, ExclusionZoneRequest } from '../Lib/RatApiTypes'; +import { runExclusionZone, cacheItem, getCacheItem, guiConfig } from '../Lib/RatApi'; +import { Limit } from '../Lib/Admin'; +import { Timer } from '../Components/Timer'; +import { ExclusionZoneForm } from './ExclusionZoneForm'; +import DownloadContents from '../Components/DownloadContents'; +import { logger } from '../Lib/Logger'; +import LoadLidarBounds from '../Components/LoadLidarBounds'; +import LoadRasBounds from '../Components/LoadRasBounds'; +import { AnalysisProgress } from '../Components/AnalysisProgress'; + +/** + * ExclusionZone.tsx: Page used for graphical ExclusionZone. Includes map display, channels, and spectrums + * author: Sam Smucny + */ + +/** + * Initial map properties + */ +const mapProps: MapProps = { + geoJson: { + type: 'FeatureCollection', + features: [], + }, + center: { + lat: 20, + lng: 180, + }, + mode: 'Exclusion', + zoom: 2, + versionId: 0, +}; + +interface MapState { + val: GeoJson; + text: string; + valid: boolean; + dimensions: { + width: number; + height: number; + }; + isModalOpen: boolean; + versionId: number; +} + +/** + * Page level component for running Exclusion Zone Analyses + */ +class ExclusionZone extends React.Component< + {}, + { + mapState: MapState; + mapCenter: { + lat: number; + lng: number; + }; + results?: ExclusionZoneResult; + + /** + * KML file that can be downloaded for more fidelity of results + */ + kml?: Blob; + + showParams: boolean; + messageType: 'None' | 'Info' | 'Warn' | 'Error'; + messageTitle: string; + messageValue: string; + extraWarning?: string; + extraWarningTitle?: string; + isModalOpen: boolean; + progress: { + percent: number; + message: string; + }; + canCancelTask: boolean; + limit: Limit; + } +> { + private styles: Map; + private paramValue?: ExclusionZoneRequest; + private formUpdateCallback?: (val: ExclusionZoneRequest) => void; + + /** + * Function used to cancel a long running task + */ + cancelTask?: () => void; + + handleJsonChange: (value: any) => void; + + constructor(props: any) { + super(props); + + const apiLimit = props.limit.kind === 'Success' ? props.limit.result : new Limit(false, 18); + + // set the default start state, but load from cache if available + this.state = { + limit: apiLimit, + showParams: true, + mapState: { + isModalOpen: false, + val: mapProps.geoJson, + text: '', + valid: false, + dimensions: { width: 0, height: 0 }, + versionId: 0, + }, + mapCenter: { + lat: 40, + lng: -100, + }, + messageTitle: '', + messageValue: '', + extraWarning: undefined, + extraWarningTitle: undefined, + messageType: 'None', + isModalOpen: false, + progress: { + percent: 0, + message: 'No Task Started', + }, + canCancelTask: false, + }; + + this.handleJsonChange = (value) => { + try { + this.setMapState({ text: value, valid: true, versionId: this.state.mapState.versionId + 1 }); + return; + } catch (e) { + this.setMapState({ text: value, valid: false, versionId: this.state.mapState.versionId + 1 }); + return; + } + }; + + this.cancelTask = undefined; + + this.styles = new Map([['BLDB', { fillOpacity: 0, strokeColor: 'blue' }]]); + } + + componentDidMount() { + const st = getCacheItem('ExclusionZoneStateCache'); + if (st !== undefined) this.setState(st); + } + + componentWillUnmount() { + // before removing object, let's cache the state in case we want to come back + const state: any = this.state; + state.messageType = 'None'; + + // cancel running task + this.cancelTask && this.cancelTask(); + + cacheItem('ExclusionZoneStateCache', state); + } + + private setMapState(obj: any) { + this.setState({ mapState: Object.assign(this.state.mapState, obj) }); + } + + /** + * Run an Exclusion Zone analysis on the AFC Engine + * @param params Parameters to send to AFC Engine + */ + private async runExclusionZone(params: ExclusionZoneRequest) { + // check AGL settings and possibly truncate + if (params.heightType === 'AGL' && params.height - params.heightUncertainty < 1) { + // modify if height is not 1m above terrain height + const minHeight = 1; + const maxHeight = params.height + params.heightUncertainty; + if (maxHeight < minHeight) { + this.setState({ + messageType: 'Error', + messageTitle: 'Invalid Height', + messageValue: `The height value must allow the AP to be at least 1m above the terrain. Currently the maximum height is ${maxHeight}m`, + }); + return; + } + const newHeight = (minHeight + maxHeight) / 2; + const newUncertainty = newHeight - minHeight; + params.height = newHeight; + params.heightUncertainty = newUncertainty; + logger.warn('Height was not at least 1 m above terrain, so it was truncated to fit AFC requirement'); + this.setState({ + extraWarningTitle: 'Truncated Height', + extraWarning: `The AP height has been truncated so that its minimum height is 1m above the terrain. The new height is ${newHeight}+/-${newUncertainty}m`, + }); + } + + this.setState({ + messageType: 'Info', + messageTitle: 'Working...', + messageValue: 'Your request is being processed', + progress: { + percent: 0, + message: 'Submitting...', + }, + }); + + let taskCanceled = false; + this.cancelTask = () => { + taskCanceled = true; + this.cancelTask = undefined; + this.setState({ canCancelTask: false }); + }; + this.setState({ canCancelTask: true }); + + await runExclusionZone( + params, + () => taskCanceled, + (prog) => this.setState({ progress: prog }), + (kml: Blob) => this.setState({ kml: kml }), + ).then((res) => { + if (res.kind === 'Success') { + if (res.result.geoJson.features.length !== 1 || res.result.geoJson.features[0].properties.kind !== 'ZONE') { + this.setState({ + messageType: 'Error', + messageTitle: 'Response Error', + messageValue: 'The result value from the analysis was malformed.', + canCancelTask: false, + }); + // short circuit + return; + } + + this.setState({ + results: res.result, + messageType: res.result.statusMessageList.length ? 'Warn' : 'None', + messageTitle: res.result.statusMessageList.length ? 'Status messages' : '', + messageValue: res.result.statusMessageList.length ? res.result.statusMessageList.join('\n') : '', + mapCenter: { + lat: res.result.geoJson.features[0].properties.lat, + lng: res.result.geoJson.features[0].properties.lon, + }, + canCancelTask: false, + }); + + this.setMapState({ + val: res.result.geoJson, + text: JSON.stringify(res.result.geoJson, null, 2), + valid: false, + versionId: this.state.mapState.versionId + 1, + }); + } else { + // error in running ExclusionZone + this.setState({ + messageType: 'Error', + messageTitle: 'Error: ' + res.errorCode, + messageValue: res.description, + canCancelTask: false, + }); + } + }); + + return; + } + + private setConfig(value: string) { + try { + const params: ExclusionZoneRequest = JSON.parse(value) as ExclusionZoneRequest; + if (this.formUpdateCallback) this.formUpdateCallback(params); + } catch (e) { + logger.error('Pasted value is not valid JSON'); + } + } + + private copyPaste(formData: ExclusionZoneRequest, updateCallback: (v: ExclusionZoneRequest) => void) { + this.formUpdateCallback = updateCallback; + this.paramValue = formData; + this.setState({ isModalOpen: true }); + } + + private getParamsText = () => (this.paramValue ? JSON.stringify(this.paramValue) : ''); + render() { + const toggleParams = () => this.setState({ showParams: !this.state.showParams }); + const runAnalysis = (x: ExclusionZoneRequest) => this.runExclusionZone(x); + + return ( + + + + Run Exclusion Zone + + this.setState({ isModalOpen: false })} + actions={[ + , + ]} + > + this.setConfig(v)} + aria-label="text area" + > + {this.getParamsText()} + + + + + void) => + this.copyPaste(formData, updateCallback) + } + /> + {this.state.canCancelTask && ( + <> + {' '} + + + )}{' '} + this.setMapState({ val: data, versionId: this.state.mapState.versionId + 1 })} + /> + this.setMapState({ val: data, versionId: this.state.mapState.versionId + 1 })} + /> + + + +
    + {this.state.extraWarning && ( + this.setState({ extraWarning: undefined, extraWarningTitle: undefined })} + /> + } + > +
    {this.state.extraWarning}
    +
    + )} + {this.state.messageType === 'Info' && ( + + {this.state.messageValue} + + + )} + {this.state.messageType === 'Error' && ( + +
    {this.state.messageValue}
    +
    + )} + {this.state.messageType === 'Warn' && ( + +
    {this.state.messageValue}
    +
    + )} +
    +
    + +
    + {this.state.results && this.state.kml && ( + this.state.kml} fileName="results.kmz" /> + )} +
    + ); + } +} + +export { ExclusionZone }; diff --git a/src/web/src/app/ExclusionZone/ExclusionZoneForm.tsx b/src/web/src/app/ExclusionZone/ExclusionZoneForm.tsx new file mode 100644 index 0000000..754f3d3 --- /dev/null +++ b/src/web/src/app/ExclusionZone/ExclusionZoneForm.tsx @@ -0,0 +1,384 @@ +import * as React from 'react'; +import { + FormGroup, + TextInput, + FormSelectOption, + FormSelect, + ActionGroup, + Button, + Alert, + AlertActionCloseButton, + InputGroupText, + InputGroup, + Gallery, + GalleryItem, +} from '@patternfly/react-core'; +import { HeightType, IndoorOutdoorType, ExclusionZoneRequest, RatResponse } from '../Lib/RatApiTypes'; +import { cacheItem, getCacheItem } from '../Lib/RatApi'; +import { logger } from '../Lib/Logger'; +import { Limit } from '../Lib/Admin'; + +/** + * ExclusionZoneForm.tsx: form for capturing paramaters for exclusion zone and validating them + * author: Sam Smucny + */ + +/** + * Form component for entering parameters for Exclusion Zone + * @param onSubmit callback to parent when a valid object of parameters is ready to be submitted + */ +export class ExclusionZoneForm extends React.Component< + { + limit: Limit; + onSubmit: (a: ExclusionZoneRequest) => Promise; + onCopyPaste?: (formData: ExclusionZoneRequest, updateCallback: (v: ExclusionZoneRequest) => void) => void; + }, + { + height?: number; + heightType: HeightType; + heightCert?: number; + insideOutside: IndoorOutdoorType; + eirp?: number; + bandwidth?: number; + centerFreq?: number; + fsid?: number; + mesgType?: 'danger' | 'info' | 'success'; + mesgTitle?: string; + mesgBody?: string; + } +> { + private static heightTypes: string[] = [HeightType.AGL.toString(), HeightType.AMSL.toString()]; + private static inOutDoors: [string, string][] = [ + ['INDOOR', IndoorOutdoorType.INDOOR.toString()], + ['OUTDOOR', IndoorOutdoorType.OUTDOOR.toString()], + ['ANY', IndoorOutdoorType.ANY.toString()], + ]; + private static bandwidths: number[] = [20, 40, 80, 160]; + private static startingFreq = 5945; + private static centerFrequencies: Map = new Map([ + [ + 20, + Array(59) + .fill(0) + .map((_, i) => i * 20 + ExclusionZoneForm.startingFreq + 10), + ], + [ + 40, + Array(29) + .fill(0) + .map((_, i) => i * 40 + ExclusionZoneForm.startingFreq + 20), + ], + [ + 80, + Array(14) + .fill(0) + .map((_, i) => i * 80 + ExclusionZoneForm.startingFreq + 40), + ], + [ + 160, + Array(7) + .fill(0) + .map((_, i) => i * 160 + ExclusionZoneForm.startingFreq + 80), + ], + ]); + + constructor(props: Readonly<{ limit: Limit; onSubmit: (a: ExclusionZoneRequest) => Promise> }>) { + super(props); + this.state = { + height: undefined, + heightType: HeightType.AMSL, + heightCert: undefined, + insideOutside: IndoorOutdoorType.INDOOR, + eirp: undefined, + bandwidth: undefined, + centerFreq: undefined, + fsid: undefined, + mesgType: undefined, + }; + } + + componentDidMount() { + const st = getCacheItem('exclusionZoneForm'); + if (st !== undefined) this.setState(st); + } + + componentWillUnmount() { + const state: any = this.state; + state.mesgType = undefined; + cacheItem('exclusionZoneForm', state); + } + + private validHeight = (s?: number) => s !== undefined && Number.isFinite(s); + private validHeightType = (s?: string) => !!s; + private validHeightCert = (s?: number) => s !== undefined && Number.isFinite(s) && s >= 0; + private validInOut = (s?: string) => !!s; + private validEirp = (s?: number) => { + if (this.props.limit.enforce) { + return s !== undefined && Number.isFinite(s) && s >= this.props.limit.limit; + } + return s !== undefined && Number.isFinite(s); + }; + private validBandwidth = (s?: number) => s !== undefined && Number.isFinite(s); + private validCenterFreq = (s?: number) => s !== undefined && Number.isFinite(s); + private validFsid = (s?: number) => !!s && Number.isInteger(s); + + private submit = () => { + const allValid = [ + this.validHeight(this.state.height), + this.validHeightType(this.state.heightType), + this.validHeightCert(this.state.heightCert), + this.validInOut(this.state.insideOutside), + this.validEirp(this.state.eirp), + this.validBandwidth(this.state.bandwidth), + this.validCenterFreq(this.state.centerFreq), + this.validFsid(this.state.fsid), + ].reduce((p, c) => p && c); + + if (!allValid) { + logger.error('Invalid inputs when submitting ExlusionZoneForm'); + this.setState({ mesgType: 'danger', mesgTitle: 'Invalid Inputs', mesgBody: 'One or more inputs are invalid' }); + return; + } + + logger.info('running exclusion zone with form state: ', this.state); + this.setState({ mesgType: 'info' }); + this.props + .onSubmit({ + height: this.state.height!, + heightType: this.state.heightType, + heightUncertainty: this.state.heightCert!, + indoorOutdoor: this.state.insideOutside, + EIRP: this.state.eirp!, + bandwidth: this.state.bandwidth!, + centerFrequency: this.state.centerFreq!, + FSID: this.state.fsid!, + }) + .then(() => { + this.setState({ mesgType: undefined }); + }) + .catch(() => { + this.setState({ mesgType: undefined }); + }); + }; + + private copyPasteClick = () => + this.props.onCopyPaste && + this.props.onCopyPaste( + { + height: this.state.height!, + heightType: this.state.heightType, + heightUncertainty: this.state.heightCert!, + indoorOutdoor: this.state.insideOutside, + EIRP: this.state.eirp!, + bandwidth: this.state.bandwidth!, + centerFrequency: this.state.centerFreq!, + FSID: this.state.fsid!, + } as ExclusionZoneRequest, + this.setParams, + ); + + private setParams = (v: ExclusionZoneRequest) => + this.setState({ + height: v.height, + heightType: v.heightType, + heightCert: v.heightUncertainty, + insideOutside: v.indoorOutdoor, + eirp: v.EIRP, + bandwidth: v.bandwidth, + centerFreq: v.centerFrequency, + fsid: v.FSID, + }); + + render() { + return ( + <> + + + + + this.setState({ height: Number(x) })} + type="number" + step="any" + id="horizontal-form-height" + name="horizontal-form-height" + isValid={this.validHeight(this.state.height)} + style={{ textAlign: 'right' }} + /> + meters + + + + + + + this.setState({ heightType: HeightType[x] })} + id="horzontal-form-height-type" + name="horizontal-form-height-type" + isValid={this.validHeightType(this.state.heightType)} + style={{ textAlign: 'right' }} + > + + {ExclusionZoneForm.heightTypes.map((option) => ( + + ))} + + + + + + + + this.setState({ heightCert: Number(x) })} + type="number" + step="any" + id="horizontal-form-height-cert" + name="horizontal-form-height-cert" + isValid={this.validHeightCert(this.state.heightCert)} + style={{ textAlign: 'right' }} + /> + meters + + + + + + + this.setState({ insideOutside: x })} + id="horzontal-form-indoor-outdoor" + name="horizontal-form-indoor-outdoor" + isValid={this.validInOut(this.state.insideOutside)} + style={{ textAlign: 'right' }} + > + + {ExclusionZoneForm.inOutDoors.map(([option, label]) => ( + + ))} + + + + + + + + this.setState({ eirp: Number(x) })} + type="number" + step="any" + id="horizontal-form-eirp" + name="horizontal-form-eirp" + isValid={this.validEirp(this.state.eirp)} + style={{ textAlign: 'right' }} + /> + dBm + + + + + + + this.setState({ bandwidth: Number.parseInt(x), centerFreq: undefined })} + id="horzontal-form-bandwidth" + name="horizontal-form-bandwidth" + isValid={this.validBandwidth(this.state.bandwidth)} + style={{ textAlign: 'right' }} + > + + {ExclusionZoneForm.bandwidths.map((option) => ( + + ))} + + MHz + + + + + + + this.setState({ centerFreq: Number.parseInt(x) })} + id="horzontal-form-centfreq" + name="horizontal-form-centfreq" + isValid={this.validCenterFreq(this.state.centerFreq)} + style={{ textAlign: 'right' }} + isDisabled={!this.state.bandwidth} + > + + {(ExclusionZoneForm.centerFrequencies.get(this.state.bandwidth || 0) || []).map((option) => ( + + ))} + + MHz + + + + + + + this.setState({ fsid: Number(x) })} + type="number" + step="any" + id="horizontal-form-fsid" + name="horizontal-form-fsid" + isValid={this.validFsid(this.state.fsid)} + style={{ textAlign: 'right' }} + /> + + + + +
    + + {this.state.mesgType && this.state.mesgType !== 'info' && ( + this.setState({ mesgType: undefined })} />} + > + {this.state.mesgBody} + + )} + +
    + <> + {' '} + + + + ); + } +} diff --git a/src/web/src/app/HeatMap/ColorLegend.tsx b/src/web/src/app/HeatMap/ColorLegend.tsx new file mode 100644 index 0000000..bae9bb8 --- /dev/null +++ b/src/web/src/app/HeatMap/ColorLegend.tsx @@ -0,0 +1,105 @@ +import * as React from 'react'; +import { Card, CardHeader, CardBody } from '@patternfly/react-core'; +import { HeatMapAnalysisType } from '../Lib/RatApiTypes'; + +/** + * ColorLegend.tsx: Graphical element that shows colors displayed on heatmap + * author: Sam Smucny + */ + +/** + * Calculates the color to be used for a given tile based on its gain + * @param val value to find color for + * @param threshold threshold to measure `val` against + * @returns the color of the tile as a CSS color + */ +export const getColor = (val: number, threshold: number) => + val < threshold - 20 + ? 'LightGray' + : val < threshold + ? 'Gray' + : val < threshold + 3 + ? 'Blue' + : val < threshold + 6 + ? 'DarkBlue' + : val < threshold + 9 + ? 'Green' + : val < threshold + 12 + ? 'DarkGreen' + : val < threshold + 15 + ? 'Yellow' + : val < threshold + 18 + ? 'Orange' + : val < threshold + 21 + ? 'Red' + : 'Maroon'; + +/** + * list of color ranges + * @param threshold threshold value to shift colors by + */ +const colorValues = (threshold: number) => [ + { label: '< ' + (threshold - 20), color: getColor(threshold - 30, threshold) }, + { label: '[' + (threshold - 20) + ', ' + threshold + ')', color: getColor(threshold - 10, threshold) }, + { label: '[' + threshold + ', ' + (threshold + 3) + ')', color: getColor(threshold + 1, threshold) }, + { label: '[' + (threshold + 3) + ', ' + (threshold + 6) + ')', color: getColor(threshold + 4, threshold) }, + { label: '[' + (threshold + 6) + ', ' + (threshold + 9) + ')', color: getColor(threshold + 7, threshold) }, + { label: '[' + (threshold + 9) + ', ' + (threshold + 12) + ')', color: getColor(threshold + 10, threshold) }, + { label: '[' + (threshold + 12) + ', ' + (threshold + 15) + ')', color: getColor(threshold + 13, threshold) }, + { label: '[' + (threshold + 15) + ', ' + (threshold + 18) + ')', color: getColor(threshold + 16, threshold) }, + { label: '[' + (threshold + 18) + ', ' + (threshold + 21) + ')', color: getColor(threshold + 19, threshold) }, + { label: '>= ' + (threshold + 21), color: getColor(threshold + 22, threshold) }, +]; + +/** + * Legend which shows gain values accociated with different colors on the heat map + * @param threshold relative value to shift color values by + */ +export const ColorLegend: React.FunctionComponent<{ threshold: number; analysisType: HeatMapAnalysisType }> = ( + props, +) => ( + + + {props.analysisType === HeatMapAnalysisType.ItoN ? <>I/N Legend (dB) : <>EIRP Legend (dBm)} + + +
    +
    + Denied region +
    +
    +
    +
    + No restriction +
    +
    + {props.analysisType === HeatMapAnalysisType.ItoN && + colorValues(props.threshold || -6).map((val, i) => ( +
    +
    + {val.label} +
    +
    + ))} + {props.analysisType === HeatMapAnalysisType.EIRP && ( + <> +
    +
    + maxEIRP is met +
    +
    +
    +
    + minEIRP <= EIRP < maxEIRP +
    +
    +
    +
    + EIRP < minEIRP +
    +
    + + )} +
    +
    +); diff --git a/src/web/src/app/HeatMap/HeatMap.tsx b/src/web/src/app/HeatMap/HeatMap.tsx new file mode 100644 index 0000000..33b39e5 --- /dev/null +++ b/src/web/src/app/HeatMap/HeatMap.tsx @@ -0,0 +1,737 @@ +import * as React from 'react'; +import { + PageSection, + CardHead, + CardBody, + Card, + Title, + Expandable, + FormGroup, + InputGroup, + TextInput, + InputGroupText, + Gallery, + GalleryItem, + Alert, + AlertActionCloseButton, + Button, + Modal, + ClipboardCopy, + ClipboardCopyVariant, +} from '@patternfly/react-core'; +import { MapContainer, MapProps } from '../Components/MapContainer'; +import { + HeatMapResult, + Bounds, + GeoJson, + error, + success, + HeatMapRequest, + IndoorOutdoorType, + HeatMapFsIdType, + HeatMapAnalysisType, +} from '../Lib/RatApiTypes'; +import { cacheItem, getCacheItem, heatMapRequestObject } from '../Lib/RatApi'; +import { HeatMapForm, HeatMapFormData } from './HeatMapForm'; +import { logger } from '../Lib/Logger'; +import { ColorLegend, getColor } from './ColorLegend'; +import { RegionSize } from './RegionSize'; +import { AnalysisProgress } from '../Components/AnalysisProgress'; +import LoadLidarBounds from '../Components/LoadLidarBounds'; +import LoadRasBounds from '../Components/LoadRasBounds'; +import { Limit } from '../Lib/Admin'; +import { spectrumInquiryRequestByString } from '../Lib/RatAfcApi'; +import { VendorExtension, AvailableSpectrumInquiryRequest } from '../Lib/RatAfcTypes'; +import { fromPer } from '../Lib/Utils'; + +/** + * HeatMap.tsx: Page used for graphical heatmap. Includes map display, channels, and spectrums + * author: Sam Smucny + */ + +/** + * Initial value of map properties + */ +const mapProps: MapProps = { + geoJson: { + type: 'FeatureCollection', + features: [], + }, + center: { + lat: 20, + lng: 180, + }, + mode: 'Heatmap', + zoom: 2, + versionId: 0, +}; + +interface MapState { + val: GeoJson; + text: string; + valid: boolean; + dimensions: { + width: number; + height: number; + }; + isModalOpen: boolean; + versionId: number; +} + +/** + * Page level component where user can run heat map analyses + */ +class HeatMap extends React.Component< + {}, + { + mapState: MapState; + selectionRectangle?: Bounds; + spacing?: number; + mapCenter: { + lat: number; + lng: number; + }; + results?: HeatMapResult; + showParams: boolean; + canCancelTask: boolean; + messageType?: 'info' | 'warning' | 'danger'; + messageTitle: string; + messageValue: string; + extraWarning?: string; + extraWarningTitle?: string; + progress: { + percent: number; + message: string; + }; + isModalOpen: boolean; + limit: Limit; + rulesetIds: string[]; + submittedAnalysisType?: HeatMapAnalysisType; + } +> { + handleJsonChange: (value: any) => void; + + /** + * Function used to cancel a long running task + */ + cancelTask?: () => void; + + minItoN: number; + maxItoN: number; + ItoNthreshold: number; + private paramValue?: HeatMapRequest; + private formUpdateCallback?: (val: HeatMapRequest) => void; + + constructor(props: any) { + super(props); + + const apiLimit = props.limit.kind === 'Success' ? props.limit.result : new Limit(false, false, 18, 18); + const rulesetIds = props.rulesetIds.kind === 'Success' ? props.rulesetIds.result : []; + // set the default start state, but load from cache if available + this.state = { + limit: apiLimit, + showParams: true, + mapState: { + isModalOpen: false, + val: mapProps.geoJson, + text: '', + valid: false, + dimensions: { width: 0, height: 0 }, + versionId: 0, + }, + mapCenter: { + lat: 40, + lng: -100, + }, + messageTitle: '', + messageValue: '', + extraWarning: undefined, + extraWarningTitle: undefined, + messageType: undefined, + selectionRectangle: undefined, + canCancelTask: false, + progress: { + percent: 0, + message: 'No Task Started', + }, + isModalOpen: false, + rulesetIds: rulesetIds, + }; + + this.minItoN = Number.NaN; + this.maxItoN = Number.NaN; + this.ItoNthreshold = Number.NaN; + + this.handleJsonChange = (value) => { + try { + this.setMapState({ text: value, valid: true, versionId: this.state.mapState.versionId + 1 }); + return; + } catch (e) { + this.setMapState({ text: value, valid: false, versionId: this.state.mapState.versionId + 1 }); + return; + } + }; + + this.cancelTask = undefined; + } + + componentDidMount() { + const st = getCacheItem('heatmapStateCache'); + if (st !== undefined) { + this.ItoNthreshold = st.ItoNThreshold; + this.setState(st); + } + } + + componentWillUnmount() { + // before removing object, let's cache the state in case we want to come back + const state: any = this.state; + state.messageType = undefined; + state.ItoNThreshold = this.ItoNthreshold; + + // cancel running task + this.cancelTask && this.cancelTask(); + + cacheItem('heatmapStateCache', state); + } + + private setN = (rect: Bounds, n: number) => { + rect.north = n; + return rect; + }; + private setS = (rect: Bounds, s: number) => { + rect.south = s; + return rect; + }; + private setE = (rect: Bounds, e: number) => { + rect.east = e; + return rect; + }; + private setW = (rect: Bounds, w: number) => { + rect.west = w; + return rect; + }; + + private validN = (r?: Bounds) => r && r.north <= 90 && r.north >= -90; + private validS = (r?: Bounds) => r && r.south <= 90 && r.south >= -90; + private validE = (r?: Bounds) => r && r.east <= 180 && r.east >= -180; + private validW = (r?: Bounds) => r && r.west <= 180 && r.west >= -180; + private validSpacing = (s?: number) => !!s && s > 0; + + private setDir = (setter: (r: Bounds, i: number) => Bounds) => (s: string) => { + if (!this.state.selectionRectangle) { + const val = Number.parseFloat(s); + this.setState({ + selectionRectangle: setter( + { + north: 0, + south: 0, + east: 0, + west: 0, + }, + val, + ), + }); + } else { + const rect = this.state.selectionRectangle; + setter(rect, Number.parseFloat(s)); + this.setState({ selectionRectangle: rect }); + this.setMapState({ versionId: this.state.mapState.versionId + 1 }); + } + }; + + private setMapState(obj: any) { + this.setState({ mapState: Object.assign(this.state.mapState, obj) }); + } + + private onRectUpdate(rect: any): void { + this.setState({ selectionRectangle: rect.getBounds().toJSON() }); + } + + private formSubmitted(form: HeatMapFormData) { + if (!this.state.selectionRectangle) + return Promise.resolve( + error('No Bounding Box Specified', undefined, 'Specifiy a bounding box to submit a request'), + ); + const validBounds = [ + ...[this.validN, this.validE, this.validS, this.validW].map((f) => f(this.state.selectionRectangle)), + this.validSpacing(this.state.spacing), + ].reduce((p, c) => p && c); + + if (!validBounds) + return Promise.resolve( + error('Invalid Bounding Box', undefined, 'One or more parameters for the bounding box are invalid'), + ); + + // check AGL settings and possibly truncate + if (form.indoorOutdoor.kind === IndoorOutdoorType.BUILDING) { + let stringHeight = ''; + + // check indoor + const formIn = form.indoorOutdoor.in; + if (formIn.heightType === 'AGL' && formIn.height - formIn.heightUncertainty < 1) { + const minHeight = 1; + const maxHeight = formIn.height + formIn.heightUncertainty; + if (maxHeight < minHeight) { + this.setState({ + messageType: 'danger', + messageTitle: 'Invalid Height', + messageValue: `The height value must allow the AP to be at least 1m above the terrain. Currently the maximum height for inside is ${maxHeight}m`, + }); + return; + } + const newHeight = (minHeight + maxHeight) / 2; + const newUncertainty = newHeight - minHeight; + form.indoorOutdoor.in.height = newHeight; + form.indoorOutdoor.in.heightUncertainty = newUncertainty; + stringHeight = `${newHeight}+/-${newUncertainty}m for indoors`; + } + + // check outdoor + const formOut = form.indoorOutdoor.out; + if (formOut.heightType === 'AGL' && formOut.height - formOut.heightUncertainty < 1) { + const minHeight = 1; + const maxHeight = formOut.height + formOut.heightUncertainty; + if (maxHeight < minHeight) { + this.setState({ + messageType: 'danger', + messageTitle: 'Invalid Height', + messageValue: `The height value must allow the AP to be at least 1m above the terrain. Currently the maximum height for outside is ${maxHeight}m`, + }); + return; + } + const newHeight = (minHeight + maxHeight) / 2; + const newUncertainty = newHeight - minHeight; + form.indoorOutdoor.out.height = newHeight; + form.indoorOutdoor.out.heightUncertainty = newUncertainty; + if (stringHeight !== '') { + stringHeight += ' and '; + } + stringHeight += `${newHeight}+/-${newUncertainty}m for outdoors`; + } + + // display warning if something was changed + if (stringHeight !== '') { + logger.warn('Height was not at least 1 m above terrain, so it was truncated to fit AFC requirement'); + this.setState({ + extraWarningTitle: 'Truncated Height', + extraWarning: `The AP height has been truncated so that its minimum height is 1m above the terrain. The new height is ${stringHeight}`, + }); + } + } else { + if ( + form.indoorOutdoor.heightType === 'AGL' && + form.indoorOutdoor.height - form.indoorOutdoor.heightUncertainty < 1 + ) { + // modify if height is not 1m above terrain height + const minHeight = 1; + const maxHeight = form.indoorOutdoor.height + form.indoorOutdoor.heightUncertainty; + if (maxHeight < minHeight) { + this.setState({ + messageType: 'danger', + messageTitle: 'Invalid Height', + messageValue: `The height value must allow the AP to be at least 1m above the terrain. Currently the maximum height is ${maxHeight}m`, + }); + return; + } + const newHeight = (minHeight + maxHeight) / 2; + const newUncertainty = newHeight - minHeight; + form.indoorOutdoor.height = newHeight; + form.indoorOutdoor.heightUncertainty = newUncertainty; + logger.warn('Height was not at least 1 m above terrain, so it was truncated to fit AFC requirement'); + this.setState({ + extraWarningTitle: 'Truncated Height', + extraWarning: `The AP height has been truncated so that its minimum height is 1m above the terrain. The new height is ${newHeight}+/-${newUncertainty}m`, + }); + } + } + + logger.info('running heat map: ', this.state); + this.setState({ + messageType: 'info', + messageTitle: 'Processing', + messageValue: '', + progress: { percent: 0, message: 'Submitting...' }, + submittedAnalysisType: form.analysis, + }); + + //Build request + let extension = generateVendorExtension(form, this.state.spacing!, this.state.selectionRectangle!); + + let dummyRequest = heatMapRequestObject(extension, form.certificationId, form.serialNumber); + + return spectrumInquiryRequestByString('1.4', JSON.stringify(dummyRequest)) + .then((r) => { + logger.info(r); + if (r.kind === 'Success') { + const response = r.result.availableSpectrumInquiryResponses[0]; + if ( + response.vendorExtensions && + response.vendorExtensions.length > 0 && + response.vendorExtensions.findIndex((x) => x.extensionId == 'openAfc.mapinfo') >= 0 + ) { + //Get the KML file and load it into the state.kml parameters; get the GeoJson if present + let kml_filename = response.vendorExtensions.find((x) => x.extensionId == 'openAfc.mapinfo')?.parameters[ + 'kmzFile' + ]; + if (!!kml_filename) { + //this.setKml(kml_filename) + } + let geoJsonData = response.vendorExtensions.find((x) => x.extensionId == 'openAfc.mapinfo')?.parameters[ + 'geoJsonFile' + ]; + let centerLat = (this.state.selectionRectangle!.north + this.state.selectionRectangle!.south) / 2; + let centerLon = (this.state.selectionRectangle!.east + this.state.selectionRectangle!.west) / 2; + + if (!!geoJsonData) { + let geojson = JSON.parse(geoJsonData); + this.ItoNthreshold = geojson.threshold; + this.maxItoN = geojson.maxItoN; + this.minItoN = geojson.minItoN; + //this.heatMapStyle.set("HMAP", (feature: any) => ({ strokeWeight: 0, fillColor: feature.getProperty("ItoN"), zIndex: 5 })); + this.heatMapStyle.set('HMAP', (feature: any) => ({ + strokeWeight: 0, + fillColor: feature.getProperty('fill'), + 'fill-opacity': feature.getProperty('fill-opacity'), + zIndex: 5, + })); + this.setMapState({ + val: geojson.geoJson as GeoJson, + valid: true, + versionId: this.state.mapState.versionId + 1, + }); + this.setState({ + canCancelTask: false, + messageType: undefined, + messageTitle: '', + messageValue: '', + mapCenter: { lng: centerLon, lat: centerLat }, + }); + } + } + return success('Request completed successfully'); + } else { + logger.error(r); + this.setState({ + canCancelTask: false, + messageType: 'danger', + messageTitle: 'An error occured while running your request', + messageValue: r.description, + }); + return success(''); + } + }) + .catch((e) => { + this.setState({ + canCancelTask: false, + messageType: 'danger', + messageTitle: 'An unexpected error occured', + messageValue: JSON.stringify(e), + }); + return error('Your request was unable to be processed', undefined, e); + }); + } + + private heatMapStyle = new Map React.CSSProperties)>([ + [ + 'HMAP', + (feature: any) => ({ + strokeWeight: 0, + fillColor: feature.getProperty('fill'), + 'fill-opacity': feature.getProperty('fill-opacity'), + zIndex: 5, + }), + ], + [ + 'BLDB', + // @ts-ignore + { strokeColor: 'blue', fillOpacity: 0, zIndex: 0 }, + ], + ]); + + private setConfig(value: string) { + try { + const params: HeatMapRequest = JSON.parse(value) as HeatMapRequest; + this.setState({ selectionRectangle: params.bounds, spacing: params.spacing }); + this.setMapState({ versionId: this.state.mapState.versionId + 1 }); + if (this.formUpdateCallback) this.formUpdateCallback(params); + } catch (e) { + logger.error('Pasted value is not valid JSON'); + } + } + + private copyPaste(formData: HeatMapRequest, updateCallback: (v: HeatMapRequest) => void) { + this.formUpdateCallback = updateCallback; + formData.bounds = this.state.selectionRectangle!; + formData.spacing = this.state.spacing!; + this.paramValue = formData; + this.setState({ isModalOpen: true }); + } + + private getParamsText = () => (this.paramValue ? JSON.stringify(this.paramValue) : ''); + + render() { + const toggleParams = () => this.setState({ showParams: !this.state.showParams }); + + return ( + + + + Run Heat Map + + this.setState({ isModalOpen: false })} + actions={[ + , + ]} + > + this.setConfig(v)} + aria-label="text area" + > + {this.getParamsText()} + + + + + Heat Map Bounds + + + + + + + degrees + + + + + + + + degrees + + + + + + + + degrees + + + + + + + + degrees + + + + + + + this.setState({ spacing: Number(x) })} + type="number" + step="any" + id="horizontal-form-fsid" + name="horizontal-form-fsid" + isValid={this.validSpacing(this.state.spacing)} + style={{ textAlign: 'right' }} + /> + meters + + + + +
    + Simulation Parameters +
    + this.formSubmitted(a)} + onCopyPaste={(form, callback) => this.copyPaste(form, callback)} + /> + {this.state.canCancelTask && ( + <> + {' '} + + + )}{' '} + this.setMapState({ val: data, versionId: this.state.mapState.versionId + 1 })} + /> + this.setMapState({ val: data, versionId: this.state.mapState.versionId + 1 })} + /> +
    +
    +
    + {this.state.extraWarning && ( + this.setState({ extraWarning: undefined, extraWarningTitle: undefined })} + /> + } + > +
    {this.state.extraWarning}
    +
    + )} + {this.state.messageType && ( + <> +
    + this.setState({ messageType: undefined })} /> + ) + } + > +
    {this.state.messageValue}
    + {this.state.messageType === 'info' && ( + + )} +
    + + )} +
    + {!!this.state.submittedAnalysisType && ( + + )} +
    +
    + this.onRectUpdate(rect)} + geoJson={this.state.mapState.val} + center={this.state.mapCenter} + zoom={mapProps.zoom} + styles={this.heatMapStyle} + versionId={this.state.mapState.versionId} + /> +
    +
    + ); + } +} + +// if any of the bounds are not defined then propogate the undefined +const correctBounds = (rect?: Bounds): Bounds | undefined => + !rect || [rect.north, rect.south, rect.east, rect.west].map((x) => !Number.isFinite(x)).reduce((a, b) => a || b) + ? undefined + : rect; + +export { HeatMap }; + +function generateVendorExtension(form: HeatMapFormData, spacing: number, location: Bounds) { + let v: VendorExtension = { + extensionId: 'openAfc.heatMap', + parameters: { + type: 'heatmap', + inquiredChannel: form.inquiredChannel, + MinLon: location.west, + MaxLon: location.east, + MinLat: location.south, + MaxLat: location.north, + RLANSpacing: spacing, + analysis: form.analysis, + fsIdType: form.fsIdType, + }, + }; + + if (form.fsIdType === HeatMapFsIdType.Single) { + v.parameters['fsId'] = form.fsId; + } + + switch (form.indoorOutdoor.kind) { + case IndoorOutdoorType.INDOOR: + v.parameters.IndoorOutdoorStr = IndoorOutdoorType.INDOOR; + v.parameters.RLANIndoorEIRPDBm = form.indoorOutdoor.EIRP; + v.parameters.RLANIndoorHeightType = form.indoorOutdoor.heightType; + v.parameters.RLANIndoorHeight = form.indoorOutdoor.height; + v.parameters.RLANIndoorHeightUncertainty = form.indoorOutdoor.heightUncertainty; + break; + case IndoorOutdoorType.OUTDOOR: + v.parameters.IndoorOutdoorStr = IndoorOutdoorType.OUTDOOR; + v.parameters.RLANOutdoorEIRPDBm = form.indoorOutdoor.EIRP; + v.parameters.RLANOutdoorHeightType = form.indoorOutdoor.heightType; + v.parameters.RLANOutdoorHeight = form.indoorOutdoor.height; + v.parameters.RLANOutdoorHeightUncertainty = form.indoorOutdoor.heightUncertainty; + break; + case IndoorOutdoorType.ANY: + // Is this used? + break; + case IndoorOutdoorType.BUILDING: + v.parameters.IndoorOutdoorStr = IndoorOutdoorType.BUILDING; + v.parameters.RLANIndoorEIRPDBm = form.indoorOutdoor.in.EIRP; + v.parameters.RLANIndoorHeightType = form.indoorOutdoor.in.heightType; + v.parameters.RLANIndoorHeight = form.indoorOutdoor.in.height; + v.parameters.RLANIndoorHeightUncertainty = form.indoorOutdoor.in.heightUncertainty; + v.parameters.RLANOutdoorEIRPDBm = form.indoorOutdoor.out.EIRP; + v.parameters.RLANOutdoorHeightType = form.indoorOutdoor.out.heightType; + v.parameters.RLANOutdoorHeight = form.indoorOutdoor.out.height; + v.parameters.RLANOutdoorHeightUncertainty = form.indoorOutdoor.out.heightUncertainty; + } + + return v; +} diff --git a/src/web/src/app/HeatMap/HeatMapForm.tsx b/src/web/src/app/HeatMap/HeatMapForm.tsx new file mode 100644 index 0000000..68e8e5c --- /dev/null +++ b/src/web/src/app/HeatMap/HeatMapForm.tsx @@ -0,0 +1,1025 @@ +import * as React from 'react'; +import { + FormGroup, + TextInput, + FormSelectOption, + FormSelect, + ActionGroup, + Button, + Alert, + AlertActionCloseButton, + InputGroupText, + InputGroup, + Gallery, + GalleryItem, + Tooltip, + Chip, + ChipGroup, + TooltipPosition, + Radio, +} from '@patternfly/react-core'; +import { + HeightType, + IndoorOutdoorType, + HeatMapRequest, + RatResponse, + HeatMapAnalysisType, + HeatMapFsIdType, +} from '../Lib/RatApiTypes'; +import { getCacheItem, cacheItem } from '../Lib/RatApi'; +import { logger } from '../Lib/Logger'; +import { Timer } from '../Components/Timer'; +import { letin } from '../Lib/Utils'; +import { Limit } from '../Lib/Admin'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; +import { CertificationId, InquiredChannel, OperatingClass, OperatingClassIncludeType } from '../Lib/RatAfcTypes'; +import { OperatingClassForm } from '../Components/OperatingClassForm'; + +/** + * HeatMapForm.tsx: form for capturing paramaters for heat map and validating them + * author: Sam Smucny + */ + +/** + * Interface definition of form data + */ +export interface HeatMapFormData { + serialNumber: string; + certificationId: CertificationId[]; + inquiredChannel: InquiredChannel; + analysis: HeatMapAnalysisType; + fsIdType: HeatMapFsIdType; + fsId?: number; + indoorOutdoor: + | { + kind: IndoorOutdoorType.INDOOR | IndoorOutdoorType.OUTDOOR | IndoorOutdoorType.ANY; + EIRP: number; + height: number; + heightType: HeightType; + heightUncertainty: number; + } + | { + kind: IndoorOutdoorType.BUILDING; + in: { + EIRP: number; + height: number; + heightType: HeightType; + heightUncertainty: number; + }; + out: { + EIRP: number; + height: number; + heightType: HeightType; + heightUncertainty: number; + }; + }; +} + +/** + * Form component for filling in heat map parameters + */ +export class HeatMapForm extends React.Component< + { + limit: Limit; + rulesetIds: string[]; + onSubmit: (a: HeatMapFormData) => Promise>; + onCopyPaste?: (formData: HeatMapFormData, updateCallback: (v: HeatMapFormData) => void) => void; + }, + { + height: { kind: 's'; val?: number } | { kind: 'b'; in?: number; out?: number }; + heightType: { kind: 's'; val?: HeightType } | { kind: 'b'; in?: HeightType; out?: HeightType }; + heightCert: { kind: 's'; val?: number } | { kind: 'b'; in?: number; out?: number }; + insideOutside: IndoorOutdoorType; + eirp: { kind: 's'; val?: number } | { kind: 'b'; in?: number; out?: number }; + mesgType?: 'info' | 'danger'; + mesgTitle?: string; + mesgBody?: string; + serialNumber?: string; + certificationId: CertificationId[]; + newCertificationId?: string; + newCertificationRulesetId?: string; + rulesetIds: string[]; + operatingClasses: OperatingClass[]; + inquiredChannel?: InquiredChannel; + analysisType: HeatMapAnalysisType; + fsIdType: HeatMapFsIdType; + fsId?: number; + } +> { + private static heightTypes: string[] = [HeightType.AGL.toString(), HeightType.AMSL.toString()]; + private static bandwidths: number[] = [20, 40, 80, 160]; + private static startingFreq = 5945; + private static centerFrequencies: Map = new Map([ + [ + 20, + Array(59) + .fill(0) + .map((_, i) => i * 20 + HeatMapForm.startingFreq + 10), + ], + [ + 40, + Array(29) + .fill(0) + .map((_, i) => i * 40 + HeatMapForm.startingFreq + 20), + ], + [ + 80, + Array(14) + .fill(0) + .map((_, i) => i * 80 + HeatMapForm.startingFreq + 40), + ], + [ + 160, + Array(7) + .fill(0) + .map((_, i) => i * 160 + HeatMapForm.startingFreq + 80), + ], + ]); + + constructor( + props: Readonly<{ + limit: Limit; + onSubmit: (a: HeatMapFormData) => Promise>; + rulesetIds: string[]; + }>, + ) { + super(props); + this.state = { + height: { kind: 's', val: undefined }, + heightType: { kind: 's', val: HeightType.AMSL }, + heightCert: { kind: 's', val: undefined }, + insideOutside: IndoorOutdoorType.INDOOR, + eirp: { kind: 's', val: undefined }, + mesgType: undefined, + serialNumber: 'HeatMapSerialNumber', + certificationId: [{ id: 'HeatMapCertificationId', rulesetId: 'US_47_CFR_PART_15_SUBPART_E' }], + rulesetIds: this.props.rulesetIds, + operatingClasses: [ + { + num: 131, + include: OperatingClassIncludeType.Some, + }, + { + num: 132, + include: OperatingClassIncludeType.Some, + }, + { + num: 133, + include: OperatingClassIncludeType.Some, + }, + { + num: 134, + include: OperatingClassIncludeType.Some, + }, + { + num: 136, + include: OperatingClassIncludeType.Some, + }, + { + num: 137, + include: OperatingClassIncludeType.Some, + }, + ], + analysisType: HeatMapAnalysisType.ItoN, + fsIdType: HeatMapFsIdType.All, + }; + } + + componentDidMount() { + const st = getCacheItem('heatMapForm'); + if (st !== undefined) this.setState(st); + } + + componentWillUnmount() { + const state: any = this.state; + state.mesgType = undefined; + cacheItem('heatMapForm', state); + } + + private validHeight = (s: { kind: 's'; val?: number } | { kind: 'b'; in?: number; out?: number }) => + this.state.insideOutside === IndoorOutdoorType.BUILDING + ? s.kind === 'b' && [Number.isFinite(s.in!), Number.isFinite(s.out!)] + : s.kind === 's' && [Number.isFinite(s.val!)]; + private validHeightType = (s: { kind: 's'; val?: HeightType } | { kind: 'b'; in?: HeightType; out?: HeightType }) => + this.state.insideOutside === IndoorOutdoorType.BUILDING + ? s.kind === 'b' && [!!s.in, !!s.out] + : s.kind === 's' && [!!s.val]; + private validHeightCert = (s: { kind: 's'; val?: number } | { kind: 'b'; in?: number; out?: number }) => { + if (this.state.insideOutside === IndoorOutdoorType.BUILDING) { + return s.kind === 'b' && [Number.isFinite(s.in!) && s.in! >= 0, Number.isFinite(s.out!) && s.out! >= 0]; + } else { + return s.kind === 's' && Number.isFinite(s.val!) && [s.val! >= 0]; + } + }; + private validInOut = (s?: string) => !!s; + private validEirp = (s: { kind: 's'; val?: number } | { kind: 'b'; in?: number; out?: number }) => { + if (this.state.analysisType === HeatMapAnalysisType.ItoN) { + if (this.state.insideOutside === IndoorOutdoorType.BUILDING) { + if (this.props.limit.indoorEnforce) { + return ( + s.kind === 'b' && [ + Number.isFinite(s.in!) && s.in! >= this.props.limit.indoorLimit, + Number.isFinite(s.out!) && s.out! >= this.props.limit.indoorLimit, + ] + ); + } else { + return s.kind === 'b' && [Number.isFinite(s.in!), Number.isFinite(s.out!)]; + } + } else if (this.state.insideOutside === IndoorOutdoorType.INDOOR) { + if (this.props.limit.indoorEnforce) { + return s.kind === 's' && Number.isFinite(s.val!) && [s.val! >= this.props.limit.indoorLimit]; + } else { + return s.kind === 's' && [Number.isFinite(s.val!)]; + } + } else if (this.state.insideOutside === IndoorOutdoorType.OUTDOOR) { + if (this.props.limit.outdoorEnforce) { + return s.kind === 's' && Number.isFinite(s.val!) && [s.val! >= this.props.limit.outdoorLimit]; + } else { + return s.kind === 's' && [Number.isFinite(s.val!)]; + } + } else { + return true; + } + } else { + return true; + } + }; + private submit = () => { + const allValid = [true] + .concat( + this.validHeight(this.state.height), + this.validHeightType(this.state.heightType), + this.validHeightCert(this.state.heightCert), + [this.validInOut(this.state.insideOutside)], + this.validEirp(this.state.eirp), + [!!this.state.inquiredChannel], + [!!this.state.serialNumber], + [!!this.state.certificationId && this.state.certificationId.length > 0], + [ + this.state.fsIdType === HeatMapFsIdType.All || + (this.state.fsIdType === HeatMapFsIdType.Single && !!this.state.fsId), + ], + ) + .reduce((p, c) => p && c); + + if (!allValid) { + logger.error('Invalid inputs when submitting HeatMapForm'); + this.setState({ mesgType: 'danger', mesgTitle: 'Invalid Inputs', mesgBody: 'One or more inputs are invalid' }); + return; + } + + logger.info('running heat map with form state: ', this.state); + const inout: any = + this.state.insideOutside === IndoorOutdoorType.BUILDING + ? { + kind: this.state.insideOutside, + in: { + // @ts-ignore + EIRP: this.state.eirp.in, + // @ts-ignore + height: this.state.height.in, + // @ts-ignore + heightType: this.state.heightType.in, + // @ts-ignore + heightUncertainty: this.state.heightCert.in, + }, + out: { + // @ts-ignore + EIRP: this.state.eirp.out, + // @ts-ignore + height: this.state.height.out, + // @ts-ignore + heightType: this.state.heightType.out, + // @ts-ignore + heightUncertainty: this.state.heightCert.out, + }, + } + : { + kind: this.state.insideOutside, + // @ts-ignore + EIRP: this.state.eirp.val, + // @ts-ignore + height: this.state.height.val, + // @ts-ignore + heightType: this.state.heightType.val, + // @ts-ignore + heightUncertainty: this.state.heightCert.val, + }; + this.setState({ mesgType: 'info' }); + this.props + .onSubmit({ + inquiredChannel: this.state.inquiredChannel, + indoorOutdoor: inout, + certificationId: this.state.certificationId, + serialNumber: this.state.serialNumber, + analysis: this.state.analysisType, + fsIdType: this.state.fsIdType, + fsId: this.state.fsId, + } as HeatMapFormData as any) + .then((res) => { + this.setState({ mesgType: undefined }); + }); + }; + + private defaultInOut(s: string) { + // @ts-ignore + const inOut: IndoorOutdoorType = s; + if (inOut === IndoorOutdoorType.BUILDING) { + return { + insideOutside: inOut, + height: { kind: 'b', in: 0, out: 0 }, + heightType: { kind: 'b', in: HeightType.AMSL, out: HeightType.AMSL }, + heightCert: { kind: 'b', in: 0, out: 0 }, + eirp: { kind: 'b', in: 0, out: 0 }, + }; + } else { + return { + insideOutside: inOut, + height: { kind: 's', val: 0 }, + heightType: { kind: 's', val: HeightType.AMSL }, + heightCert: { kind: 's', val: 0 }, + eirp: { kind: 's', val: 0 }, + }; + } + } + + private copyPasteClick = () => + this.props.onCopyPaste && + this.props.onCopyPaste( + letin( + this.state.insideOutside === IndoorOutdoorType.BUILDING + ? { + kind: this.state.insideOutside, + in: { + // @ts-ignore + EIRP: this.state.eirp.in, + // @ts-ignore + height: this.state.height.in, + // @ts-ignore + heightType: this.state.heightType.in, + // @ts-ignore + heightUncertainty: this.state.heightCert.in, + }, + out: { + // @ts-ignore + EIRP: this.state.eirp.out, + // @ts-ignore + height: this.state.height.out, + // @ts-ignore + heightType: this.state.heightType.out, + // @ts-ignore + heightUncertainty: this.state.heightCert.out, + }, + } + : { + kind: this.state.insideOutside, + // @ts-ignore + EIRP: this.state.eirp.val, + // @ts-ignore + height: this.state.height.val, + // @ts-ignore + heightType: this.state.heightType.val, + // @ts-ignore + heightUncertainty: this.state.heightCert.val, + }, + (inout) => ({ + indoorOutdoor: inout, + analysis: this.state.analysisType, + certificationId: this.state.certificationId, + fsIdType: this.state.fsIdType, + fsId: this.state.fsId, + inquiredChannel: this.state.inquiredChannel, + serialNumber: this.state.serialNumber, + }), + ) as HeatMapFormData, + this.setParams, + ); + + private setParams = (v: HeatMapFormData) => { + let newState = { + analysisType: v.analysis, + certificationId: v.certificationId, + fsIdType: v.fsIdType, + inquiredChannel: v.inquiredChannel, + serialNumber: v.serialNumber, + }; + if (v.fsIdType === HeatMapFsIdType.Single) { + newState['fsId'] = v.fsId; + } + this.setState(newState); + this.updateOperatingClass( + { + num: v.inquiredChannel.globalOperatingClass, + include: OperatingClassIncludeType.Some, + channels: [v.inquiredChannel.channelCfi], + }, + 0, + ); + if (v.indoorOutdoor.kind === IndoorOutdoorType.BUILDING) { + this.setState({ + insideOutside: v.indoorOutdoor.kind, + height: { kind: 'b', in: v.indoorOutdoor.in.height, out: v.indoorOutdoor.out.height }, + heightType: { kind: 'b', in: v.indoorOutdoor.in.heightType, out: v.indoorOutdoor.out.heightType }, + heightCert: { kind: 'b', in: v.indoorOutdoor.in.heightUncertainty, out: v.indoorOutdoor.out.heightUncertainty }, + eirp: { kind: 'b', in: v.indoorOutdoor.in.EIRP, out: v.indoorOutdoor.out.EIRP }, + }); + } else { + this.setState({ + insideOutside: v.indoorOutdoor.kind, + height: { kind: 's', val: v.indoorOutdoor.height }, + heightType: { kind: 's', val: v.indoorOutdoor.heightType }, + heightCert: { kind: 's', val: v.indoorOutdoor.heightUncertainty }, + eirp: { kind: 's', val: v.indoorOutdoor.EIRP }, + }); + } + }; + + deleteCertificationId(currentCid: string): void { + const copyOfcertificationId = this.state.certificationId.filter((x) => x.id != currentCid); + this.setState({ certificationId: copyOfcertificationId }); + } + addCertificationId(newCertificationId: CertificationId): void { + const copyOfcertificationId = this.state.certificationId.slice(); + copyOfcertificationId.push({ id: newCertificationId.id, rulesetId: newCertificationId.rulesetId }); + this.setState({ + certificationId: copyOfcertificationId, + newCertificationId: '', + newCertificationRulesetId: this.props.rulesetIds[0], + }); + } + + resetCertificationId(newCertificationId: CertificationId): void { + const copyOfcertificationId = [{ id: newCertificationId.id, rulesetId: newCertificationId.rulesetId }]; + this.setState({ + certificationId: copyOfcertificationId, + newCertificationId: '', + newCertificationRulesetId: this.props.rulesetIds[0], + }); + } + + updateOperatingClass( + x: { num: number; include: OperatingClassIncludeType; channels?: number[] | undefined }, + i: number, + ): void { + let ocs = this.state.operatingClasses.map((oc) => { + if (oc.num == x.num) { + return { num: x.num, include: OperatingClassIncludeType.Some, channels: x.channels }; + } else { + return { num: oc.num, include: OperatingClassIncludeType.Some, channels: [] }; + } + }); + if (!!x.channels && x.channels.length >= 1) { + this.setState({ + inquiredChannel: { globalOperatingClass: x.num, channelCfi: x.channels[0] }, + operatingClasses: ocs, + }); + } else { + this.setState({ inquiredChannel: undefined, operatingClasses: ocs }); + } + } + + render() { + return ( + <> + + + + {' '} + +

    The following Serial Number and Certification ID pair can be used for any rulesetID:

    +
      +
    • Serial Number=HeatMapSerialNumber
    • + +
    • CertificationId=HeatMapCertificationId
    • +
    + + } + > + +
    + this.setState({ serialNumber: x })} + isValid={!!this.state.serialNumber} + style={{ textAlign: 'right' }} + /> +
    +
    + + } //This is not supported in our version of Patternfly + validated={this.state.certificationId.length > 0 ? 'success' : 'error'} + > + {' '} + +

    The following Serial Number and Certification ID pair can be used for any rulesetID:

    +
      +
    • Serial Number=HeatMapSerialNumber
    • + +
    • CertificationId=HeatMapCertificationId
    • +
    + + } + > + +
    + + {this.state.certificationId.map((currentCid) => ( + this.deleteCertificationId(currentCid.id)}> + {currentCid.rulesetId + ' ' + currentCid.id} + + ))} + +
    + {' '} + +
    +
    + this.setState({ newCertificationRulesetId: x })} + type="text" + step="any" + id="horizontal-form-certification-nra" + name="horizontal-form-certification-nra" + style={{ textAlign: 'left' }} + placeholder="Ruleset" + > + {this.props.rulesetIds.map((x) => ( + + ))} + + + + this.setState({ newCertificationId: x })} + type="text" + step="any" + id="horizontal-form-certification-list" + name="horizontal-form-certification-list" + style={{ textAlign: 'left' }} + placeholder="Id" + /> +
    +
    +
    + + + + this.setState(this.defaultInOut(x))} + id="horzontal-form-indoor-outdoor" + name="horizontal-form-indoor-outdoor" + isValid={this.validInOut(this.state.insideOutside)} + style={{ textAlign: 'right' }} + > + + + + + + + + + + {this.state.height.kind === 's' ? ( + + + this.setState({ height: { kind: 's', val: Number(x) } })} + type="number" + step="any" + id="horizontal-form-height" + name="horizontal-form-height" + isValid={this.validHeight(this.state.height)[0]} + style={{ textAlign: 'right' }} + /> + meters + + + ) : ( + <> + + + + this.setState((st) => ({ height: { kind: 'b', out: st.height.out, in: Number(x) } })) + } + type="number" + step="any" + id="horizontal-form-height-in" + name="horizontal-form-height-in" + isValid={this.validHeight(this.state.height)[0]} + style={{ textAlign: 'right' }} + /> + meters + + + + + + this.setState((st) => ({ height: { kind: 'b', in: st.height.in, out: Number(x) } })) + } + type="number" + step="any" + id="horizontal-form-height-out" + name="horizontal-form-height-out" + isValid={this.validHeight(this.state.height)[1]} + style={{ textAlign: 'right' }} + /> + meters + + + + )} + + + {this.state.heightType.kind === 's' ? ( + + + this.setState({ heightType: { kind: 's', val: HeightType[x] } })} + id="horzontal-form-height-type" + name="horizontal-form-height-type" + isValid={this.validHeightType(this.state.heightType)[0]} + style={{ textAlign: 'right' }} + > + + {HeatMapForm.heightTypes.map((option) => ( + + ))} + + + + ) : ( + <> + + + + this.setState({ heightType: { kind: 'b', in: HeightType[x], out: this.state.heightType.out } }) + } + id="horzontal-form-height-type-in" + name="horizontal-form-height-type-in" + isValid={this.validHeightType(this.state.heightType)[0]} + style={{ textAlign: 'right' }} + > + + {HeatMapForm.heightTypes.map((option) => ( + + ))} + + + + + + + this.setState({ heightType: { kind: 'b', out: HeightType[x], in: this.state.heightType.in } }) + } + id="horzontal-form-height-type-out" + name="horizontal-form-height-type-out" + isValid={this.validHeightType(this.state.heightType)[1]} + style={{ textAlign: 'right' }} + > + + {HeatMapForm.heightTypes.map((option) => ( + + ))} + + + + + )} + + + {this.state.heightCert.kind === 's' ? ( + + + this.setState({ heightCert: { kind: 's', val: Number(x) } })} + type="number" + step="any" + id="horizontal-form-height-cert" + name="horizontal-form-height-cert" + isValid={this.validHeightCert(this.state.heightCert)[0]} + style={{ textAlign: 'right' }} + /> + meters + + + ) : ( + <> + + + + this.setState({ heightCert: { kind: 'b', in: Number(x), out: this.state.heightCert.out } }) + } + type="number" + step="any" + id="horizontal-form-height-cert-in" + name="horizontal-form-height-cert-in" + isValid={this.validHeightCert(this.state.heightCert)[0]} + style={{ textAlign: 'right' }} + /> + meters + + + + + + this.setState({ heightCert: { kind: 'b', out: Number(x), in: this.state.heightCert.in } }) + } + type="number" + step="any" + id="horizontal-form-height-cert-out" + name="horizontal-form-height-cert-out" + isValid={this.validHeightCert(this.state.heightCert)[1]} + style={{ textAlign: 'right' }} + /> + meters + + {' '} + + )} + + {this.state.analysisType === HeatMapAnalysisType.ItoN && ( + + {this.state.eirp.kind === 's' ? ( + + + this.setState({ eirp: { kind: 's', val: Number(x) } })} + type="number" + step="any" + id="horizontal-form-eirp" + name="horizontal-form-eirp" + isValid={this.validEirp(this.state.eirp)[0]} + style={{ textAlign: 'right' }} + /> + dBm + + + ) : ( + <> + + + + this.setState({ eirp: { kind: 'b', in: Number(x), out: this.state.eirp.out } }) + } + type="number" + step="any" + id="horizontal-form-eirp-in" + name="horizontal-form-eirp-in" + isValid={this.validEirp(this.state.eirp)[0]} + style={{ textAlign: 'right' }} + /> + dBm + + + + + this.setState({ eirp: { kind: 'b', out: Number(x), in: this.state.eirp.in } })} + type="number" + step="any" + id="horizontal-form-eirp-out" + name="horizontal-form-eirp-out" + isValid={this.validEirp(this.state.eirp)[1]} + style={{ textAlign: 'right' }} + /> + dBm + + + + )} + + )} + + + + this.setState({ analysisType: x })} + id="horizontal-form-analysis-type" + name="horizontal-form-analysis-type" + style={{ textAlign: 'right' }} + > + + + + + + + + + { + if (isChecked) { + this.setState({ fsIdType: HeatMapFsIdType.All }); + } + }} + /> +
    + { + if (isChecked) { + this.setState({ fsIdType: HeatMapFsIdType.Single }); + } + }} + /> +
    + {this.state.fsIdType === HeatMapFsIdType.Single ? ( + + !isNaN(+x) ? this.setState({ fsId: Number(x) }) : this.setState({ fsId: undefined }) + } + type="number" + id="horizontal-form-fs-id-single" + name="horizontal-form-fs-id-single" + style={{ textAlign: 'left' }} + placeholder="FS Id" + /> + ) : ( + <> + )} +
    +
    +
    + + + {this.state.operatingClasses.map((e, i) => ( + this.updateOperatingClass(x, i)} + allowOnlyOneChannel={true} + > + ))} + + +
    + + {this.state.mesgType && this.state.mesgType !== 'info' && ( + this.setState({ mesgType: undefined })} />} + > + {this.state.mesgBody} + + )} + +
    + <> + {' '} + + + + ); + } +} diff --git a/src/web/src/app/HeatMap/RegionSize.tsx b/src/web/src/app/HeatMap/RegionSize.tsx new file mode 100644 index 0000000..f16afe1 --- /dev/null +++ b/src/web/src/app/HeatMap/RegionSize.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { Bounds } from '../Lib/RatApiTypes'; + +/** + * RegionSize.tsx: displays size of heat map region in simulation + * author: Sam Smucny + */ + +/** + * `RegionSize` properties + */ +interface RegionSizeProps { + bounds?: Bounds; +} + +const earthRadius = 6.378137e6; +const minAbsLat = (latA: number, latB: number) => Math.min(Math.abs(latA), Math.abs(latB)); +const getLatDistance = (latA: number, latB: number) => ((Math.abs(latA - latB) * Math.PI) / 180) * earthRadius; +const getLonDistance = (bounds: Bounds) => + ((Math.abs(bounds.east - bounds.west) * Math.PI) / 180) * + earthRadius * + Math.cos((minAbsLat(bounds.north, bounds.south) * Math.PI) / 180); + +/** + * Component that displays the height and width of given bounds in latitude and longitude + * @param props `RegionSizeProps` + */ +export const RegionSize: React.FunctionComponent = (props: RegionSizeProps) => ( +

    + Selected Region Size (m):{' '} + {props.bounds + ? getLatDistance(props.bounds.north, props.bounds.south).toFixed(0) + + ' x ' + + getLonDistance(props.bounds).toFixed(0) + : 'No heat map region defined'} +

    +); diff --git a/src/web/src/app/ImportExport/ImportExport.tsx b/src/web/src/app/ImportExport/ImportExport.tsx new file mode 100644 index 0000000..891bbce --- /dev/null +++ b/src/web/src/app/ImportExport/ImportExport.tsx @@ -0,0 +1,129 @@ +import * as React from 'react'; +import { AFCConfigFile, RatResponse } from '../Lib/RatApiTypes'; +import { + CardBody, + PageSection, + Card, + CardHead, + TextInput, + Alert, + AlertActionCloseButton, +} from '@patternfly/react-core'; +import DownloadContents from '../Components/DownloadContents'; +import { exportCache, putAfcConfigFile, importCache, guiConfig } from '../Lib/RatApi'; +import { logger } from '../Lib/Logger'; + +export class ImportExport extends React.Component< + { afcConfig: RatResponse }, + { message?: string; messageType?: 'danger' | 'success' } +> { + constructor(props: Readonly<{ afcConfig: RatResponse }>) { + super(props); + + if (props.afcConfig.kind === 'Success') { + this.state = {}; + } else { + logger.error('AFC Config was not loaded', props.afcConfig); + this.state = { messageType: 'danger', message: 'AFC Config was not loaded' }; + } + } + + private export = () => + new Blob( + [ + JSON.stringify( + Object.assign( + exportCache(), + this.props.afcConfig.kind === 'Success' ? { afcConfig: this.props.afcConfig.result } : {}, + ), + ), + ], + { + type: 'application/json', + }, + ); + + private import = async (name: string, ev: React.FormEvent) => { + // @ts-ignore + const file = ev.target.files[0]; + const reader = new FileReader(); + try { + reader.onload = async () => { + try { + const value: any = JSON.parse(reader.result as string); + if (value.afcConfig) { + if (value.afcConfig.version !== guiConfig.version) { + const warning: string = + "The imported file is from a different version. It has version '" + + value.afcConfig.version + + "', and you are currently running '" + + guiConfig.version + + "'."; + logger.warn(warning); + } + const putResp = await putAfcConfigFile(value.afcConfig); + if (putResp.kind === 'Error') { + this.setState({ messageType: 'danger', message: putResp.description }); + return; + } + } + + value.afcConfig = undefined; + + importCache(value); + + this.setState({ messageType: 'success', message: 'Import successful!' }); + } catch (e) { + this.setState({ messageType: 'danger', message: 'Unable to import file' }); + } + }; + + reader.readAsText(file); + } catch (e) { + logger.error('Failed to import application state', e); + this.setState({ messageType: 'danger', message: 'Failed to import application state' }); + } + }; + + render() { + return ( + + + Import + +
    + this.import(f, ev)} + /> + {this.state.messageType && ( + <> +
    +
    + this.setState({ messageType: undefined })} />} + > +
    {this.state.message}
    +
    + + )} +
    +
    +
    + + Export + + this.export()} /> +
    +
    +
    +
    + ); + } +} diff --git a/src/web/src/app/Lib/Admin.ts b/src/web/src/app/Lib/Admin.ts new file mode 100644 index 0000000..41b04e9 --- /dev/null +++ b/src/web/src/app/Lib/Admin.ts @@ -0,0 +1,593 @@ +import { guiConfig, getCSRF } from './RatApi'; +import { + UserModel, + success, + error, + AccessPointModel, + AccessPointListModel, + FreqRange, + DeniedRegion, + ExclusionCircle, + ExclusionTwoRect, + ExclusionRect, + ExclusionHorizon, + MTLSModel, +} from './RatApiTypes'; +import { logger } from './Logger'; +import { Role, retrieveUserData } from './User'; +import { Rect } from 'react-konva'; +import { Circle } from 'konva/types/shapes/Circle'; +import { RatResponse } from './RatApiTypes'; + +/** + * Admin.ts: Functions for Admin API. User and account management, and permissions + * author: Sam Smucny + */ + +export class Limit { + indoorEnforce: boolean; + outdoorEnforce: boolean; + indoorLimit: number; + outdoorLimit: number; + constructor(enforceIndoor: boolean, enforceOutdoor: boolean, indoorLimit: number, outdoorLimit: number) { + this.indoorEnforce = enforceIndoor; + this.outdoorEnforce = enforceOutdoor; + this.indoorLimit = indoorLimit; + this.outdoorLimit = outdoorLimit; + } +} + +/** + * Gets the current Minimum EIRP value + * @returns object indicating minimum value and whether or not it's enforced if successful, error otherwise + */ +export const getMinimumEIRP = () => + fetch(guiConfig.admin_url.replace('-1', 'eirp_min'), { + method: 'GET', + }) + .then(async (res) => { + if (res.ok) { + return success((await res.json()) as Limit); + } else { + return error('Unable to load limits', res.status, res); + } + }) + .catch((e) => { + logger.error(e); + return error('Request failed'); + }); + +/** + * Sets the Minimum EIRP value. + * @param limit the new EIRP value + */ +export const setMinimumEIRP = async (limit: Limit) => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.admin_url.replace('-1', 'eirp_min'), { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf_token }, + body: JSON.stringify(limit), + }) + .then(async (res) => { + if (res.ok) { + return success((await res.json()).limit as Limit); + } else { + return error(res.statusText, res.status, res); + } + }) + .catch((err) => error('An error was encountered #1', undefined, err)); +}; + +/** + * Return list of all users. Must be Admin + * There is current no support for queried searches/filters/etc. Just returns all. + * @returns list of users if successful, error otherwise + */ +export const getUsers = () => + fetch(guiConfig.admin_url.replace('-1', '0'), { + method: 'GET', + }) + .then(async (res) => { + if (res.ok) { + return success((await res.json()).users as UserModel[]); + } else { + return error('Unable to load users', res.status, res); + } + }) + .catch((e) => { + logger.error(e); + return error('Request failed'); + }); + +/** + * gets a single user by id + * @param id User Id + * @return The user if found, error otherwise + */ +export const getUser = (id: number) => + fetch(guiConfig.admin_url.replace('-1', String(id)), { + method: 'GET', + }) + .then(async (res) => + res.ok ? success((await res.json()).user as UserModel) : error('Unable to load user', res.status, res), + ) + .catch((e) => { + logger.error(e); + return error('Request failed'); + }); + +/** + * Update a user's data + * @param user User to replace with + */ +export const updateUser = async (user: { email: string; password: string; id: number; active: boolean }) => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.admin_url.replace('-1', String(user.id)), { + method: 'POST', + body: JSON.stringify(Object.assign(user, { setProps: true })), + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf_token }, + }).then((res) => (res.ok ? success(res.statusText) : error(res.statusText, res.status, res))); +}; + +/** + * Give a user a role + * @param id user's Id + * @param role role to add + */ +export const addUserRole = async (id: number, role: Role) => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.admin_url.replace('-1', String(id)), { + method: 'POST', + body: JSON.stringify({ addRole: role }), + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf_token }, + }) + .then((res) => { + if (res.ok) { + return success(res.status); + } else { + return error(res.statusText, res.status, res); + } + }) + .catch((err) => error('An error was encountered #2', undefined, err)); +}; + +/** + * Remove a role from a user + * @param id user's Id + * @param role role to remove + */ +export const removeUserRole = async (id: number, role: Role) => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.admin_url.replace('-1', id.toString()), { + method: 'POST', + body: JSON.stringify({ removeRole: role }), + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf_token }, + }) + .then((res) => { + if (res.ok) { + return success(res.status); + } else { + return error(res.statusText, res.status, res); + } + }) + .catch((err) => error('An error was encountered #3', undefined, err)); +}; + +/** + * Delete a user from the system + * @param id user'd Id + */ +export const deleteUser = async (id: number) => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.admin_url.replace('-1', String(id)), { + method: 'DELETE', + headers: { 'X-CSRF-Token': csrf_token }, + }) + .then((res) => { + if (res.ok) { + return success(res.status); + } else { + return error(res.statusText, res.status, res); + } + }) + .catch((err) => error('An error was encountered #4', undefined, err)); +}; + +/** + * Get access points. If `userId` is provided then only return access + * points owned by the user. If no `userId` is provided then return all + * access points (must be `Admin`). + * @param userId (optional) user's Id + * @returns list of access points if successful, error otherwise + */ +export const getAccessPointsDeny = (userId?: number) => + fetch(guiConfig.ap_deny_admin_url.replace('-1', String(userId || 0)), { + method: 'GET', + }) + .then(async (res) => { + if (res.ok) { + return success(await res.text()); + } else { + return error('Unable to load access points', res.status, res); + } + }) + .catch((err) => error('An error was encountered #8', undefined, err)); + +/** + * Register an access point with a user. + * @param ap Access point to add + * @param userId owner of new access point + */ +export const addAccessPointDeny = async (ap: AccessPointModel, userId: number) => { + let csrf_token = await getCSRF(); + + return fetch(guiConfig.ap_deny_admin_url.replace('-1', String(userId)), { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf_token }, + body: JSON.stringify(ap), + }) + .then(async (res) => { + if (res.ok) { + return success((await res.json()).id as number); + } else if (res.status === 400) { + return error('Invalid AP data', res.status, res); + } else { + return error(res.statusText, res.status, res); + } + }) + .catch((err) => error('An error was encountered #9', undefined, err)); +}; + +/** + * Post a new deny access point file + * @param ap Access point to add + * @param userId owner of new access point + */ +export const putAccessPointDenyList = async (ap: AccessPointListModel, userId: number) => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.ap_deny_admin_url.replace('-1', String(userId)), { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf_token }, + body: JSON.stringify(ap), + }) + .then((res) => { + if (res.ok) { + return success(res.status); + } else if (res.status === 400) { + return error('Invalid AP data', res.status, res); + } else { + return error(res.statusText, res.status, res); + } + }) + .catch((err) => error('An error was encountered #10', undefined, err)); +}; + +/** + * Register an mtls certificate + * @param mtls cert to add + * @param userId who creates the new mtls cert + */ +export const addMTLS = async (mtls: MTLSModel, userId: number) => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.mtls_admin_url.replace('-1', String(userId)), { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf_token }, + body: JSON.stringify(mtls), + }) + .then(async (res) => { + if (res.ok) { + return success((await res.json()).id as number); + } else if (res.status === 400) { + return error('Unable to add new certificate', res.status, res); + } else { + return error(res.statusText, res.status, res); + } + }) + .catch((err) => error('An error was encountered #11', undefined, err)); +}; + +/** + * Delete an mtls cert from the system. + * @param id mtls cert id + */ +export const deleteMTLSCert = async (id: number) => { + // here the id in the url is the mtls id, not the user id + let csrf_token = await getCSRF(); + return fetch(guiConfig.mtls_admin_url.replace('-1', String(id)), { + method: 'DELETE', + headers: { 'X-CSRF-Token': csrf_token }, + }) + .then((res) => { + if (res.ok) { + return success(undefined); + } else { + return error(res.statusText, res.status, res); + } + }) + .catch((err) => error('An error was encountered #12', undefined, err)); +}; + +/** + * Get mtls cert. If `userId` is 0, then return all certificates (super) + * or all certificates in the same org as the user (`Admin`). + * 'userId' non zero is not currently supported as certificate do not belong + * a single user. + * @param userId user's Id + * @returns list of mtls certs if successful, error otherwise + */ +export const getMTLS = (userId?: number) => + fetch(guiConfig.mtls_admin_url.replace('-1', String(userId || 0)), { + method: 'GET', + }) + .then(async (res) => { + if (res.ok) { + return success((await res.json()).mtls as MTLSModel[]); + } else { + return error('Unable to load mtls', res.status, res); + } + }) + .catch((err) => error('An error was encountered #13', undefined, err)); + +export const getDeniedRegions = (regionStr: string) => { + return fetch(guiConfig.dr_admin_url.replace('XX', regionStr), { + method: 'GET', + headers: { + 'content-type': 'text/csv', + }, + }) + .then(async (res) => { + if (res.ok) { + return success(mapDeniedRegionFromCsv(await res.text(), regionStr)); + } else if (res.status == 404) { + return success([]); + } else { + return error('Unable to get denied regions for ' + regionStr, res.status, res); + } + }) + .catch((err) => error('An error was encountered #14', undefined, err)); +}; + +export const getDeniedRegionsCsvFile = (regionStr: string) => { + return fetch(guiConfig.dr_admin_url.replace('XX', regionStr), { + method: 'GET', + headers: { + 'content-type': 'text/csv', + }, + }) + .then(async (res) => { + if (res.ok) { + return success(await res.text()); + } else { + return error('Unable to get denied regions for ' + regionStr, res.status, res); + } + }) + .catch((err) => error('An error was encountered #15', undefined, err)); +}; + +// Update the denied regions for a given region +export const updateDeniedRegions = async (records: DeniedRegion[], regionStr: string) => { + let body = mapDeniedRegionToCsv(records, regionStr, true); + let csrf_token = await getCSRF(); + + return fetch(guiConfig.dr_admin_url.replace('XX', regionStr), { + method: 'PUT', + headers: { 'Content-Type': 'text/csv', 'X-CSRF-Token': csrf_token }, + body: body, + }) + .then(async (res) => { + if (res.status === 204) { + return success('Denied regions updated.'); + } else { + return error(res.statusText, res.status); + } + }) + .catch((err) => { + logger.error(err); + return error('Unable to update denied regions.'); + }); +}; + +function parseCSV(str: string, headers = true) { + const arr: string[][] = []; + let quote = false; // 'true' means we're inside a quoted field + + // Iterate over each character, keep track of current row and column (of the returned array) + for (let row = 0, col = 0, c = 0; c < str.length; c++) { + let cc = str[c], + nc = str[c + 1]; // Current character, next character + arr[row] = arr[row] || []; // Create a new row if necessary + arr[row][col] = arr[row][col] || ''; // Create a new column (start with empty string) if necessary + + // If the current character is a quotation mark, and we're inside a + // quoted field, and the next character is also a quotation mark, + // add a quotation mark to the current column and skip the next character + if (cc == '"' && quote && nc == '"') { + arr[row][col] += cc; + ++c; + continue; + } + + // If it's just one quotation mark, begin/end quoted field + if (cc == '"') { + quote = !quote; + continue; + } + + // If it's a comma and we're not in a quoted field, move on to the next column + if (cc == ',' && !quote) { + ++col; + continue; + } + + // If it's a newline (CRLF) and we're not in a quoted field, skip the next character + // and move on to the next row and move to column 0 of that new row + if (cc == '\r' && nc == '\n' && !quote) { + ++row; + col = 0; + ++c; + continue; + } + + // If it's a newline (LF or CR) and we're not in a quoted field, + // move on to the next row and move to column 0 of that new row + if (cc == '\n' && !quote) { + ++row; + col = 0; + continue; + } + if (cc == '\r' && !quote) { + ++row; + col = 0; + continue; + } + + // Otherwise, append the current character to the current column + arr[row][col] += cc; + } + + if (headers) { + let headerRow = arr[0]; + } + + return arr; +} + +function parseCSVtoObjects(csvString: string) { + var csvRows = parseCSV(csvString); + + var columnNames = csvRows[0]; + var firstDataRow = 1; + + var result = []; + for (var i = firstDataRow, n = csvRows.length; i < n; i++) { + var rowObject: any = {}; + var row = csvRows[i]; + for (var j = 0, m = Math.min(row.length, columnNames.length); j < m; j++) { + var columnName = columnNames[j]; + var columnValue = row[j]; + rowObject[columnName] = columnValue; + } + result.push(rowObject); + } + return result; +} + +const mapDeniedRegionFromCsv = (data: string, regionStr: string) => { + let records = parseCSVtoObjects(data); + let objects = records.map((x) => { + let newRegion: DeniedRegion = { + regionStr: regionStr, + name: x['Location'], + endFreq: x['Stop Freq (MHz)'], + startFreq: x['Start Freq (MHz)'], + exclusionZone: dummyExclusionZone, + zoneType: 'Circle', + }; + + //Is a one or two rect if it has a rect1 lat 1 + if (!!x['Rectangle1 Lat 1']) { + //Is a two rect if has a rect 2 lat 1 + if (!!x['Rectangle2 Lat 1']) { + let rect: ExclusionTwoRect = { + rectangleOne: { + topLat: x['Rectangle1 Lat 1'], + leftLong: x['Rectangle1 Lon 1'], + bottomLat: x['Rectangle1 Lat 2'], + rightLong: x['Rectangle1 Lon 2'], + }, + rectangleTwo: { + topLat: x['Rectangle2 Lat 1'], + leftLong: x['Rectangle2 Lon 1'], + bottomLat: x['Rectangle2 Lat 2'], + rightLong: x['Rectangle2 Lon 2'], + }, + }; + newRegion.exclusionZone = rect; + newRegion.zoneType = 'Two Rectangles'; + } else { + let rect: ExclusionRect = { + topLat: x['Rectangle1 Lat 1'], + leftLong: x['Rectangle1 Lon 1'], + bottomLat: x['Rectangle1 Lat 2'], + rightLong: x['Rectangle1 Lon 2'], + }; + newRegion.exclusionZone = rect; + newRegion.zoneType = 'One Rectangle'; + } + } else if (!!x['Circle Radius (km)']) { + let circ: ExclusionCircle = { + latitude: x['Circle center Lat'], + longitude: x['Circle center Lon'], + radiusKm: x['Circle Radius (km)'], + }; + newRegion.exclusionZone = circ; + newRegion.zoneType = 'Circle'; + } else { + let horz: ExclusionHorizon = { + latitude: x['Circle center Lat'], + longitude: x['Circle center Lon'], + aglHeightM: x['Antenna AGL height (m)'], + }; + newRegion.exclusionZone = horz; + newRegion.zoneType = 'Horizon Distance'; + } + return newRegion; + }); + return objects; +}; + +const mapDeniedRegionToCsv = (records: DeniedRegion[], regionStr: string, includeHeader: boolean = true) => { + let result: string[] = []; + if (includeHeader) { + result.push(defaultDeniedRegionHeaders); + } + let strings = records + .filter((x) => x.regionStr == regionStr) + .map((rec) => { + let header = `${rec.name},${rec.startFreq},${rec.endFreq},${rec.zoneType},`; + let excl = ''; + switch (rec.zoneType) { + case 'Circle': + { + let x = rec.exclusionZone as ExclusionCircle; + excl = `,,,,,,,,${x.radiusKm},${x.latitude},${x.longitude},`; + } + break; + case 'One Rectangle': + { + let x = rec.exclusionZone as ExclusionRect; + excl = `${x.topLat},${x.bottomLat},${x.leftLong},${x.rightLong},,,,,,,,`; + } + break; + case 'Two Rectangles': + { + let x = rec.exclusionZone as ExclusionTwoRect; + excl = `${x.rectangleOne.topLat},${x.rectangleOne.bottomLat},${x.rectangleOne.leftLong},${x.rectangleOne.rightLong},${x.rectangleTwo.topLat},${x.rectangleTwo.bottomLat},${x.rectangleTwo.leftLong},${x.rectangleTwo.rightLong},,,,`; + } + break; + case 'Horizon Distance': + { + let x = rec.exclusionZone as ExclusionHorizon; + excl = `,,,,,,,,,${x.latitude},${x.longitude},${x.aglHeightM}`; + } + break; + default: + throw 'Bad data in mapDeniedRegionToCsv: ' + JSON.stringify(rec); + } + return header + excl; + }); + result = result.concat(strings); + return result.join('\n'); +}; + +const dummyExclusionZone: ExclusionCircle = { latitude: 0, longitude: 0, radiusKm: 0 }; +const defaultDeniedRegionHeaders = + 'Location,Start Freq (MHz),Stop Freq (MHz),Exclusion Zone,Rectangle1 Lat 1,Rectangle1 Lat 2,Rectangle1 Lon 1,Rectangle1 Lon 2,Rectangle2 Lat 1,Rectangle2 Lat 2,Rectangle2 Lon 1,Rectangle2 Lon 2,Circle Radius (km),Circle center Lat,Circle center Lon,Antenna AGL height (m)'; +export const BlankDeniedRegion: DeniedRegion = { + regionStr: 'US', + name: 'Placeholder', + endFreq: 5298, + startFreq: 5298, + exclusionZone: dummyExclusionZone, + zoneType: 'Circle', +}; diff --git a/src/web/src/app/Lib/FileApi.ts b/src/web/src/app/Lib/FileApi.ts new file mode 100644 index 0000000..af52e47 --- /dev/null +++ b/src/web/src/app/Lib/FileApi.ts @@ -0,0 +1,60 @@ +import { guiConfig } from './RatApi'; +import { RatResponse, success, error } from './RatApiTypes'; + +/** + * FileApi.ts: Application API to access Web Dav resources on server + * author: Sam Smucny + */ + +/** + * Gets all ULS files that have a specific file extension. + * @param ext file extension to filter on. + * @returns Promise with list of file or error + */ +const getFilesOfType = (url: string, ext: string): Promise> => + fetch(url, { + method: 'GET', + }) + .then(async (res: Response) => { + if (res.ok) { + const el = document.createElement('html'); + el.innerHTML = await res.text(); + const td = el.getElementsByTagName('td'); + const len = td.length; + let names = []; + for (let i = 0; i < len; i++) { + if (td[i].children.length > 0 && td[i].textContent.endsWith(ext)) { + names.push(td[i].textContent); + } + } + return success(names); + } else { + return error(res.statusText, res.status, res.body); + } + }) + .catch((err: any) => error(err.message, err.statusCode, err)); + +/** + * Get ULS files that can be used by the AFC Engine + * @returns ULS files of type `.sqlite3` or error + */ +export const getUlsFiles = (): Promise> => { + return getFilesOfType(guiConfig.uls_url, '.sqlite3'); +}; + +/** + * Get ULS files that cannot be used by the AFC Engine but can be converted to + * the compatible sqlite format. + * @returns ULS files of type `.csv` or error + */ +export const getUlsFilesCsv = (): Promise> => { + return getFilesOfType(guiConfig.uls_url, '.csv'); +}; + +/** + * Gets all antenna patterns which can be used by the AFC Engine + * @returns List of antenna pattern names or error + */ +export const getAntennaPatterns = (): Promise> => { + return getFilesOfType(guiConfig.antenna_url, '.csv'); +}; diff --git a/src/web/src/app/Lib/Logger.ts b/src/web/src/app/Lib/Logger.ts new file mode 100644 index 0000000..ad8d50e --- /dev/null +++ b/src/web/src/app/Lib/Logger.ts @@ -0,0 +1,43 @@ +/** + * Logger.ts: logging module for web app + * author: Sam Smucny + */ + +/** + * Global logger with configurable log levels in source code. + * At build time the if statement is compiled out leaving only one of the two branches. + * Usage is identical to console.{log|warn|error}(). + * + * ``` + * logger.info('info logging...', ...); + * logger.warn('warn logging...', ...); + * logger.error('error logging...', ...); + * ``` + */ +const logger = { + /** + * Log informative messages to console + * ``` + * logger.info('helpful information...'); + * ``` + */ + info: false ? console.log : () => {}, + + /** + * Log warning messages to console + * ``` + * logger.warn('This is a warning!'); + * ``` + */ + warn: false ? console.warn : () => {}, + + /** + * Low error messages to console + * ``` + * logger.error('ERROR!'); + * ``` + */ + error: true ? console.error : () => {}, +}; + +export { logger }; diff --git a/src/web/src/app/Lib/PawsApi.ts b/src/web/src/app/Lib/PawsApi.ts new file mode 100644 index 0000000..611ee3a --- /dev/null +++ b/src/web/src/app/Lib/PawsApi.ts @@ -0,0 +1,515 @@ +import { guiConfig } from './RatApi'; +import { PAWSRequest, PAWSResponse, RatResponse, success, error } from './RatApiTypes'; +import { logger } from './Logger'; + +/** + * PawsApi.ts: types and functions for utilizing server's paws api + * this file embeds a javascript json-rpc library + * author: Sam Smucny + */ + +/** + * global jsonrpc object + */ +var simple_jsonrpc: any = null; + +// @ts-ignore +(function (root) { + 'use strict'; + /* + name: simple-jsonrpc-js + version: 1.0.0 + source: https://github.com/jershell/simple-jsonrpc-js/blob/master/simple-jsonrpc-js.js + license: MIT + */ + var _Promise = Promise; + + if (typeof _Promise === 'undefined') { + _Promise = root.Promise; + } + + if (_Promise === undefined) { + throw 'Promise is not supported! Use latest version node/browser or promise-polyfill'; + } + + var isUndefined = function (value) { + return value === undefined; + }; + + var isArray = Array.isArray; + + var isObject = function (value) { + var type = typeof value; + return value != null && (type == 'object' || type == 'function'); + }; + + var isFunction = function (target) { + return typeof target === 'function'; + }; + + var isString = function (value) { + return typeof value === 'string'; + }; + + var isEmpty = function (value) { + if (isObject(value)) { + for (var idx in value) { + if (value.hasOwnProperty(idx)) { + return false; + } + } + return true; + } + if (isArray(value)) { + return !value.length; + } + return !value; + }; + + // @ts-ignore + var forEach = function (target, callback) { + if (isArray(target)) { + return target.map(callback); + } else { + for (var _key in target) { + if (target.hasOwnProperty(_key)) { + callback(target[_key]); + } + } + } + }; + + var clone = function (value) { + return JSON.parse(JSON.stringify(value)); + }; + + var ERRORS = { + PARSE_ERROR: { + code: -32700, + message: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.', + }, + INVALID_REQUEST: { + code: -32600, + message: 'Invalid Request. The JSON sent is not a valid Request object.', + }, + METHOD_NOT_FOUND: { + code: -32601, + message: 'Method not found. The method does not exist / is not available.', + }, + INVALID_PARAMS: { + code: -32602, + message: 'Invalid params. Invalid method parameter(s).', + }, + INTERNAL_ERROR: { + code: -32603, + message: 'Internal error. Internal JSON-RPC error.', + }, + }; + + // @ts-ignore + function ServerError(code, message, data) { + // @ts-ignore + this.message = message || ''; + // @ts-ignore + this.code = code || -32000; + + if (Boolean(data)) { + // @ts-ignore + this.data = data; + } + } + + ServerError.prototype = new Error(); + + simple_jsonrpc = function () { + // @ts-ignore + var self = this, + waitingframe = {}, + id = 0, + dispatcher = {}; + + function setError(jsonrpcError, exception) { + var error = clone(jsonrpcError); + if (!!exception) { + if (isObject(exception) && exception.hasOwnProperty('message')) { + error.data = exception.message; + } else if (isString(exception)) { + error.data = exception; + } + + if (exception instanceof ServerError) { + error = { + message: exception.message, + // @ts-ignore + code: exception.code, + }; + if (exception.hasOwnProperty('data')) { + // @ts-ignore + error.data = exception.data; + } + } + } + return error; + } + + function isPromise(thing: any) { + return !!thing && 'function' === typeof thing.then; + } + + function isError(message: any) { + return !!message.error; + } + + function isRequest(message: any) { + return !!message.method; + } + + function isResponse(message: any) { + return message.hasOwnProperty('result') && message.hasOwnProperty('id'); + } + + function beforeResolve(message: any) { + var promises = []; + if (isArray(message)) { + forEach(message, function (msg: any) { + // @ts-ignore + promises.push(resolver(msg)); + }); + } else if (isObject(message)) { + // @ts-ignore + promises.push(resolver(message)); + } + + return _Promise.all(promises).then(function (result) { + var toStream = []; + forEach(result, function (r: any) { + if (!isUndefined(r)) { + // @ts-ignore + toStream.push(r); + } + }); + + if (toStream.length === 1) { + self.toStream(JSON.stringify(toStream[0])); + } else if (toStream.length > 1) { + self.toStream(JSON.stringify(toStream)); + } + return result; + }); + } + + function resolver(message: any) { + try { + if (isError(message)) { + return rejectRequest(message); + } else if (isResponse(message)) { + return resolveRequest(message); + } else if (isRequest(message)) { + return handleRemoteRequest(message); + } else { + return _Promise.resolve({ + id: null, + jsonrpc: '2.0', + // @ts-ignore + error: setError(ERRORS.INVALID_REQUEST), + }); + } + } catch (e) { + logger.error('Resolver error:' + e.message, e); + return _Promise.reject(e); + } + } + + function rejectRequest(error: any) { + if (waitingframe.hasOwnProperty(error.id)) { + waitingframe[error.id].reject(error.error); + } else { + logger.error('Unknown request', error); + } + } + + function resolveRequest(result) { + if (waitingframe.hasOwnProperty(result.id)) { + waitingframe[result.id].resolve(result.result); + delete waitingframe[result.id]; + } else { + logger.error('unknown request', result); + } + } + + function handleRemoteRequest(request) { + if (dispatcher.hasOwnProperty(request.method)) { + try { + var result; + + if (request.hasOwnProperty('params')) { + if (dispatcher[request.method].params == 'pass') { + result = dispatcher[request.method].fn.call(dispatcher, request.params); + } else if (isArray(request.params)) { + result = dispatcher[request.method].fn.apply(dispatcher, request.params); + } else if (isObject(request.params)) { + if (dispatcher[request.method].params instanceof Array) { + var argsValues = []; + dispatcher[request.method].params.forEach(function (arg) { + if (request.params.hasOwnProperty(arg)) { + // @ts-ignore + argsValues.push(request.params[arg]); + delete request.params[arg]; + } else { + // @ts-ignore + argsValues.push(undefined); + } + }); + + if (Object.keys(request.params).length > 0) { + return _Promise.resolve({ + jsonrpc: '2.0', + id: request.id, + error: setError(ERRORS.INVALID_PARAMS, { + message: 'Params: ' + Object.keys(request.params).toString() + ' not used', + }), + }); + } else { + result = dispatcher[request.method].fn.apply(dispatcher, argsValues); + } + } else { + return _Promise.resolve({ + jsonrpc: '2.0', + id: request.id, + error: setError(ERRORS.INVALID_PARAMS, 'Undeclared arguments of the method ' + request.method), + }); + } + } + } else { + result = dispatcher[request.method].fn(); + } + + if (request.hasOwnProperty('id')) { + if (isPromise(result)) { + return result + .then(function (res) { + if (isUndefined(res)) { + res = true; + } + return { + jsonrpc: '2.0', + id: request.id, + result: res, + }; + }) + .catch(function (e) { + return { + jsonrpc: '2.0', + id: request.id, + error: setError(ERRORS.INTERNAL_ERROR, e), + }; + }); + } else { + if (isUndefined(result)) { + result = true; + } + + return _Promise.resolve({ + jsonrpc: '2.0', + id: request.id, + result: result, + }); + } + } else { + return _Promise.resolve(); //nothing, it notification + } + } catch (e) { + return _Promise.resolve({ + jsonrpc: '2.0', + id: request.id, + error: setError(ERRORS.INTERNAL_ERROR, e), + }); + } + } else { + return _Promise.resolve({ + jsonrpc: '2.0', + id: request.id, + error: setError(ERRORS.METHOD_NOT_FOUND, { + message: request.method, + }), + }); + } + } + + function notification(method, params) { + var message = { + jsonrpc: '2.0', + method: method, + params: params, + }; + + if (isObject(params) && !isEmpty(params)) { + message.params = params; + } + + return message; + } + + function call(method, params) { + id += 1; + var message = { + jsonrpc: '2.0', + method: method, + id: id, + }; + + if (isObject(params) && !isEmpty(params)) { + // @ts-ignore + message.params = params; + } + + return { + promise: new _Promise(function (resolve, reject) { + waitingframe[id.toString()] = { + resolve: resolve, + reject: reject, + }; + }), + message: message, + }; + } + + self.toStream = function (a: any) { + logger.error('Need define the toStream method before use', arguments); + }; + + self.dispatch = function (functionName, paramsNameFn, fn) { + if (isString(functionName) && paramsNameFn == 'pass' && isFunction(fn)) { + dispatcher[functionName] = { + fn: fn, + params: paramsNameFn, + }; + } else if (isString(functionName) && isArray(paramsNameFn) && isFunction(fn)) { + dispatcher[functionName] = { + fn: fn, + params: paramsNameFn, + }; + } else if (isString(functionName) && isFunction(paramsNameFn) && isUndefined(fn)) { + dispatcher[functionName] = { + fn: paramsNameFn, + params: null, + }; + } else { + throw new Error('Missing required argument: functionName - string, paramsNameFn - string or function'); + } + }; + + self.on = self.dispatch; + + self.off = function (functionName) { + delete dispatcher[functionName]; + }; + + self.call = function (method, params) { + var _call = call(method, params); + self.toStream(JSON.stringify(_call.message)); + return _call.promise; + }; + + self.notification = function (method, params) { + self.toStream(JSON.stringify(notification(method, params))); + }; + + self.batch = function (requests) { + var promises = []; + var message = []; + + forEach(requests, function (req) { + if (req.hasOwnProperty('call')) { + var _call = call(req.call.method, req.call.params); + // @ts-ignore + message.push(_call.message); + //TODO(jershell): batch reject if one promise reject, so catch reject and resolve error as result; + // @ts-ignore + promises.push( + _call.promise.then( + function (res) { + return res; + }, + function (err) { + return err; + }, + ), + ); + } else if (req.hasOwnProperty('notification')) { + // @ts-ignore + message.push(notification(req.notification.method, req.notification.params)); + } + }); + + self.toStream(JSON.stringify(message)); + return _Promise.all(promises); + }; + + self.messageHandler = function (rawMessage: any): Promise { + try { + var message = JSON.parse(rawMessage); + return beforeResolve(message); + } catch (e) { + logger.error('Error in messageHandler(): ', e); + throw ERRORS.PARSE_ERROR; + // self.toStream(JSON.stringify({ + // "id": null, + // "jsonrpc": "2.0", + // "error": ERRORS.PARSE_ERROR + // })); + } + }; + + self.customException = function (code, message, data) { + return new ServerError(code, message, data); + }; + }; + + // @ts-ignore + if (typeof define == 'function' && define.amd) { + // @ts-ignore + define('simple_jsonrpc', [], function () { + return simple_jsonrpc; + }); + } else if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = simple_jsonrpc; + } else if (typeof root !== 'undefined') { + root.simple_jsonrpc = simple_jsonrpc; + } else { + return simple_jsonrpc; + } +})(this); + +// create client +const jrpc = new simple_jsonrpc(); + +// add handler for json-rpc +jrpc.toStream = function (msg: { jsonrpc: string; method: any; id: number }) { + const xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + if (this.readyState !== 4) return; + try { + jrpc.messageHandler(this.responseText); + } catch (e) { + jrpc.messageHandler(JSON.stringify({ id: JSON.parse(msg as any).id, error: e })); + } + }; + xhr.open('POST', guiConfig.paws_url, true); + xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8'); + xhr.send(msg as any); +}; + +/** + * Retreives an analysis just like a real AP would + * @param params JSON request object + * @returns PAWSResponse or error + */ +export const PAWSAvailableSpectrum = (params: PAWSRequest): Promise> => + jrpc.call('spectrum.paws.getSpectrum', params).then((resp: any) => { + if (resp.deviceDesc) { + return success(resp); + } else { + return error(resp.message, resp.code, resp); + } + }); diff --git a/src/web/src/app/Lib/RatAfcApi.ts b/src/web/src/app/Lib/RatAfcApi.ts new file mode 100644 index 0000000..f7b0342 --- /dev/null +++ b/src/web/src/app/Lib/RatAfcApi.ts @@ -0,0 +1,134 @@ +import { + AvailableSpectrumInquiryRequest, + AvailableSpectrumInquiryResponse, + AvailableSpectrumInquiryResponseMessage, + DeploymentEnum, + VendorExtension, +} from './RatAfcTypes'; +import { RatResponse, success, error } from './RatApiTypes'; +import { guiConfig, getDefaultAfcConf, getCSRF } from './RatApi'; + +/** + * RatAfcApi.ts + * API functions for RAT AFC + */ + +/** + * Call RAT AFC resource + */ +export const spectrumInquiryRequest = async ( + request: AvailableSpectrumInquiryRequest, +): Promise> => { + let csrf_token = await getCSRF(); + + let resp = await fetch(guiConfig.rat_afc + '?debug=True&gui=True', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrf_token, + }, + body: JSON.stringify({ version: '1.4', availableSpectrumInquiryRequests: [request] }), + }); + + if (resp.status == 200) { + const data = (await resp.json()) as AvailableSpectrumInquiryResponseMessage; + // Get the first response until API can handle multiple requests + const response = data.availableSpectrumInquiryResponses[0]; + if (response.response.responseCode == 0) { + return success(data); + } else { + return error(response.response.shortDescription, response.response.responseCode, response); + } + } else { + return error(resp.statusText, resp.status, resp); + } +}; + +export const spectrumInquiryRequestByString = async ( + version: string, + requestAsJsonString: string, +): Promise> => { + let csrf_token = await getCSRF(); + let resp = await fetch(guiConfig.rat_afc + '?debug=True&gui=True', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrf_token, + }, + body: JSON.stringify({ version: version, availableSpectrumInquiryRequests: [JSON.parse(requestAsJsonString)] }), + }); + if (resp.status == 200) { + const data = (await resp.json()) as AvailableSpectrumInquiryResponseMessage; + // Get the first response until API can handle multiple requests + const response = data.availableSpectrumInquiryResponses[0]; + if (response.response.responseCode == 0) { + return success(data); + } else { + return error(response.response.shortDescription, response.response.responseCode, response); + } + } else { + return error(resp.statusText, resp.status, resp); + } +}; + +/** + * name + */ +export function downloadMapData(kml_filename: any, method: string): Promise { + var url = guiConfig.afc_kml.replace('p_kml_file', kml_filename); + return fetch(url, { + method: method, + headers: { + 'Content-Type': 'application/json', + 'Content-Encoding': 'gzip', + }, + }); +} + +export const sampleRequestObject: AvailableSpectrumInquiryRequest = { + requestId: '0', + deviceDescriptor: { + serialNumber: 'sample-ap', + certificationId: [ + { + rulesetId: 'US_47_CFR_PART_15_SUBPART_E', + id: '1234567890', + }, + ], + }, + location: { + ellipse: { + center: { + latitude: 41, + longitude: -74, + }, + majorAxis: 200, + minorAxis: 100, + orientation: 90, + }, + elevation: { + height: 15, + verticalUncertainty: 5, + heightType: 'AGL', + }, + + indoorDeployment: DeploymentEnum.indoorDeployment, + }, + minDesiredPower: 15, + vendorExtensions: [ + { + extensionId: 'RAT v1.3 AFC Config', + parameters: getDefaultAfcConf('US'), + }, + ], + inquiredChannels: [ + { + globalOperatingClass: 133, + }, + { + globalOperatingClass: 134, + channelCfi: [15, 47, 79], + }, + ], + inquiredFrequencyRange: [{ lowFrequency: 5925000000, highFrequency: 6425000000 }], +}; diff --git a/src/web/src/app/Lib/RatAfcTypes.ts b/src/web/src/app/Lib/RatAfcTypes.ts new file mode 100644 index 0000000..c4cb487 --- /dev/null +++ b/src/web/src/app/Lib/RatAfcTypes.ts @@ -0,0 +1,138 @@ +/** + * Type definitions for AFC API + */ + +import { AFCConfigFile } from './RatApiTypes'; + +/** + * Main request inquiry type. Passed to server inside of an array + */ +export interface AvailableSpectrumInquiryRequest { + requestId: string; + deviceDescriptor: DeviceDescriptor; + location: Location; + inquiredFrequencyRange?: FrequencyRange[]; + inquiredChannels?: Channels[]; + minDesiredPower?: number; + vendorExtensions: VendorExtension[]; +} +export interface CertificationId { + rulesetId: string; + id: string; +} + +export interface DeviceDescriptor { + serialNumber: string; + certificationId: CertificationId[]; +} + +export interface Elevation { + height?: number; + verticalUncertainty?: number; + heightType?: string; +} + +export interface Location { + ellipse?: Ellipse; + linearPolygon?: LinearPolygon; + radialPolygon?: RadialPolygon; + elevation?: Elevation; + indoorDeployment: DeploymentEnum; +} + +export interface Ellipse { + center: Point; + majorAxis: number; + minorAxis: number; + orientation: number; +} + +export interface LinearPolygon { + outerBoundary: Point[]; +} + +export interface RadialPolygon { + center: Point; + outerBoundary: Vector[]; +} + +export interface Point { + longitude: number; + latitude: number; +} + +export interface Vector { + length: number; + angle: number; +} + +export interface FrequencyRange { + lowFrequency: number; + highFrequency: number; +} + +export interface Channels { + globalOperatingClass: number; // TODO: check to see if we only want to allow one value + channelCfi?: number[]; +} + +export interface AvailableSpectrumInquiryResponseMessage { + version: string; + availableSpectrumInquiryResponses: AvailableSpectrumInquiryResponse[]; + vendorExtensions?: VendorExtension[]; +} + +export interface AvailableSpectrumInquiryResponse { + requestId: number; + availableFrequencyInfo?: AvailableSpectrumInfo[]; + availableChannelInfo?: AvailableChannelInfo[]; + availabilityExpireTime?: string; + response: InquiryResponse; + vendorExtensions?: VendorExtension[]; +} + +export interface AvailableSpectrumInfo { + frequencyRange: FrequencyRange; + maxPsd: number; +} + +export interface AvailableChannelInfo { + globalOperatingClass: number; + channelCfi: number[]; + maxEirp: number[]; +} + +export interface InquiryResponse { + responseCode: number; + shortDescription: string; //, + //supplementalInfo: SupplementalInfo +} + +export interface VendorExtension { + extensionId: string; + parameters: any; +} + +export enum DeploymentEnum { + indoorDeployment = 1, + outdoorDeployment = 2, + unkown = 0, +} + +export enum OperatingClassIncludeType { + None = 'None', + Some = 'Some', + All = 'All', +} + +export interface OperatingClass { + num: number; + include: OperatingClassIncludeType; + channels?: number[]; +} + +// A single channel for the heat map inquiry +export interface InquiredChannel { + channelCfi: number; + globalOperatingClass: number; +} diff --git a/src/web/src/app/Lib/RatApi.ts b/src/web/src/app/Lib/RatApi.ts new file mode 100644 index 0000000..a50192d --- /dev/null +++ b/src/web/src/app/Lib/RatApi.ts @@ -0,0 +1,1180 @@ +import { + GuiConfig, + AFCConfigFile, + PAWSRequest, + PAWSResponse, + AnalysisResults, + RatResponse, + ResSuccess, + ResError, + success, + error, + AFCEngineException, + ExclusionZoneRequest, + HeatMapRequest, + ExclusionZoneResult, + HeatMapResult, + FreqRange, +} from './RatApiTypes'; +import { logger } from './Logger'; +import { delay } from './Utils'; +import { hasRole } from './User'; +import { resolve } from 'path'; +import { AvailableSpectrumInquiryRequest, CertificationId, VendorExtension } from './RatAfcTypes'; + +/** + * RatApi.ts: member values and functions for utilizing server ratpi services + * author: Sam Smucny + */ + +// Member Definitions + +/** + * Global application index of URLs for API and other configuration variables + */ +export var guiConfig: GuiConfig = Object.freeze({ + paws_url: '', + uls_url: '', + antenna_url: '', + history_url: '', + afcconfig_defaults: '', + google_apikey: '', + rat_api_analysis: '', + uls_convert_url: '', + allowed_freq_url: '', + login_url: '', + admin_url: '', + ap_deny_admin_url: '', + dr_admin_url: '', + lidar_bounds: '', + ras_bounds: '', + rat_afc: '', + afcconfig_trial: '', + afc_kml: '', + mtls_admin_url: '', + version: 'API NOT LOADED', +}); + +/** + * Storage object to save page state in copied object when travelling between pages. + * The coppied object is a deep copy and breaks any references as the react component is destroyed. + * On component mount/dismount it is the responsibility of the component to correctly load/save state. + * + * The key for a cache item must be unique. Either use top level page name or combination of component name and parent component name + */ +const applicationCache: { [k: string]: any } = {}; + +/** + * If when the user opens the AFC config page there is no saved config on the server this is used instead. + * This is a last fallback to prevent undefined exceptions. The server can decide to provide a global default + * if current user does not have a saved config. + * @returns Default AFC config object + */ +const defaultAfcConf: () => AFCConfigFile = () => ({ + freqBands: [ + { + region: 'US', + name: 'UNII-5', + startFreqMHz: 5925, + stopFreqMHz: 6425, + }, + { + region: 'US', + name: 'UNII-7', + startFreqMHz: 6525, + stopFreqMHz: 6875, + }, + ], + ulsDefaultAntennaType: 'WINNF-AIP-07', + scanPointBelowGroundMethod: 'truncate', + polarizationMismatchLoss: { + kind: 'Fixed Value', + value: 3, + }, + bodyLoss: { + kind: 'Fixed Value', + valueIndoor: 0, + valueOutdoor: 0, + }, + buildingPenetrationLoss: { + kind: 'Fixed Value', + value: 0, + }, + receiverFeederLoss: { + IDU: 3, + ODU: 0, + UNKNOWN: 3, + }, + fsReceiverNoise: { + freqList: [6425], + noiseFloorList: [-110, -109.5], + }, + threshold: -6, + maxLinkDistance: 200, + maxEIRP: 36, + minEIRPIndoor: 21, + minEIRPOutdoor: -100, + minPSD: -100, + propagationModel: { + kind: 'FCC 6GHz Report & Order', + win2ConfidenceCombined: 16, + win2ConfidenceLOS: 16, + winner2LOSOption: 'BLDG_DATA_REQ_TX', + win2UseGroundDistance: false, + fsplUseGroundDistance: false, + winner2HgtFlag: false, + winner2HgtLOS: 15, + itmConfidence: 5, + itmReliability: 20, + p2108Confidence: 25, + buildingSource: 'None', + terrainSource: '3DEP (30m)', + }, + propagationEnv: 'NLCD Point', + fsDatabaseFile: 'rat_transfer/ULS_Database/FS_LATEST.sqlite3', + regionStr: 'US', + APUncertainty: { + points_per_degree: 3600, + height: 5, + maxVerticalUncertainty: 100, + maxHorizontalUncertaintyDistance: 650, + }, + ITMParameters: { + polarization: 'Vertical', + ground: 'Good Ground', + dielectricConst: 25, + conductivity: 0.02, + minSpacing: 30, + maxPoints: 1500, + }, + rlanITMTxClutterMethod: 'FORCE_TRUE', + clutterAtFS: true, + fsClutterModel: { + p2108Confidence: 5, + maxFsAglHeight: 6, + }, + nlcdFile: 'rat_transfer/nlcd/nlcd_production', + enableMapInVirtualAp: true, + channelResponseAlgorithm: 'psd', + visibilityThreshold: -6, + version: guiConfig.version, + allowScanPtsInUncReg: false, + passiveRepeaterFlag: true, + printSkippedLinksFlag: false, + reportErrorRlanHeightLowFlag: false, + nearFieldAdjFlag: true, + deniedRegionFile: '', + indoorFixedHeightAMSL: false, + reportUnavailableSpectrum: true, + reportUnavailPSDdBPerMHz: -40, + globeDir: 'rat_transfer/globe', + srtmDir: 'rat_transfer/srtm3arcsecondv003', + depDir: 'rat_transfer/3dep/1_arcsec', + cdsmDir: '', + lidarDir: 'rat_transfer/proc_lidar_2019', + nfaTableFile: 'rat_transfer/nfa/nfa_table_data.csv', + prTableFile: 'rat_transfer/pr/WINNF-TS-1014-V1.2.0-App02.csv', + radioClimateFile: 'rat_transfer/itudata/TropoClim.txt', + surfRefracFile: 'rat_transfer/itudata/N050.TXT', + rainForestFile: '', + regionDir: 'rat_transfer/population', + worldPopulationFile: 'rat_transfer/population/gpw_v4_population_density_rev11_2020_30_sec.tif', + roundPSDEIRPFlag: true, +}); + +const defaultAfcConfCanada: () => AFCConfigFile = () => ({ + freqBands: [ + { + region: 'CA', + name: 'Canada', + startFreqMHz: 5925, + stopFreqMHz: 6875, + }, + ], + ulsDefaultAntennaType: 'WINNF-AIP-07-CAN', + scanPointBelowGroundMethod: 'truncate', + polarizationMismatchLoss: { + kind: 'Fixed Value', + value: 3, + }, + bodyLoss: { + kind: 'Fixed Value', + valueIndoor: 0, + valueOutdoor: 0, + }, + buildingPenetrationLoss: { + kind: 'Fixed Value', + value: 0, + }, + receiverFeederLoss: { + IDU: 3, + ODU: 0, + UNKNOWN: 3, + }, + fsReceiverNoise: { + freqList: [6425], + noiseFloorList: [-110, -109.5], + }, + threshold: -6, + maxLinkDistance: 150, + maxEIRP: 36, + minEIRPIndoor: 21, + minEIRPOutdoor: -100, + minPSD: 8, + propagationModel: { + kind: 'ISED DBS-06', + win2ConfidenceCombined: 16, + win2ConfidenceLOS: 50, + win2ConfidenceNLOS: 50, + winner2LOSOption: 'CDSM', + win2UseGroundDistance: false, + fsplUseGroundDistance: false, + winner2HgtFlag: false, + winner2HgtLOS: 15, + itmConfidence: 5, + itmReliability: 20, + p2108Confidence: 10, + surfaceDataSource: 'Canada DSM (2000)', + terrainSource: '3DEP (30m)', + rlanITMTxClutterMethod: 'FORCE_TRUE', + }, + propagationEnv: 'NLCD Point', + fsDatabaseFile: 'rat_transfer/ULS_Database/FS_LATEST.sqlite3', + regionStr: 'CA', + APUncertainty: { + points_per_degree: 3600, + height: 5, + maxVerticalUncertainty: 100, + maxHorizontalUncertaintyDistance: 650, + }, + ITMParameters: { + polarization: 'Vertical', + ground: 'Good Ground', + dielectricConst: 25, + conductivity: 0.02, + minSpacing: 30, + maxPoints: 1500, + }, + rlanITMTxClutterMethod: 'FORCE_TRUE', + clutterAtFS: false, + fsClutterModel: { + p2108Confidence: 5, + maxFsAglHeight: 6, + }, + enableMapInVirtualAp: false, + channelResponseAlgorithm: 'pwr', + visibilityThreshold: -6, + version: guiConfig.version, + allowScanPtsInUncReg: false, + passiveRepeaterFlag: true, + printSkippedLinksFlag: false, + reportErrorRlanHeightLowFlag: false, + nearFieldAdjFlag: false, + deniedRegionFile: '', + indoorFixedHeightAMSL: false, + reportUnavailableSpectrum: false, + reportUnavailPSDdBPerMHz: -40, + srtmDir: 'rat_transfer/srtm3arcsecondv003', + depDir: 'rat_transfer/3dep/1_arcsec_wgs84', + cdsmDir: 'rat_transfer/cdsm/3ov4_arcsec_wgs84', + globeDir: 'rat_transfer/globe', + lidarDir: 'rat_transfer/proc_lidar_2019', + nfaTableFile: 'rat_transfer/nfa/nfa_table_data.csv', + prTableFile: 'rat_transfer/pr/WINNF-TS-1014-V1.2.0-App02.csv', + radioClimateFile: 'rat_transfer/itudata/TropoClim.txt', + surfRefracFile: 'rat_transfer/itudata/N050.TXT', + rainForestFile: '', + nlcdFile: 'rat_transfer/nlcd/ca/landcover-2020-classification_resampled.tif', + regionDir: 'rat_transfer/population', + worldPopulationFile: 'rat_transfer/population/gpw_v4_population_density_rev11_2020_30_sec.tif', + cdsmLOSThr: 0.5, + roundPSDEIRPFlag: false, +}); + +const defaultAfcConfBrazil: () => AFCConfigFile = () => ({ + freqBands: [ + { + region: 'BR', + name: 'Brazil', + startFreqMHz: 5925, + stopFreqMHz: 7125, + }, + ], + ulsDefaultAntennaType: 'F.699', + scanPointBelowGroundMethod: 'truncate', + polarizationMismatchLoss: { + kind: 'Fixed Value', + value: 3, + }, + bodyLoss: { + kind: 'Fixed Value', + valueIndoor: 0, + valueOutdoor: 0, + }, + buildingPenetrationLoss: { + kind: 'Fixed Value', + value: 0, + }, + receiverFeederLoss: { + IDU: 3, + ODU: 0, + UNKNOWN: 3, + }, + fsReceiverNoise: { + freqList: [6425], + noiseFloorList: [-110, -109.5], + }, + threshold: -6, + maxLinkDistance: 130, + maxEIRP: 36, + minEIRPIndoor: 21, + minEIRPOutdoor: -100, + minPSD: 8, + propagationModel: { + kind: 'Brazilian Propagation Model', + win2ConfidenceCombined: 50, + win2ConfidenceLOS: 50, + winner2LOSOption: 'BLDG_DATA_REQ_TX', + win2UseGroundDistance: false, + fsplUseGroundDistance: false, + winner2HgtFlag: false, + winner2HgtLOS: 15, + itmConfidence: 50, + itmReliability: 50, + p2108Confidence: 50, + buildingSource: 'None', + terrainSource: 'SRTM (30m)', + }, + propagationEnv: 'Population Density Map', + fsDatabaseFile: 'rat_transfer/ULS_Database/FS_LATEST.sqlite3', + regionStr: 'BR', + APUncertainty: { + points_per_degree: 3600, + height: 5, + maxVerticalUncertainty: 100, + maxHorizontalUncertaintyDistance: 650, + }, + ITMParameters: { + polarization: 'Vertical', + ground: 'Good Ground', + dielectricConst: 25, + conductivity: 0.02, + minSpacing: 30, + maxPoints: 1500, + }, + rlanITMTxClutterMethod: 'FORCE_TRUE', + clutterAtFS: false, + fsClutterModel: { + p2108Confidence: 5, + maxFsAglHeight: 6, + }, + nlcdFile: '', + enableMapInVirtualAp: false, + channelResponseAlgorithm: 'pwr', + visibilityThreshold: -6, + version: guiConfig.version, + allowScanPtsInUncReg: false, + passiveRepeaterFlag: true, + printSkippedLinksFlag: false, + reportErrorRlanHeightLowFlag: false, + nearFieldAdjFlag: false, + deniedRegionFile: '', + indoorFixedHeightAMSL: false, + reportUnavailableSpectrum: true, + reportUnavailPSDdBPerMHz: -40, + srtmDir: 'rat_transfer/srtm1arcsecond_wgs84', + rainForestFile: 'rat_transfer/population/Brazil_AmazonRainForest.kml', + roundPSDEIRPFlag: false, + depDir: 'rat_transfer/3dep/1_arcsec_wgs84', + cdsmDir: 'rat_transfer/cdsm/3ov4_arcsec_wgs84', + globeDir: 'rat_transfer/globe', + lidarDir: 'rat_transfer/proc_lidar_2019', + nfaTableFile: 'rat_transfer/nfa/nfa_table_data.csv', + prTableFile: 'rat_transfer/pr/WINNF-TS-1014-V1.2.0-App02.csv', + radioClimateFile: 'rat_transfer/itudata/TropoClim.txt', + surfRefracFile: 'rat_transfer/itudata/N050.TXT', + regionDir: 'rat_transfer/population', + worldPopulationFile: 'rat_transfer/population/gpw_v4_population_density_rev11_2020_30_sec.tif', +}); + +const defaultAfcConfUnitedKingdom: () => AFCConfigFile = () => ({ + freqBands: [ + { + region: 'GB', + name: 'United Kingdom', + startFreqMHz: 5925, + stopFreqMHz: 7125, + }, + ], + ulsDefaultAntennaType: 'F.699', + scanPointBelowGroundMethod: 'truncate', + polarizationMismatchLoss: { + kind: 'Fixed Value', + value: 3, + }, + bodyLoss: { + kind: 'Fixed Value', + valueIndoor: 0, + valueOutdoor: 0, + }, + buildingPenetrationLoss: { + kind: 'Fixed Value', + value: 20.5, + }, + receiverFeederLoss: { + IDU: 3, + ODU: 0, + UNKNOWN: 3, + }, + fsReceiverNoise: { + freqList: [6425], + noiseFloorList: [-110, -109.5], + }, + threshold: -6, + maxLinkDistance: 130, + maxEIRP: 36, + minEIRPIndoor: 21, + minEIRPOutdoor: -100, + minPSD: 8, + propagationModel: { + kind: 'Ofcom Propagation Model', + win2ConfidenceCombined: 50, + win2ConfidenceLOS: 50, + winner2LOSOption: 'BLDG_DATA_REQ_TX', + win2UseGroundDistance: false, + fsplUseGroundDistance: false, + winner2HgtFlag: false, + winner2HgtLOS: 15, + itmConfidence: 50, + itmReliability: 50, + p2108Confidence: 50, + buildingSource: 'None', + terrainSource: 'SRTM (30m)', + }, + propagationEnv: 'NLCD Point', + fsDatabaseFile: 'rat_transfer/ULS_Database/FS_LATEST.sqlite3', + regionStr: 'GB', + APUncertainty: { + points_per_degree: 3600, + height: 5, + maxVerticalUncertainty: 100, + maxHorizontalUncertaintyDistance: 650, + }, + ITMParameters: { + polarization: 'Vertical', + ground: 'Good Ground', + dielectricConst: 25, + conductivity: 0.02, + minSpacing: 30, + maxPoints: 1500, + }, + rlanITMTxClutterMethod: 'FORCE_TRUE', + clutterAtFS: false, + fsClutterModel: { + p2108Confidence: 5, + maxFsAglHeight: 6, + }, + nlcdFile: 'rat_transfer/nlcd/eu/U2018_CLC2012_V2020_20u1_resampled.tif', + enableMapInVirtualAp: false, + channelResponseAlgorithm: 'pwr', + visibilityThreshold: -6, + version: guiConfig.version, + allowScanPtsInUncReg: false, + passiveRepeaterFlag: true, + printSkippedLinksFlag: false, + reportErrorRlanHeightLowFlag: false, + nearFieldAdjFlag: false, + deniedRegionFile: '', + indoorFixedHeightAMSL: false, + reportUnavailableSpectrum: true, + reportUnavailPSDdBPerMHz: -40, + srtmDir: 'rat_transfer/srtm1arcsecond_wgs84', + roundPSDEIRPFlag: false, + depDir: 'rat_transfer/3dep/1_arcsec_wgs84', + cdsmDir: 'rat_transfer/cdsm/3ov4_arcsec_wgs84', + globeDir: 'rat_transfer/globe', + lidarDir: 'rat_transfer/proc_lidar_2019', + nfaTableFile: 'rat_transfer/nfa/nfa_table_data.csv', + prTableFile: 'rat_transfer/pr/WINNF-TS-1014-V1.2.0-App02.csv', + radioClimateFile: 'rat_transfer/itudata/TropoClim.txt', + surfRefracFile: 'rat_transfer/itudata/N050.TXT', + rainForestFile: '', + regionDir: 'rat_transfer/population', + worldPopulationFile: 'rat_transfer/population/gpw_v4_population_density_rev11_2020_30_sec.tif', +}); + +const defaultAllRegionFreqRanges: () => FreqRange[] = () => [ + { + region: 'US', + name: 'UNII-5', + startFreqMHz: 5925, + stopFreqMHz: 6425, + }, + { + region: 'US', + name: 'UNII-7', + startFreqMHz: 6525, + stopFreqMHz: 6875, + }, + { + region: 'CA', + name: 'Canada', + startFreqMHz: 5925, + stopFreqMHz: 6875, + }, + { + region: 'BR', + name: 'Brazil', + startFreqMHz: 5925, + stopFreqMHz: 7125, + }, + { + region: 'GB', + name: 'United Kingdom', + startFreqMHz: 5925, + stopFreqMHz: 7125, + }, +]; +// API Calls + +/** + * Retrive basic configuration options used across app + * and sets the [[guiConfig]] object. + */ +export async function getGuiConfig() { + await fetch('../ratapi/v1/guiconfig', { + method: 'GET', + }) + .then((res) => { + return res.json() as Promise; + }) + .then((conf) => { + guiConfig = Object.freeze(conf); + logger.info('server configuration loaded'); + }) + .catch((err) => { + logger.error(err); + }); +} + +/** + * Retrive the known regions for the Country options + */ +export const getRegions = (): Promise> => + fetch('../ratapi/v1/regions', { + method: 'GET', + }) + .then((res) => { + return res.text(); + }) + .then((name) => { + return success(name.split(' ')); + }) + .catch((err) => { + logger.error(err); + return err(err); + }); + +export const getAboutLoginAfc = (): Promise> => + fetch(guiConfig.about_login_url, { + method: 'GET', + }) + .then(async (res: Response) => { + if (res.ok) { + const content = await (res.text() as Promise); + logger.info('success loaded about login page' + content); + return success(content); + } else { + logger.error('could not load about login page'); + return error(res.statusText, res.status, res.body); + } + }) + .catch((err: any) => { + logger.error(err); + logger.error('could not load about login page'); + return error('could not load about login page'); + }); + +export const getAboutSiteKey = () => guiConfig.about_sitekey; + +export const getAboutAfc = (): Promise> => + fetch(guiConfig.about_url, { + method: 'GET', + }) + .then(async (res: Response) => { + if (res.ok) { + const content = await (res.text() as Promise); + logger.info('success loaded about page' + content); + return success(content); + } else { + logger.error('could not load about page'); + return error(res.statusText, res.status, res.body); + } + }) + .catch((err: any) => { + logger.error(err); + logger.error('could not load about page'); + return error('could not load about page'); + }); + +export const setAboutAfc = async ( + name: string, + email: string, + org: string, + token: string, +): Promise> => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.about_url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrf_token, + }, + body: JSON.stringify({ name: name, email: email, org: org, token: token }, undefined, 3), + }) + .then((res) => { + if (res.status === 204) { + return success('Access request submitted'); + } else { + return error(res.statusText, res.status); + } + }) + .catch((err) => { + logger.error(err); + return error('Unable to submit access request'); + }); +}; + +/** + * Retrive the known rulesets + */ +export const getRulesetIds = (): Promise> => + fetch('../ratapi/v1/rulesetIds', { + method: 'GET', + }) + .then((res) => { + return res.text(); + }) + .then((name) => { + return success(name.split(' ')); + }) + .catch((err) => { + logger.error(err); + return error(err); + }); + +/** + * Return a copy of the hard coded afc confic used as the default + * @returns The default AFC Configuration + */ +export const getDefaultAfcConf = (x: string | undefined) => { + if (!!x && (x.startsWith('TEST_') || x.startsWith('DEMO_'))) { + let testOrDemo: AFCConfigFile = getDefaultAfcConf(x.substring(5)); + testOrDemo.regionStr = x; + return testOrDemo; + } + switch (x) { + case 'CA': + return defaultAfcConfCanada(); + case 'BR': + return defaultAfcConfBrazil(); + case 'GB': + return defaultAfcConfUnitedKingdom(); + case 'US': + default: + return defaultAfcConf(); + } +}; + +/** + * Return the current afc config that is stored on the server. + * The config will be scoped to the current user + * @returns this user's current AFC Config or error + */ +export const getAfcConfigFile = (region: string): Promise> => + fetch(guiConfig.afcconfig_defaults.replace('default', region), { + method: 'GET', + }) + .then(async (res: Response) => { + if (res.ok) { + const config = await (res.json() as Promise); + return success(config); + } else { + logger.error('could not load afc configuration so falling back to dev default'); + return error(res.statusText, res.status, res.body); + } + }) + .catch((err: any) => { + logger.error(err); + logger.error('could not load afc configuration so falling back to dev default'); + return error('unable to load afc configuration'); + }); + +/** + * Update the afc config on the server with the one created by the user + * @param conf The AFC Config that will overwrite the server + * @returns success message or error + */ +export const putAfcConfigFile = async (conf: AFCConfigFile): Promise> => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.afcconfig_defaults.replace('default', conf.regionStr ?? 'US'), { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrf_token, + }, + body: JSON.stringify(conf, undefined, 3), + }) + .then((res) => { + if (res.status === 204) { + return success('AFC configuration updated'); + } else { + return error(res.statusText, res.status); + } + }) + .catch((err) => { + logger.error(err); + return error('Unable to update configuration'); + }); +}; + +/** + * Gets the admin supplied allowed frequency ranges for all regions. + * @returns Success: An array of FreqBand indicating the admin approved ranges. + * Error: why it failed + */ +export const getAllowedRanges = () => + fetch(guiConfig.admin_url.replace('-1', 'frequency_range'), { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }) + .then(async (res) => { + if (res.ok) { + const data = await (res.json() as Promise); + return success(data); + } else if (res.status == 404) { + return success(defaultAllRegionFreqRanges()); + } else { + logger.error(res); + return error(res.statusText, res.status, (await res.json()) as any); + } + }) + .catch((err) => { + if (err instanceof TypeError) { + logger.error('Unable to read allowedFrequencies.json, substituting defaults'); + return success(defaultAllRegionFreqRanges()); + } else { + logger.error(err); + return error('Your request was unable to be processed', undefined, err); + } + }); + +// Update all the frequency ranges to a new set +export const updateAllAllowedRanges = async (allRanges: FreqRange[]) => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.admin_url.replace('-1', 'frequency_range'), { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrf_token, + }, + body: JSON.stringify(allRanges), + }) + .then((res) => { + if (res.status === 204) { + return success('Frequency Range(s) updated.'); + } else { + return error(res.statusText, res.status); + } + }) + .catch((err) => { + logger.error(err); + return error('Unable to update frequency ranges.'); + }); +}; + +//Update all the ranges for a single region +export const updateAllowedRanges = async (regionStr: string, conf: FreqRange[]) => + getAllowedRanges() + .then((res) => { + let allRanges: FreqRange[]; + if (res.kind == 'Success') { + allRanges = res.result; + } else { + allRanges = defaultAllRegionFreqRanges(); + } + let updated = allRanges.filter((s) => s.region != regionStr).concat(conf); + Promise.resolve(updated); + }) + .then(async (newData) => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.admin_url.replace('-1', 'frequency_range'), { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrf_token, + }, + body: JSON.stringify(newData), + }) + .then((res) => { + if (res.status === 204) { + return success('Frequency Range(s) updated.'); + } else { + return error(res.statusText, res.status); + } + }) + .catch((err) => { + logger.error(err); + return error('Unable to update frequency ranges.'); + }); + }); + +// The following are part of the original way that results were posted and polled to the server. +// Could be deleted but would need to tear out all the references in the older code too +/** + * Helper method in request polling + * @param url URL to fetch + * @param method HTTP method to use + */ +const analysisUpdate = (url: string, method: string) => + fetch(url, { + method: method, + headers: { + 'Content-Type': 'application/json', + 'Content-Encoding': 'gzip', + }, + }); + +/** + * Continues a long running task by polling for status updates every 3 seconds. + * automatically handles cleanup of the task when completed barring any network interruption. + * optional parameters can be used to control the process while it is running. + * @typeparam T The type of result to be returned in promise when task completes + * @param isCanceled If this callback function exists it will be polled before each status update. If it returns true then the task will be aborted + * @param status Callback function that is used to update the caller of the task progress until completion + * @param setKml Callback function that is used to set KML file object if this request generates a KML file output + * @returns On sucess: The response type T. On failure: error message + */ +function analysisContinuation( + isCanceled?: () => boolean, + status?: (progress: { percent: number; message: string }) => void, + setKml?: (kml: Blob) => void, +) { + return async (startResponse: Response) => { + // use this to monitor and delete the task + const task = (await startResponse.json()) as any; + const url: string = task.statusUrl; + const kml: string | undefined = task.kmlUrl; + + // enter polling loop + while (true) { + await delay(3000); // wait 3 seconds before polling + if (isCanceled && isCanceled()) { + await analysisUpdate(url, 'DELETE'); + + // exit and return + return error('Task canceled'); + } + const res = await analysisUpdate(url, 'GET'); + if (res.status === 202 || res.status == 503) { + // task still in progress + if (!status || res.status == 503) continue; + const info = (await res.json()) as { percent: number; message: string }; + status(info); + } else if (res.status === 200 || res.status == 503) { + if (status) status({ percent: 100, message: 'Loading...' }); + + // get the result data + const data = (await res.json()) as T; + if (kml && setKml) { + // get kml in background since it is huge + analysisUpdate(kml, 'GET').then(async (kmlResp) => { + if (kmlResp.ok) setKml(await kmlResp.blob()); + await analysisUpdate(url, 'DELETE'); + }); + } else { + // delete resource since we are finished + await analysisUpdate(url, 'DELETE'); + } + + // exit and return + return success(data); + } else if (res.status === 550) { + // AFC Engine encoutered error + logger.error(res); + const exception = (await res.json()) as AFCEngineException; + await analysisUpdate(url, 'DELETE'); + + // exit and return + return error(exception.description, exception.exitCode, exception.env); + } else { + logger.error(res); + + // exit and return + return error(res.statusText, res.status); + } + } + }; +} + +/** + * Run PointAnalysis + * @param params request parameters + * @param isCanceled callback which indicates if the task should be terminated + * @param status callback which is used to notify caller of status of task. The PointAnalysis request provides minimal progress updates + * @returns Analysis results or error + */ +export const phase1Analysis = async ( + params: PAWSRequest, + isCanceled?: () => boolean, + status?: (progress: { percent: number; message: string }) => void, + setKml?: (kml: Blob) => void, +): Promise> => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.rat_api_analysis.replace('p_request_type', 'PointAnalysis'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrf_token, + }, + body: JSON.stringify(params), + }) + .then(analysisContinuation(isCanceled, status, setKml)) + .catch((err) => { + logger.error(err); + return error('Your request was unable to be processed', undefined, err); + }); +}; + +/** + * Run ExclusionZoneAnalysis + * @param params request parameters + * @param isCanceled callback which indicates if the task should be terminated + * @param status callback which is used to notify caller of status of task. The ExclusionZoneAnalysis request provides minimal progress updates + * @param setKml Callback function that is used to set KML file object that is generated by the exclusion zone + * @returns Exclusion zone result or error + */ +export const runExclusionZone = async ( + params: ExclusionZoneRequest, + isCanceled?: () => boolean, + status?: (progress: { percent: number; message: string }) => void, + setKml?: (kml: Blob) => void, +): Promise> => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.rat_api_analysis.replace('p_request_type', 'ExclusionZoneAnalysis'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrf_token, + }, + body: JSON.stringify(params), + }) + .then(analysisContinuation(isCanceled, status, setKml)) + .catch((err) => { + logger.error(err); + return error('Your request was unable to be processed', undefined, err); + }); +}; + +/** + * Run HeatmapAnalysis + * @param params request parameters + * @param isCanceled callback which indicates if the task should be terminated + * @param status callback which notifies caller of progress updates. Heatmap provides percentages and ETA in message string. + * @returns Heat map result or error + */ +export const runHeatMap = async ( + params: HeatMapRequest, + isCanceled?: () => boolean, + status?: (progress: { percent: number; message: string }) => void, +): Promise> => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.rat_api_analysis.replace('p_request_type', 'HeatmapAnalysis'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrf_token, + }, + body: JSON.stringify(params), + }) + .then(analysisContinuation(isCanceled, status)) + .catch((err) => { + logger.error(err); + return error('Your request was unable to be processed', undefined, err); + }); +}; + +/** + * Convert a ULS file in .csv format to .sqlite3 + * @param fileName `.csv` file to convert + * @returns Success: number of rows in ULS file that could not be converted. Error: information on why file could not be converted + */ +export const ulsFileConvert = async ( + fileName: string, +): Promise> => { + let csrf_token = await getCSRF(); + return fetch(guiConfig.uls_convert_url.replace('p_uls_file', fileName), { + method: 'POST', + headers: { 'X-CSRF-Token': csrf_token }, + }) + .then(async (res) => { + if (res.ok) { + const data = (await res.json()) as { invalidRows: number; errors: string[] }; + return success(data); + } else { + logger.error(res); + return error(res.statusText, res.status, (await res.json()) as any); + } + }) + .catch((err) => { + logger.error(err); + return error('Your request was unable to be processed', undefined, err); + }); +}; + +/** + * Continues a long running task by polling for status updates every 3 seconds. + * automatically handles cleanup of the task when completed barring any network interruption. + * optional parameters can be used to control the process while it is running. + * @param isCanceled If this callback function exists it will be polled before each status update. If it returns true then the task will be aborted + * @param status Callback function that is used to update the caller of the task progress until completion + * @returns On sucess: The result of the parse. On failure: error message + */ +function ulsParseContinuation( + isCanceled?: () => boolean, + status?: (progress: { percent: number; message: string }) => void, +) { + return async (startResponse: Response) => { + // use this to monitor and delete the task + const task = (await startResponse.json()) as any; + const url: string = task.statusUrl; + // enter polling loop + while (true) { + await delay(3000); // wait 3 seconds before polling + if (isCanceled && isCanceled()) { + await analysisUpdate(url, 'DELETE'); + + // exit and return + return error('Task canceled'); + } + const res = await analysisUpdate(url, 'GET'); + if (res.status === 202) { + //still running + //todo : add progress tracking + } else if (res.status === 200) { + // get the result data + const data = (await res.json()) as { entriesUpdated: number; entriesAdded: number; finishTime: string }; + // exit and return + return success(data); + } else if (res.status == 503) { + return error('Manual parse already in progress'); + } else { + logger.error(res); + + // exit and return + return error(res.statusText, res.status); + } + } + }; +} + +// End old code + +/** + * Cache an item in the global application cache + * @param key address to store at + * @param value object to cache + */ +export const cacheItem = (key: string, value: any) => { + applicationCache[key] = value; +}; + +/** + * Retrieve stored item from the global application cache + * @param key address of object to retrieve + * @returns Object if `key` could be found, `undefined` otherwise + */ +export const getCacheItem = (key: string): any | undefined => + applicationCache.hasOwnProperty(key) ? applicationCache[key] : undefined; + +/** + * Return a deep copy of the cache object that can be exported + * @returns deep copy of application state cache + */ +export const exportCache = () => JSON.parse(JSON.stringify(applicationCache)); + +/** + * Overwrite properties of existing cache with a new cache + * @param s cache object with new value to overwrite existing cache with + */ +export const importCache = (s: { [k: string]: any }) => Object.assign(applicationCache, s); + +/** + * Removes all items in cache + */ +export const clearCache = (): void => Object.keys(applicationCache).forEach((key) => delete applicationCache[key]); + +export const getCSRF = (): Promise => + fetch(guiConfig.about_csrf, { + method: 'GET', + }) + .then(async (res) => { + if (res.ok) { + const el = document.createElement('html'); + el.innerHTML = await res.text(); + const inp = el.getElementsByTagName('input'); + const len = inp.length; + for (let i = 0; i < len; i++) { + if (inp[i].name === 'csrf_token') { + return inp[i].value; + } + } + } else { + console.log(res.statusText, res.status); + return ''; + } + }) + .catch((e) => { + console.log('encountered an error when fetching csrf', undefined, e); + return ''; + }); + +export const heatMapRequestObject = ( + v: VendorExtension, + certificationId: CertificationId[], + serialNumber: string, +): AvailableSpectrumInquiryRequest => { + let d = { + deviceDescriptor: { + certificationId: certificationId, + serialNumber: serialNumber, + }, + inquiredChannels: [], + inquiredFrequencyRange: [ + { + highFrequency: 6321, + lowFrequency: 6320, + }, + ], + location: { + elevation: { + height: 7, + heightType: 'AGL', + verticalUncertainty: 0, + }, + ellipse: { + center: { + latitude: 40.3965175101759, + longitude: -74.2415393201817, + }, + majorAxis: 0, + minorAxis: 0, + orientation: 0, + }, + indoorDeployment: 0, + }, + requestId: '0', + vendorExtensions: [v], + }; + return d; +}; diff --git a/src/web/src/app/Lib/RatApiTypes.ts b/src/web/src/app/Lib/RatApiTypes.ts new file mode 100644 index 0000000..1b32e70 --- /dev/null +++ b/src/web/src/app/Lib/RatApiTypes.ts @@ -0,0 +1,716 @@ +/** + * RatApiTypes.ts: type definitions used by RatApi.ts + * author: Sam Smucny + */ + +import { string } from 'prop-types'; + +/** + * Interface of GUI configuration + */ +export interface GuiConfig { + paws_url: string; + uls_url: string; + antenna_url: string; + history_url: string; + afcconfig_defaults: string; + afcconfig_trial: string; + lidar_bounds: string; + ras_bounds: string; + google_apikey: string; + rat_api_analysis: string; + uls_convert_url: string; + login_url: string; + admin_url: string; + ap_deny_admin_url: string; + dr_admin_url: string; + mtls_admin_url: string; + rat_afc: string; + afc_kml: string; + version: string; +} + +export interface AFCConfigFile { + freqBands: FreqRange[]; + version: string; + maxEIRP: number; + minEIRPIndoor: number; + minEIRPOutdoor: number; + minPSD: number; + buildingPenetrationLoss: PenetrationLossModel; + polarizationMismatchLoss: PolarizationLossModel; + receiverFeederLoss: FSReceiverFeederLoss; + bodyLoss: BodyLossModel; + threshold: number; + maxLinkDistance: number; + ulsDefaultAntennaType: DefaultAntennaType; + scanPointBelowGroundMethod: ScanPointBelowGroundMethod; + propagationModel: PropagationModel; + APUncertainty: APUncertainty; + propagationEnv: 'NLCD Point' | 'Population Density Map' | 'Urban' | 'Suburban' | 'Rural'; + nlcdFile?: string; + ITMParameters: ITMParameters; + fsReceiverNoise: FSReceiverNoise; + rlanITMTxClutterMethod?: 'FORCE_TRUE' | 'FORCE_FALSE' | 'BLDG_DATA'; + clutterAtFS: boolean; + fsClutterModel?: FSClutterModel; + regionStr?: string; + enableMapInVirtualAp?: boolean; + channelResponseAlgorithm: ChannelResponseAlgorithm; + visibilityThreshold?: number; + allowScanPtsInUncReg?: boolean; + passiveRepeaterFlag?: boolean; + printSkippedLinksFlag?: boolean; + reportErrorRlanHeightLowFlag?: boolean; + nearFieldAdjFlag?: boolean; + deniedRegionFile?: string; + indoorFixedHeightAMSL?: boolean; + reportUnavailableSpectrum?: boolean; + reportUnavailPSDdBPerMHz?: number; + globeDir: string; + srtmDir: string; + depDir: string; + cdsmDir: string; + rainForestFile: string; + fsDatabaseFile: string; + lidarDir: string; + regionDir: string; + worldPopulationFile: string; + nfaTableFile: string; + prTableFile: string; + radioClimateFile: string; + surfRefracFile: string; + cdsmLOSThr?: number; + roundPSDEIRPFlag?: boolean; +} + +export type FreqRange = { + name: string; + startFreqMHz: number; + stopFreqMHz: number; + region?: string; +}; + +export type FSReceiverFeederLoss = { + IDU: number; + ODU: number; + UNKNOWN: number; +}; + +export type FSReceiverNoise = { + freqList: number[]; + noiseFloorList: number[]; +}; + +export type PenetrationLossModel = P2109 | FixedValue; + +export interface P2109 { + kind: 'ITU-R Rec. P.2109'; + buildingType: 'Traditional' | 'Efficient'; + confidence: number; +} + +export interface FixedValue { + kind: 'Fixed Value'; + value: number; +} + +export type PolarizationLossModel = EU | FixedValue; + +export interface EU { + kind: 'EU'; +} + +export type BodyLossModel = + | EU + | { + kind: 'Fixed Value'; + valueIndoor: number; + valueOutdoor: number; + }; + +export type APUncertainty = { + points_per_degree: number; + height: number; +}; + +export type ITMParameters = { + polarization: 'Vertical' | 'Horizontal'; + ground: GroundType; + dielectricConst: number; + conductivity: number; + minSpacing: number; + maxPoints: number; +}; + +export type GroundType = 'Average Ground' | 'Poor Ground' | 'Good Ground' | 'Fresh Water' | 'Sea Water'; + +export type AntennaPatternState = { + defaultAntennaPattern: DefaultAntennaType; + userUpload?: UserUpload; +}; + +export type DefaultAntennaType = 'F.1245' | 'F.699' | 'WINNF-AIP-07' | 'WINNF-AIP-07-CAN'; + +export type UserAntennaPattern = { + kind: string; + value: string; +}; + +export interface UserUpload { + kind: 'User Upload' | 'None'; + value: string; +} + +export type PropagationModel = + | FSPL + | Win2ItmDb + | Win2ItmClutter + | RayTrace + | FCC6GHz + | CustomPropagation + | IsedDbs06 + | BrazilPropModel + | OfcomPropModel; + +export type BuildingSourceValues = 'B-Design3D' | 'LiDAR' | 'None' | 'Canada DSM (2000)'; + +export interface FSPL { + kind: 'FSPL'; + fsplUseGroundDistance: boolean; +} +export interface Win2ItmDb { + kind: 'ITM with building data'; + win2ProbLosThreshold: number; + win2Confidence: number; + itmConfidence: number; + itmReliability: number; + p2108Confidence: number; + buildingSource: BuildingSourceValues; +} +export interface Win2ItmClutter { + kind: 'ITM with no building data'; + win2ProbLosThreshold: number; + win2Confidence: number; + itmConfidence: number; + itmReliability: number; + p2108Confidence: number; + terrainSource: 'SRTM (90m)' | '3DEP (30m)'; +} +export interface RayTrace { + kind: 'Ray Tracing'; +} +export interface FCC6GHz { + kind: 'FCC 6GHz Report & Order'; + win2ConfidenceCombined: number; + win2ConfidenceLOS?: number; + win2ConfidenceNLOS?: number; + itmConfidence: number; + itmReliability: number; + p2108Confidence: number; + buildingSource: BuildingSourceValues; + terrainSource: 'SRTM (90m)' | '3DEP (30m)'; + winner2LOSOption: 'UNKNOWN' | 'BLDG_DATA_REQ_TX'; + win2UseGroundDistance: boolean; + fsplUseGroundDistance: boolean; + winner2HgtFlag: boolean; + winner2HgtLOS: number; +} + +export interface IsedDbs06 { + kind: 'ISED DBS-06'; + winner2LOSOption: 'CDSM'; + win2ConfidenceCombined?: number; + win2ConfidenceLOS?: number; + win2ConfidenceNLOS?: number; + itmConfidence: number; + itmReliability: number; + p2108Confidence: number; + surfaceDataSource: 'Canada DSM (2000)' | 'None'; + terrainSource: 'SRTM (90m)' | '3DEP (30m)'; + rlanITMTxClutterMethod?: 'FORCE_TRUE' | 'FORCE_FALSE' | 'BLDG_DATA'; + win2UseGroundDistance: boolean; + fsplUseGroundDistance: boolean; + winner2HgtFlag: boolean; + winner2HgtLOS: number; +} + +export interface BrazilPropModel { + kind: 'Brazilian Propagation Model'; + win2ConfidenceCombined: number; + win2ConfidenceLOS?: number; + win2ConfidenceNLOS?: number; + itmConfidence: number; + itmReliability: number; + p2108Confidence: number; + buildingSource: BuildingSourceValues; + terrainSource: 'SRTM (30m)'; + winner2LOSOption: 'UNKNOWN' | 'BLDG_DATA_REQ_TX'; + win2UseGroundDistance: boolean; + fsplUseGroundDistance: boolean; + winner2HgtFlag: boolean; + winner2HgtLOS: number; +} + +export interface OfcomPropModel { + kind: 'Ofcom Propagation Model'; + win2ConfidenceCombined: number; + win2ConfidenceLOS?: number; + win2ConfidenceNLOS?: number; + itmConfidence: number; + itmReliability: number; + p2108Confidence: number; + buildingSource: BuildingSourceValues; + terrainSource: 'SRTM (30m)'; + winner2LOSOption: 'UNKNOWN' | 'BLDG_DATA_REQ_TX'; + win2UseGroundDistance: boolean; + fsplUseGroundDistance: boolean; + winner2HgtFlag: boolean; + winner2HgtLOS: number; +} + +export type CustomPropagationLOSOptions = 'UNKNOWN' | 'FORCE_LOS' | 'FORCE_NLOS' | 'BLDG_DATA_REQ_TX'; + +export interface CustomPropagation { + kind: 'Custom'; + winner2LOSOption: CustomPropagationLOSOptions; + win2ConfidenceCombined?: number; + win2ConfidenceLOS?: number; + win2ConfidenceNLOS?: number; + itmConfidence: number; + itmReliability: number; + p2108Confidence: number; + buildingSource: BuildingSourceValues; + terrainSource: 'SRTM (90m)' | '3DEP (30m)'; + rlanITMTxClutterMethod?: 'FORCE_TRUE' | 'FORCE_FALSE' | 'BLDG_DATA'; + win2UseGroundDistance: boolean; + fsplUseGroundDistance: boolean; + winner2HgtFlag: boolean; + winner2HgtLOS: number; +} + +export interface FSClutterModel { + p2108Confidence: number; + maxFsAglHeight: number; +} + +export type ScanPointBelowGroundMethod = 'discard' | 'truncate'; + +export type ChannelResponseAlgorithm = 'pwr' | 'psd'; + +/** + * PAWS request format is specified in + * [PAWS Profile AFC-6GHZ-DEMO-1.1](https://rkfeng.sharepoint.com/:w:/r/sites/FBRLANAFCTool/_layouts/15/Doc.aspx?sourcedoc=%7B0977F769-2B73-495C-8CEC-FDA231393192%7D&file=PAWS%20Profile%20AFC-6GHZ-DEMO-1.1.docx&action=default&mobileredirect=true) + */ +export interface PAWSRequest { + type: 'AVAIL_SPECTRUM_REQ'; + version: '1.0'; + deviceDesc: DeviceDescriptor; + location: GeoLocation; + owner?: DeviceOwner; + antenna: AntennaCharacteristics; + capabilities: DeviceCapabilities; + useAdjacentChannel?: boolean; +} + +/** + * PAWS response format is specified in + * [PAWS Profile AFC-6GHZ-DEMO-1.1](https://rkfeng.sharepoint.com/:w:/r/sites/FBRLANAFCTool/_layouts/15/Doc.aspx?sourcedoc=%7B0977F769-2B73-495C-8CEC-FDA231393192%7D&file=PAWS%20Profile%20AFC-6GHZ-DEMO-1.1.docx&action=default&mobileredirect=true) + */ +export interface PAWSResponse { + type: 'AVAIL_SPECTRUM_RESP'; + version: '1.0'; + timestamp: string; + deviceDesc: DeviceDescriptor; + spectrumSpecs: SpectrumSpec[]; + databaseChange?: DbUpdateSpec; +} + +interface DeviceDescriptor { + serialNumber: string; + manufacturerId?: string; + modelId?: string; + rulesetIds: ['AFC-6GHZ-DEMO-1.1']; +} + +interface GeoLocation { + point: Ellipse; +} + +export type Ellipse = { + center: { + latitude: number; + longitude: number; + }; + semiMajorAxis: number; + semiMinorAxis: number; + orientation: number; // default to 0 if not present +}; + +interface DeviceOwner {} + +interface AntennaCharacteristics { + height: number; + heightType: HeightType; + heightUncertainty: number; +} + +export enum HeightType { + AGL = 'AGL', + AMSL = 'AMSL', +} + +export interface DeviceCapabilities { + indoorOutdoor: IndoorOutdoorType; +} + +export enum IndoorOutdoorType { + INDOOR = 'Indoor', + OUTDOOR = 'Outdoor', + ANY = 'Any', + BUILDING = 'Database', +} + +export enum HeatMapAnalysisType { + ItoN = 'iton', + EIRP = 'availability', +} + +export enum HeatMapFsIdType { + All = 'All', + Single = 'Single', +} + +interface SpectrumSpec { + rulesetInfo: { authority: string; rulesetId: string }; + spectrumSchedules: SpectrumSchedule[]; + timeRange?: string; + frequencyRanges?: FrequencyRange[]; +} + +interface SpectrumSchedule { + eventTime: { startTime: string; stopTime: string }; + spectra: { + resolutionBwHz: number; + profiles: SpectrumProfile[][]; + }[]; +} + +export interface SpectrumProfile { + hz: number; // x axis on graph + dbm: number | undefined | null; // y axis on graph +} + +interface FrequencyRange { + min: number; + max: number; +} + +interface DbUpdateSpec {} + +export interface AnalysisResults { + // geoJson object as defined on https://tools.ietf.org/html/rfc7946 + // only properties that are related to this project are listed here. + geoJson: GeoJson; + channelData: ChannelData[]; + spectrumData: PAWSResponse; // Use the same structure as the PAWS response for this part of the response + statusMessageList?: string[]; +} + +export interface ExclusionZoneRequest { + height: number; + heightType: HeightType; + heightUncertainty: number; + indoorOutdoor: IndoorOutdoorType; + EIRP: number; + bandwidth: number; + centerFrequency: number; + FSID: number; +} +export interface ExclusionZoneResult { + geoJson: GeoJson; + statusMessageList?: string[]; +} + +export interface Bounds { + north: number; + south: number; + east: number; + west: number; +} + +export interface HeatMapRequest { + bounds: Bounds; + bandwidth: number; + centerFrequency: number; + spacing: number; + indoorOutdoor: + | { + kind: IndoorOutdoorType.INDOOR | IndoorOutdoorType.OUTDOOR | IndoorOutdoorType.ANY; + EIRP: number; + height: number; + heightType: HeightType; + heightUncertainty: number; + } + | { + kind: IndoorOutdoorType.BUILDING; + in: { + EIRP: number; + height: number; + heightType: HeightType; + heightUncertainty: number; + }; + out: { + EIRP: number; + height: number; + heightType: HeightType; + heightUncertainty: number; + }; + }; +} + +export interface HeatMapResult { + geoJson: GeoJson; + minItoN: number; + maxItoN: number; + threshold: number; + statusMessageList?: string[]; +} + +/** + * GeoJson specifications can be found at: [Geo JSON Spec](https://geojson.org/) + */ +export interface GeoJson { + type: 'FeatureCollection'; + features: { + type: 'Feature'; + /** + * A polymorphic member that has different functions + * depending on what is being displayed on map + */ + properties: + | { + kind: 'FS'; + FSID: number; + startFreq: number; + stopFreq: number; + } + | { + /** + * Buildings bounding box + */ + kind: 'BLDB'; + } + | { + kind: 'RLAN'; + FSLonLat: [number, number]; + startFrame?: number; + endFrame?: number; + } + | { + /** + * FS Exlusion Zone + */ + kind: 'ZONE'; // FS exclusion zone + FSID: number; // accociated FS + lat: number; + lon: number; + terrainHeight: number; + height: number; // AGL using terrainHeight + } + | { + kind: 'HMAP'; // heat map + ItoN: number; + indoor: 'Y' | 'N'; + }; // arbitrary description data (useful for pop-ups) + geometry: { + type: 'Polygon'; // Use for elipses and cones + coordinates: [number, number][][]; + }; + }[]; +} + +/** + * Channel data to be displayed on channel plot using `ChannelDisplay` component + */ +export interface ChannelData { + channelWidth: number; + startFrequency: number; + channels: { + color: string; + name: string; + maxEIRP?: number; + }[]; +} + +/** + * Specifies a color event in the Mobile AP + */ +export interface ColorEvent { + startFrame: number; + endFrame: number; + blinkPeriod: number; + colorA: string; + colorB: string; + require: + | { + type: 'REQ_PEND'; + requestIndex: number; + } + | { + type: 'NO_SERVICE'; + }; +} + +/** + * Configuration object used for Mobile AP Demo + * @see https://jira.rkf-engineering.com/jira/wiki/p/RAT/view/mobile-ap-demo/176 + */ +export interface MobileAPConfig { + path: { + frame: number; + lon: number; + lat: number; + }[]; + requests: { + request: PAWSRequest; + sendRequestFrame: number; + frameStart: number; + frameEnd: number; + }[]; + colorEvents: ColorEvent[]; +} + +/** + * User model modeled on database schema + */ +export interface UserModel { + id: number; + email: string; + org: string; + active: boolean; + firstName?: string; + lastName?: string; + roles: string[]; +} + +/** + * Access point model modeled on database schema + */ +export interface AccessPointModel { + id: number; + serialNumber: string; + certificationId?: string; + org?: string; + rulesetId?: string; +} + +export interface AccessPointListModel { + accessPoints?: string; +} + +/** + * MTLS model modeled on database schema + */ +export interface MTLSModel { + id: number; + cert: string; + note: string; + org: string; + created: string; +} + +/** + * Exception type generated when the AFC Engine encounters an error + */ +export interface AFCEngineException { + type: 'AFC Engine Exception'; + exitCode: number; + description: string; + env?: any; +} + +/** + * Denied Region model + */ +export interface ExclusionCircle { + latitude: number; + longitude: number; + radiusKm: number; +} +export interface ExclusionRect { + topLat: number; + leftLong: number; + bottomLat: number; + rightLong: number; +} +export interface ExclusionTwoRect { + rectangleOne: ExclusionRect; + rectangleTwo: ExclusionRect; +} +export interface ExclusionHorizon { + latitude: number; + longitude: number; + aglHeightM: number; +} + +export interface DeniedRegion { + regionStr: string; + name: string; + startFreq: number; + endFreq: number; + zoneType: 'Circle' | 'One Rectangle' | 'Two Rectangles' | 'Horizon Distance'; + exclusionZone: ExclusionCircle | ExclusionRect | ExclusionTwoRect | ExclusionHorizon; +} + +/** + * Simple implementation of the Either sum type. Used to encode the possibility of success or error in type. + * This Either type is specialized to encode errors that are usually the result of failed HTTP requests + * but in principle the type can be used anywhere. + * See the Haskell Either type for more details on the general type and examples of usage. + */ +export type RatResponse = ResError | ResSuccess; + +/** + * Constructs a pure RatResponse that is a success + * @param res payload of type T + */ +export function success(res: T): ResSuccess { + return { + kind: 'Success', + result: res, + }; +} + +/** + * Constructs a pure RatResponse that is an error + * @param des textual description of what went wrong + * @param code optional numerical error code if present + * @param body optional context that error occured in. Put stack traces, failed HTTP response, lots of text, or anything else that caller downstream user might find helpful. + */ +export function error(des: string, code?: number, body?: any): ResError { + return { + kind: 'Error', + description: des, + errorCode: code, + body: body, + }; +} + +/** + * Error/Left side of RatResponse + */ +export interface ResError { + kind: 'Error'; + description: string; + errorCode?: number; + body?: any; +} + +/** + * Success/Right side of RatResponse + */ +export interface ResSuccess { + kind: 'Success'; + result: T; +} diff --git a/src/web/src/app/Lib/User.ts b/src/web/src/app/Lib/User.ts new file mode 100644 index 0000000..bbd3605 --- /dev/null +++ b/src/web/src/app/Lib/User.ts @@ -0,0 +1,141 @@ +/** + * Portions copyright © 2022 Broadcom. All rights reserved. + * The term "Broadcom" refers solely to the Broadcom Inc. corporate + * affiliate that owns the software below. + * This work is licensed under the OpenAFC Project License, a copy + * of which is included with this software program. + */ + +import { createContext } from 'react'; +import { clearCache, guiConfig, importCache } from './RatApi'; +import { logger } from './Logger'; +import { RatResponse, error, success } from './RatApiTypes'; +import { letin } from './Utils'; + +/** + * User.ts: Module for handling user state and login + * author: Sam Smuncy + */ + +/** + * Storage object of current user of application + */ +export interface UserState { + data: + | { + loggedIn: false; + } + | { + loggedIn: true; + email: string; + org: string; + userId: number; + roles: Role[]; + firstName?: string; + lastName?: string; + active: boolean; + }; +} + +/** + * Sum type of possible roles a user can have + */ +export type Role = 'AP' | 'Analysis' | 'Admin' | 'Super' | 'Trial'; + +/** + * Create React context. This is used to provide a global service so that any component can access user state. + * Use this from root component to initialize user state before configuring user API + */ +export const UserContext = createContext({ data: { loggedIn: false } }); + +/** + * update the current user. To be overridden by configure. + * @param user The user to set as the current user in the application + */ +var setUser: (user: UserState) => void = () => {}; + +/** + * get the current user. To be overridden by configure. + * @returns The current user + */ +var getUser: () => UserState = () => ({ data: { loggedIn: false } }); + +/** + * Configure the User at application startup. + * Call from root node and pass in hooks so that user API can access global user object. + * @param get function to get the current user + * @param set function to update the current user + */ +export const configure = (get: () => UserState, set: (user: UserState) => void) => { + getUser = get; + setUser = set; +}; + +/** + * Gets user state from server and set user state. + * @param token authentication token for user + * @returns result of getting user data + */ +export const retrieveUserData = async (): Promise> => { + // get user information + const userInfoResult = await fetch(guiConfig.status_url, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'GET', + }); + + if (!userInfoResult.ok) return error(userInfoResult.statusText, userInfoResult.status); + + const userInfo = await userInfoResult.json(); + if (userInfo.status !== 'success') { + setUser({ data: { loggedIn: false } }); + return error(status, userInfoResult.status, userInfoResult); + } + + logger.info('User is logged in'); + // set the user session + setUser({ + data: Object.assign( + { + loggedIn: true, + }, + userInfo.data, + ), + }); + + logger.info('User: ', getUser().data); + + return success('User loaded'); +}; + +/** + * Is user credential is editable + */ +export const isEditCredential = () => getUser().data.editCredential; + +/** + * Is current user logged in? + * @returns User login status + */ +export const isLoggedIn = () => getUser().data.loggedIn; + +/** + * Does the current user have an active account? + * ie. have they verified their email. + * @returns status of active status + */ +export const isActive = () => letin(getUser(), (u) => u.data.loggedIn && u.data.active); + +/** + * Does the user have this role? + * @param role + * @returns Tells if user has this role + */ +export const hasRole = (role: Role) => letin(getUser(), (u) => u.data.loggedIn && u.data.roles.includes(role)); + +/** + * Is the current user an admin? + * @returns Tells if user is an admin + */ +export const isAdmin = () => hasRole('Admin') || hasRole('Super'); diff --git a/src/web/src/app/Lib/Utils.ts b/src/web/src/app/Lib/Utils.ts new file mode 100644 index 0000000..47b4b3d --- /dev/null +++ b/src/web/src/app/Lib/Utils.ts @@ -0,0 +1,210 @@ +import { Ellipse, AnalysisResults, SpectrumProfile, PAWSRequest, PAWSResponse } from './RatApiTypes'; + +/** + * Utils.ts: reusable functions that are accessible across app + * author: Sam Smucny + */ + +/** + * curried function that rotates `[x,y]` by `theta` (in radians) in the ccw direction + * @param theta angle to rotate by in counter clockwise direction + * @param (x, y) Point coordinates to be rotated by `theta` + * @returns The rotated point + */ +export const rotate = + (theta: number) => + ([x, y]: [number, number]): [number, number] => [ + x * Math.cos(theta) - y * Math.sin(theta), + y * Math.cos(theta) + x * Math.sin(theta), + ]; + +/** + * Calculates how much a translation in meters is in lat/lon using flat earth approximation. + * `dx` and `dy` specify an offset vector from some point. + * @param lat latitude at which to approximate + * @param (dy, dx) amount to offset in units of meters in each direction + * @returns a new offset vector in units of degrees of `[longitude, latitude]` + */ +export const meterOffsetToDeg = + (lat: number) => + ([dy, dx]: [number, number]): [number, number] => { + const latOffset = dy / 111111; // 1 degree of lat ~ 111111 m in flat approximation which should work here since we have small meter values of uncertainty and it is only a visual display + const lngOffset = dx / Math.cos((lat * Math.PI) / 180) / 111111; // 1 degree lng ~ 111111 * sec(lat) m + return [lngOffset, latOffset]; + }; + +/** + * approximates an ellipse shape using a polygon with n sides + * @param e Ellipse object to generate + * @param n number of sides (more sides is smoother) + * @returns points along elipse (length is `n+1`) + */ +export const rasterizeEllipse = (e: Ellipse, n: number): [number, number][] => { + const omega = (2 * Math.PI) / n; // define the angle increment in angle to use in raster + return Array(n + 1) + .fill(undefined) + .map((_, i) => { + const alpha = omega * i; // define angle to transform for this step + // use ellipse parameterization to generate offset point before rotation + return [e.semiMajorAxis * Math.sin(alpha), e.semiMinorAxis * Math.cos(alpha)] as [number, number]; + }) + .map(rotate((e.orientation * Math.PI) / 180)) // rotate offset in meter coordinates by orientation (- sign gives correct behavior with sin/cos) + .map(meterOffsetToDeg(e.center.latitude)) // transform offset point in meter coordinates to degree offset + .map(([dLng, dLat]) => [dLng + e.center.longitude, dLat + e.center.latitude]); // add offset to center point +}; + +/** + * Number checking continuation. + * @param s `string` that may be a number + * @param f continuation function that specifies what to do if value is a number. If `s` is not a number or is not valid then this is not called. + * @param validate optional validation function which provides additional restrictions on `s`'s value + */ +export function ifNum(s: string, f: (n: number) => void, validate?: (n: number) => boolean): void { + try { + const n = Number(s); + if (Number.isNaN(n)) { + return; + } else if (validate === undefined || validate(n)) { + f(n); + } + } catch {} +} + +/** + * Convert decimal to percent + * @param n decimal value + * @param percentage value + */ +export const toPer = (n: number) => n * 100; + +/** + * Convert percent to decimal + * @param n percent value + * @returns decimal value + */ +export const fromPer = (n: number) => n / 100; + +/** + * Convert from degrees to radians + * @param n argument in degrees + */ +export const toRad = (n: number) => (n * Math.PI) / 180.0; + +/** + * Creates an empty promise that resolves after `ms` milliseconds + * @returns promise that resolves after `ms` milliseconds + */ +export const delay = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Creates deep copy of object. + * @param obj object to copy + * @returns deep copy of `obj` + */ +export const clone = (obj: T): T => JSON.parse(JSON.stringify(obj)); + +/** + * Helper continuation function + * @param obj entity to evaluate + * @param f continuation function + * @returns result of `obj` applied to `f` (`f(obj)`) + */ +export const letin = (obj: T, f: (a: T) => U) => f(obj); + +/** + * Guaruntees an entity is not undefined + * @param obj possibly undefined object + * @param def default to return if object is undefined + * @returns `obj` value or `def` value if `obj` is `undefined` + */ +export const maybe = (obj: T | undefined, def: T) => (obj === undefined ? def : obj); + +/** + * Calculates the distance between two points in miles + * @param p1 point 1 + * @param p2 point 2 + * @returns distance between `p1` and `p2` in miles + */ +export const distancePtoP = (p1: { lat: number; lon: number }, p2: { lat: number; lon: number }) => { + // https://www.movable-type.co.uk/scripts/latlong.html + const R = 6371e3; // metres + const phi1 = toRad(p1.lat); // φ, λ in radians + const phi2 = toRad(p2.lat); + const deltaphi = toRad(p2.lat - p1.lat); + const deltalambda = toRad(p2.lon - p1.lon); + + const a = + Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) + + Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltalambda / 2) * Math.sin(deltalambda / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return (R * c) / 1609; // in mi +}; + +/** + * Calculates the distance between two points in meters + * @param p1 point 1 + * @param p2 point 2 + * @returns distance between `p1` and `p2` in meters + */ +export const distancePtoPSI = (p1: { lat: number; lon: number }, p2: { lat: number; lon: number }) => { + return distancePtoP(p1, p2) * 1609; // in m +}; + +export const bearing = (p1: { lat: number; lon: number }, p2: { lat: number; lon: number }) => { + // https://www.movable-type.co.uk/scripts/latlong.html + const dphi = toRad(p2.lat - p1.lat); // radians + const dlambda = toRad(p2.lon - p1.lon); // radians + if (dphi === 0) return dlambda > 0 ? 90 : 270; + const y = Math.sin(dlambda) * Math.cos(toRad(p2.lat)); + const x = + Math.cos(toRad(p1.lat)) * Math.sin(toRad(p2.lat)) - + Math.sin(toRad(p1.lat)) * Math.cos(toRad(p2.lat)) * Math.cos(dlambda); + const θ = Math.atan2(y, x); + const brng = ((θ * 180) / Math.PI + 360) % 360; // bearing in degrees + return brng; +}; + +// Gets the last used region from the afc config page value to use in presenting the correct +// config on the config page and looking up the last used config for showing/hiding the map on the AFC page +export const getLastUsedRegionFromCookie = () => { + var lastRegFromCookie: string | undefined = undefined; + if (document.cookie.indexOf('afc-config-last-region=') >= 0) { + lastRegFromCookie = document.cookie + .split('; ') + .find((row) => row.startsWith('afc-config-last-region=')) + ?.split('=')[1]; + } else { + lastRegFromCookie = 'US'; + } + + return lastRegFromCookie!; +}; + +// Converts the region codes to human readable text +export const mapRegionCodeToName = (code: string) => { + switch (code) { + case 'US': + return 'USA'; + break; + case 'CA': + return 'Canada'; + break; + case 'BR': + return 'Brazil'; + break; + case 'GB': + return 'United Kingdom'; + break; + default: + return code; + } +}; + +export const trimmedRegionStr = (regionStr: string | undefined) => { + if (!!regionStr && (regionStr.startsWith('TEST_') || regionStr.startsWith('DEMO_'))) { + return regionStr.substring(5); + } else { + return regionStr; + } +}; diff --git a/src/web/src/app/MTLS/MTLS.tsx b/src/web/src/app/MTLS/MTLS.tsx new file mode 100644 index 0000000..13d0f1d --- /dev/null +++ b/src/web/src/app/MTLS/MTLS.tsx @@ -0,0 +1,109 @@ +import * as React from 'react'; +import { MTLSModel, error, success } from '../Lib/RatApiTypes'; +import { getMTLS, addMTLS, deleteMTLSCert } from '../Lib/Admin'; +import { Card, CardHead, CardHeader, CardBody, PageSection } from '@patternfly/react-core'; +import { NewMTLS } from './NewMTLS'; +import { MTLSTable } from './MTLSTable'; +import { logger } from '../Lib/Logger'; +import { UserContext, UserState, isLoggedIn, hasRole } from '../Lib/User'; +import { UserModel } from '../Lib/RatApiTypes'; + +/** + * MTLSList.tsx: List of MTLS certs to add/remove + */ + +/** + * Interface definition of `MTLS` properties + */ +export interface MTLSProps { + userId: number; //the one created this certificate + filterId: number; //the userid to filter + org: string; +} + +interface MTLSState { + mtlsList: MTLSModel[]; +} + +/** + * Page level component to list a user's registerd access points. Users use these + * credentials to utilize the PAWS interface. + */ +export class MTLS extends React.Component { + constructor(props: MTLSProps) { + super(props); + + this.state = { + mtlsList: [], + }; + + getMTLS(props.filterId).then((res) => { + if (res.kind === 'Success') { + this.setState({ mtlsList: res.result }); + } else { + alert(res.description); + } + }); + } + + private async onAdd(mtls: MTLSModel) { + const res = await addMTLS(mtls, this.props.userId); + if (res.kind === 'Success') { + const newMTLSList = await getMTLS(this.props.filterId); + if (newMTLSList.kind === 'Success') { + this.setState({ mtlsList: newMTLSList.result }); + return success('Added'); + } + return error('Could not refresh list'); + } else { + return res; + } + } + + private async deleteMTLS(id: number) { + console.log('deleteMTLS:', String(id)); + const res = await deleteMTLSCert(id); + if (res.kind === 'Success') { + const newMTLSList = await getMTLS(this.props.filterId); + if (newMTLSList.kind === 'Success') { + this.setState({ mtlsList: newMTLSList.result }); + } + } else { + logger.error(res); + } + } + + render() { + return ( + + + MTLS + + + this.onAdd(mtls)} + userId={this.props.userId} + filterId={this.props.filterId} + /> +
    + this.deleteMTLS(id)} + filterId={this.props.filterId} + /> +
    +
    + ); + } +} + +/** + * wrapper for mtls when it is not embedded in another page + */ +export const MTLSPage: React.FunctionComponent = () => ( + + + {(u: UserState) => u.data.loggedIn && } + + +); diff --git a/src/web/src/app/MTLS/MTLSTable.tsx b/src/web/src/app/MTLS/MTLSTable.tsx new file mode 100644 index 0000000..2a32244 --- /dev/null +++ b/src/web/src/app/MTLS/MTLSTable.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { headerCol, Table, TableVariant, TableHeader, TableBody } from '@patternfly/react-table'; +import { AccessPointModel, UserModel } from '../Lib/RatApiTypes'; + +/** + * MTLS.tsx: Table that displays mtls certs. Shows org column if filterId is 0 + * author: Huy Ton + */ + +/** + * Interface definition of `MTLSTable` properties + */ +interface MTLSTableProps { + mtlsList: MTLSModel[]; + /** + * If `filterId` is 0 then the org column will be displayed (Super admin feature) + */ + filterId: number; + onDelete: (id: number) => void; +} + +/** + * Table component to display a user's access points. + */ +export class MTLSTable extends React.Component { + private columns = [ + { title: 'Certificate ID', cellTransforms: [headerCol()] }, + { title: 'Note' }, + { title: 'Created' }, + ]; + + constructor(props: MTLSTableProps) { + super(props); + this.state = { + rows: [], + }; + + if (props.filterId === 0) { + this.columns.push({ + title: 'Org', + }); + } + } + + private mtlsToRow = (mtls: MTLSModel) => ({ + id: mtls.id, + cells: this.props.filterId + ? [mtls.id, mtls.note || '', mtls.created || ''] + : [mtls.id, mtls.note || '', mtls.created || '', mtls.org], + }); + + actionResolver(data: any, extraData: any) { + return [ + { + title: 'Remove', + onClick: (event: any, rowId: number, rowData: any, extra: any) => this.props.onDelete(rowData.id), + }, + ]; + } + + render() { + return ( + this.actionResolver(a, b)} + > + + +
    + ); + } +} diff --git a/src/web/src/app/MTLS/NewMTLS.tsx b/src/web/src/app/MTLS/NewMTLS.tsx new file mode 100644 index 0000000..f4d26be --- /dev/null +++ b/src/web/src/app/MTLS/NewMTLS.tsx @@ -0,0 +1,159 @@ +import * as React from 'react'; +import { MTLSModel, RatResponse } from '../Lib/RatApiTypes'; +import { + Gallery, + GalleryItem, + FormGroup, + TextInput, + Button, + Alert, + AlertActionCloseButton, + FormSelect, + FormSelectOption, + InputGroup, +} from '@patternfly/react-core'; +import { PlusCircleIcon } from '@patternfly/react-icons'; +import { UserModel } from '../Lib/RatApiTypes'; +import { getUser } from '../Lib/Admin'; +import { hasRole } from '../Lib/User'; + +/** + * NewMTLS.tsx: Form for creating a new MTLS certificate + * author: Huy Ton + */ + +/** + * Interface definition of `NewMTLS` properties + */ +interface NewMTLSProps { + userId: number; + onAdd: (mtls: MTLSModel) => Promise>; +} + +interface NewMTLSState { + note: string; + cert: string; + messageType?: 'danger' | 'success'; + messageValue: string; + org?: string; +} + +/** + * Component with form for user to register a new mtls certificate. + * + */ +export class NewMTLS extends React.Component { + constructor(props: NewMTLSProps) { + super(props); + this.state = { + cert: '', + note: '', + org: '', + }; + getUser(props.userId).then((res) => + res.kind === 'Success' + ? this.setState({ + org: res.result.org, + } as NewMTLSState) + : this.setState({ + messageType: 'danger', + messageValue: res.description, + org: '', + }), + ); + } + + private submit() { + if (!this.state.org === 0) { + this.setState({ messageType: 'danger', messageValue: 'Org must be specified' }); + return; + } + + this.props + .onAdd({ + id: 0, + cert: this.state.cert, + note: this.state.note, + org: this.state.org, + created: '', + }) + .then((res) => { + if (res.kind === 'Success') { + this.setState((s: NewMTLSState) => ({ messageType: 'success', messageValue: 'Added with Note ' + s.note })); + } else { + this.setState({ messageType: 'danger', messageValue: res.description }); + } + }); + } + + private hideAlert = () => this.setState({ messageType: undefined }); + + fileChange(e) { + let files = e.target.files; + let reader = new FileReader(); + reader.readAsDataURL(files[0]); + reader.onload = (e) => { + console.warn('data file', e.target.result); + this.setState({ cert: e.target.result }); + }; + } + + render() { + const noteChange = (s?: string) => this.setState({ note: s }); + const orgChange = (s?: string) => this.setState({ org: s }); + + return ( + <> + {this.state.messageType && ( + } + /> + )} +
    + + + + this.fileChange(e)} /> + + +
    + + + + + + + + {hasRole('Super') && ( + + + + + + )} + + + +
    + + ); + } +} diff --git a/src/web/src/app/MobileAP/MobileAP.tsx b/src/web/src/app/MobileAP/MobileAP.tsx new file mode 100644 index 0000000..833ca0e --- /dev/null +++ b/src/web/src/app/MobileAP/MobileAP.tsx @@ -0,0 +1,822 @@ +import * as React from 'react'; +import { + PageSection, + CardHead, + CardBody, + Card, + Button, + Title, + Alert, + FormGroup, + InputGroup, + AlertActionCloseButton, +} from '@patternfly/react-core'; +import 'react-measure'; +import { MapContainer, MapProps } from '../Components/MapContainer'; +import { rasterizeEllipse, distancePtoP, letin } from '../Lib/Utils'; +import { + AnalysisResults, + PAWSRequest, + GeoJson, + PAWSResponse, + SpectrumProfile, + ChannelData, + AFCConfigFile, +} from '../Lib/RatApiTypes'; +import { cacheItem, getAfcConfigFile, getCacheItem } from '../Lib/RatApi'; +import { SpectrumDisplayPAWS } from '../Components/SpectrumDisplay'; +import { logger } from '../Lib/Logger'; +import { PAWSAvailableSpectrum } from '../Lib/PawsApi'; +import { MobileAPConfig, ColorEvent } from '../Lib/RatApiTypes'; +import { filterUNII, generateConfig, PathConfig, pawsToChannels } from './PathProcessor'; +import { ChannelDisplay } from '@app/Components/ChannelDisplay'; +import Measure from 'react-measure'; + +/** + * MobileAP.tsx: Page used for Mobile AP Animation + * author: Sam Smucny + */ + +/** + * Initial values of map properties + */ +const mapProps: MapProps = { + geoJson: { + type: 'FeatureCollection', + features: [], + }, + center: { + lat: 40, + lng: -100, + }, + mode: 'Point', + zoom: 2, + versionId: 0, +}; + +interface MapState { + val: GeoJson; + text: string; + valid: boolean; + dimensions: { + width: number; + height: number; + }; + versionId: number; +} + +/** + * Page level component for Mobile AP + */ +class MobileAP extends React.Component< + {}, + { + // Map display data + mapState: MapState; + mapCenter: { + lat: number; + lng: number; + }; + zoom: number; + latVal?: number; + lonVal?: number; + latValid: boolean; + lonValid: boolean; + markerColor?: string; + + results?: AnalysisResults; + messageType: 'None' | 'Info' | 'Error' | 'Warn'; + messageTitle: string; + messageValue: string; + extraWarning?: string; + extraWarningTitle?: string; + + /** + * The current frame being displayed on the map + */ + currentFrame: number; + + // time line properties (pause/play, playback speed, and timeline size) + isPlaying: boolean; + frameIncrement: number; + timeEditDisabled: boolean; + maxFrame: number; + + /** + * PAWS spectrum data that is going to be displayed on the map. Data is + * drawn from `state.spectrumCache` + */ + spectrums: (PAWSResponse | undefined)[]; + + /** + * The spectrum cache stores responses and information on when they were received. + * The cached data is then loaded by reference into the `state.spectrums` object for + * a given frame to simulate realtime playback + */ + spectrumCache: { spec: PAWSResponse; channels: ChannelData[]; frameStamp: number }[]; + + isCanceled: boolean; + apConfig?: MobileAPConfig; + minEIRP: number; + } +> { + /** + * Map styles for drawn objects + */ + private styles: Map; + + /** + * The period in ms used to update the display + */ + private intervalUpdatePeriod: number = 100; + + /** + * Interval Id of play timer. Do NOT update directly. + * Use `this.setThisInterval()` and `this.clearThisInterval()` instead. + */ + private intervalId?: number = undefined; + + /** + * Internal constant that determines the approximate framerate + */ + private fps = 5; + + handleJsonChange: (value: any) => void; + + constructor(props: any) { + super(props); + + // set the default start state, but load from cache if available + this.state = { + mapState: { + val: mapProps.geoJson, + text: '', + valid: false, + dimensions: { width: 0, height: 0 }, + versionId: 0, + }, + mapCenter: { + lat: 40, + lng: -100, + }, + latVal: undefined, + lonVal: undefined, + latValid: false, + lonValid: false, + zoom: 2, + messageTitle: '', + messageValue: '', + messageType: 'None', + extraWarning: undefined, + extraWarningTitle: undefined, + currentFrame: 0, + isPlaying: false, + frameIncrement: 1, + maxFrame: 30 * 25, + spectrums: [], + spectrumCache: [], + isCanceled: false, + timeEditDisabled: true, + minEIRP: 0, + }; + + this.handleJsonChange = (value) => { + try { + this.setMapState({ text: value, valid: true, versionId: this.state.mapState.versionId + 1 }); + return; + } catch (e) { + this.setMapState({ text: value, valid: false, versionId: this.state.mapState.versionId + 1 }); + return; + } + }; + + this.styles = new Map([ + ['PATH', { strokeColor: 'red' }], + ['RLAN', { strokeColor: 'blue', fillColor: 'lightblue' }], + ]); + } + + componentDidMount() { + const st = getCacheItem('mobileAPStateCache'); + if (st !== undefined) this.setState(st); + } + + componentWillUnmount() { + // before removing object, let's cache the state in case we want to come back + const state: any = this.state; + state.messageType = 'None'; + cacheItem('mobileAPStateCache', state); + } + + /** + * Sets the singular private interval + * @param callback The function to execute when the interval goes off + * @param interval The period in ms to call `callback` + */ + private setThisInterval(callback: () => void, interval: number) { + clearInterval(this.intervalId); + this.intervalId = setInterval(callback, interval) as unknown as number; + this.setState({ isPlaying: true }); + } + + /** + * Clears the singular private interval + */ + private clearThisInterval() { + clearInterval(this.intervalId); + this.intervalId = undefined; + this.setState({ isPlaying: false }); + } + + /** + * Increase the speed of playback by 2x + */ + private speedUp() { + this.setState({ frameIncrement: this.state.frameIncrement * 2 }); + } + + /** + * Decrease the speed of playback by 1/2. + * Does not decrease below 1x. + */ + private slowDown() { + if (this.state.frameIncrement >= 2) this.setState({ frameIncrement: this.state.frameIncrement / 2 }); + } + + /** + * Process a run configuration file and requests fresh data from afc-engine using PAWS + */ + private async runMobileAP() { + // get file input and load configuration + const fileInput = document.getElementById('mobile-ap-run-config-input'); + const files = (fileInput as any).files as File[]; + + // if there is a file selected then load it, else do nothing + if (files.length == 1) { + logger.info('parsing path config...'); + let pathConf: PathConfig; + try { + pathConf = JSON.parse(await files[0].text()) as PathConfig; + } catch (e) { + logger.error(e); + this.setState({ + messageType: 'Error', + messageTitle: 'Mobile AP Not Loaded', + messageValue: + 'The Mobile AP configuration could not be loaded from the file. The underlying file may have been modified. Try selecting a configuration file with a different name.', + }); + return; + } + logger.info('parsed path config:', pathConf); + + // convert config into more verbose mobile ap config + const conf = generateConfig(pathConf, this.fps); + + // load AFC Config from server because we need several properties to compute dispaly information + logger.info('retreiving afc config'); + //const afcConfig = await getAfcConfigFile(); + const afcConfig = await getAfcConfigFile('US'); + if (afcConfig.kind !== 'Success') { + this.setState({ + messageType: 'Error', + messageTitle: 'AFC Config Missing', + messageValue: 'The AFC Configuration could not be loaded from the server', + }); + return; + } + + // update state to prepare for play and load the spectrums + logger.info('Running mobile AP with config:', conf); + this.setState({ + apConfig: conf, + minEIRP: afcConfig.result.minEIRP, + currentFrame: 0, + maxFrame: conf.path[conf.path.length - 1].frame, + spectrumCache: [], + timeEditDisabled: true, + frameIncrement: 1, + }); + this.loadSpectrums(conf.requests, afcConfig.result); + } + } + + /** + * Sequentially loads multiple `PAWSRequest`s. + * Caches results with timestamp in `this.state.spectrumCache` + * to be used in playback. This function controls playback + * as data is loaded. + * @param requests A list of requests to process in order + */ + private async loadSpectrums( + requests: { + request: PAWSRequest; + sendRequestFrame: number; + result?: PAWSResponse; + frameStart: number; + frameEnd: number; + }[], + config: AFCConfigFile, + ) { + try { + // play and load through each of the PAWS requests in order + for (let i = 0; i < requests.length; i++) { + const spectrumResult = await this.loadAndPlayRequest(requests[i]); + if (spectrumResult === undefined) return; + const filteredSpectrum = filterUNII(spectrumResult); + if (spectrumResult !== undefined) { + this.state.spectrumCache[i] = { + spec: filteredSpectrum, + channels: pawsToChannels(filteredSpectrum, config.minEIRP, config.maxEIRP), + frameStamp: this.state.currentFrame, + }; + this.setState({ spectrums: this.state.spectrums }); + } + } + + // play through the remaining paths at the end when no more requests remain + this.playFromTo(this.state.currentFrame, this.state.maxFrame); + + // Now that all the data is loaded allow replay + this.setState({ timeEditDisabled: false }); + } catch (e) { + logger.error(e); + this.setState({ messageType: 'Error', messageTitle: 'Error: ' + e.code || '', messageValue: e.message }); + this.clearThisInterval(); + } + } + + /** + * processes a single request for the mobile ap + * waits until the request should be sent, sends it, + * and when the response arrives the playback + * is paused and the response it is returned + * @param request the request to process + */ + private async loadAndPlayRequest(request: { + request: PAWSRequest; + sendRequestFrame: number; + result?: PAWSResponse; + frameStart: number; + frameEnd: number; + }) { + // If the request is not supposed to be sent yet then continue play until that time + if (this.state.currentFrame < request.sendRequestFrame) { + this.setState({ messageType: 'Info', messageTitle: 'No Request Pending...', messageValue: '' }); + await this.playFromTo(this.state.currentFrame, request.sendRequestFrame); + } + + // Send the request + + // check AGL settings and possibly truncate + if ( + request.request.antenna.heightType === 'AGL' && + request.request.antenna.height - request.request.antenna.heightUncertainty < 1 + ) { + // modify if height is not 1m above terrain height + const minHeight = 1; + const maxHeight = request.request.antenna.height + request.request.antenna.heightUncertainty; + if (maxHeight < minHeight) { + this.setState({ + messageType: 'Error', + messageTitle: 'Invalid Height', + messageValue: `The height value must allow the AP to be at least 1m above the terrain. Currently the maximum height is ${maxHeight}m`, + }); + return; + } + const newHeight = (minHeight + maxHeight) / 2; + const newUncertainty = newHeight - minHeight; + request.request.antenna.height = newHeight; + request.request.antenna.heightUncertainty = newUncertainty; + logger.warn('Height was not at least 1 m above terrain, so it was truncated to fit AFC requirement'); + this.setState({ + extraWarningTitle: 'Truncated Height', + extraWarning: `The AP height has been truncated so that its minimum height is 1m above the terrain. The new height is ${newHeight}+/-${newUncertainty}m`, + }); + } + this.setState({ messageType: 'Info', messageTitle: 'Sending Request...', messageValue: '' }); + const resultPromise = PAWSAvailableSpectrum(request.request); + + // asyncronously play while waiting for response + this.playFromTo(this.state.currentFrame, this.state.maxFrame); + + // once the result arrives we pause play and load the data before continuing + const result = await resultPromise; + this.setState({ messageType: 'Info', messageTitle: 'Result Received.', messageValue: '' }); + + // clear the interval set by playFromTo() + this.clearThisInterval(); + + // return the result or throw an error + if (result.kind === 'Success') { + logger.info(result.result); + return result.result; + } else { + logger.error(result); + this.setState({ + messageType: 'Error', + messageTitle: 'A PAWS request encountered an error', + messageValue: result.description, + }); + throw result; + } + } + + /** + * Starts playback of animation between two frames + * @param from Frame to start at + * @param to Frame to end at + */ + private async playFromTo(from: number, to: number) { + return new Promise((resolve) => { + this.setState({ currentFrame: from }); + this.setThisInterval(() => { + const newFrame = Math.min(this.state.currentFrame + this.state.frameIncrement, to); + this.displayFrame(newFrame); + + if (this.state.currentFrame == to) { + this.clearThisInterval(); + resolve(); // return to awaiter once interval has cleared + } + }, this.intervalUpdatePeriod); + }); + } + + /** + * Indicates if a given spectrum should be displayed + * @param spectrumIndex Index of spectrum to check + * @param requests List of all requests + */ + private displaySpectrum = ( + spectrumIndex: number, + requests: { request: PAWSRequest; result?: PAWSResponse; frameStart: number; frameEnd: number }[], + ) => + requests[spectrumIndex] && + requests[spectrumIndex].frameStart <= this.state.currentFrame && + requests[spectrumIndex].frameEnd >= this.state.currentFrame; + + /** + * finds the channels for a given spectrum by reference + * @param spectrum the coresponding spectrum + */ + private getChannels = (spectrum?: PAWSResponse): ChannelData[] | undefined => + spectrum && + letin( + this.state.spectrumCache.findIndex((o) => o.spec === spectrum), + (index) => (index !== -1 ? this.state.spectrumCache[index].channels : undefined), + ); + + /** + * Get the marker position at a given frame + * @param path Path information to interpolate from + * @param frameNum Frame at which to get marker position + */ + private getMarkerPosition(path: { lat: number; lon: number; frame: number }[], frameNum: number) { + // Find the path that the marker is currently in + for (let i = 1; i < path.length; i++) { + if (frameNum <= path[i].frame) { + // Interpolate marker path + const deltaLat = path[i].lat - path[i - 1].lat; + const deltaLon = path[i].lon - path[i - 1].lon; + const pathFrac = (frameNum - path[i - 1].frame) / (path[i].frame - path[i - 1].frame); + return { lat: path[i - 1].lat + deltaLat * pathFrac, lng: path[i - 1].lon + deltaLon * pathFrac }; + } + } + throw { message: 'Marker position out of frame bounds', frameNum: frameNum, path: path }; + } + + /** + * Get the GeoJson that is visible at a given frame + * @param requests List of all requests + * @param frameNum The current frame + */ + private getVisibleGeoJson = ( + requests: { request: PAWSRequest; frameStart: number; frameEnd: number }[], + frameNum: number, + ) => + requests + // only include requests that overlap with the current frame + .filter((r, i) => r.frameStart <= frameNum && r.frameEnd >= frameNum && this.state.spectrums[i]) + // convert each visible request into an ellipse + .map((r) => ({ + type: 'Feature', + properties: { + kind: 'RLAN', + FSLonLat: [r.request.location.point.center.longitude, r.request.location.point.center.latitude], + startFrame: r.frameStart, + endFrame: r.frameEnd, + }, + geometry: { + type: 'Polygon', + coordinates: [rasterizeEllipse(r.request.location.point, 16)], + }, + })) + // add the polyline for the AP's path + //@ts-ignore + .concat( + !this.state.apConfig + ? [] + : [ + { + type: 'Feature', + properties: { + kind: 'PATH', + }, + geometry: { + type: 'LineString', + coordinates: this.state.apConfig.path.map((line) => [line.lon, line.lat]), + }, + }, + ], + ); + + /** + * Checks if any channel is available in a spectrum (ie. there exists EIRP >= min EIRP) + * @param spectra specta to check + * @param minEIRP minimum EIRP value that needs to be met to be available + */ + private channelAvailable( + spectra: { + resolutionBwHz: number; + profiles: SpectrumProfile[][]; + }[], + minEIRP: number, + ) { + for (let s = 0; s < spectra.length; s++) { + const spectrum = spectra[s]; + for (let p = 0; p < spectrum.profiles.length; p++) { + const profile = spectrum.profiles[p]; + for (let i = 0; i < profile.length; i++) { + if (Number.isFinite(profile[i].dbm) && profile[i].dbm! >= minEIRP) return true; + } + } + } + return false; + } + + /** + * Indicates if a given color event should be engaged + * based on some requirement + * @param req `ColorEvent` requirement to check + */ + private canBlink(req: { type: 'REQ_PEND'; requestIndex: number } | { type: 'NO_SERVICE' }, frameNum: number) { + if (req.type === 'REQ_PEND') { + // color event requires that there is a pending request so the coresponding spectrum should be `undefined` (not recieved yet) + return !this.state.spectrums[req.requestIndex]; + } else if (req.type === 'NO_SERVICE') { + // color event requres that there is no service so all visible request ellipses should have no available channels + const minEIRP = this.state.minEIRP; + // check if AP is in the covered ellipse of any request + for (let i = 0; i < this.state.apConfig!.requests.length; i++) { + const request = this.state.apConfig!.requests[i]; + if ( + !!this.state.spectrums[i] && + request.frameStart <= frameNum && + request.frameEnd >= frameNum && + this.channelAvailable(this.state.spectrums[i]!.spectrumSpecs[0].spectrumSchedules[0].spectra, minEIRP) + ) { + return false; + } + } + // if nothing in the for loop returned then it has no service + return true; + } else { + logger.warn('invalid color event detected'); + return false; + } + } + + /** + * Gets the marker color at a specific frame + * @param colorEvents List of `ColorEvent`s with display priority given in order of array + * @param frameNum Frame to display + */ + private getMarkerColor = (colorEvents: ColorEvent[], frameNum: number) => { + const colorEventsFiltered = colorEvents.filter((e) => e.startFrame <= frameNum && e.endFrame >= frameNum); + + for (let i = 0; i < colorEventsFiltered.length; i++) { + const colorEvent = colorEventsFiltered[i]; + if (this.canBlink(colorEvent.require, frameNum)) { + // divide two colors in half to simulate blinking + return (frameNum - colorEvent.startFrame) % colorEvent.blinkPeriod < colorEvent.blinkPeriod / 2 + ? colorEvent.colorA + : colorEvent.colorB; + } + } + return 'blue'; + }; + + private setMapState(obj: any) { + this.setState({ mapState: Object.assign(this.state.mapState, obj) }); + } + + /** + * Handler for clicking pause/play button + */ + private playPause = () => { + if (!this.state.apConfig) return; + if (this.state.isPlaying) { + this.clearThisInterval(); + } else { + this.playFromTo(this.state.currentFrame, this.state.maxFrame); + } + }; + + /** + * Renders a frame on the map + * @param frameNum The frame to display + */ + private displayFrame(frameNum: number) { + if (!this.state.apConfig) return; + + // Update the spectrum list from cache so that it follows playback accuratly + for (let i = 0; i < this.state.apConfig.requests.length; i++) { + const cache = this.state.spectrumCache[i]; + this.state.spectrums[i] = cache && cache.frameStamp <= this.state.currentFrame ? cache.spec : undefined; + } + + // get map display data + const markerPosition = this.getMarkerPosition(this.state.apConfig.path, frameNum); + const markerColor = this.getMarkerColor(this.state.apConfig.colorEvents, frameNum); + const geoJson: GeoJson = { + type: 'FeatureCollection', + //@ts-ignore + features: this.getVisibleGeoJson(this.state.apConfig.requests, frameNum), + }; + + // draw on map + this.setState({ + currentFrame: frameNum, + latVal: markerPosition.lat, + lonVal: markerPosition.lng, + markerColor: markerColor, + spectrums: this.state.spectrums, + }); + this.setMapState({ val: geoJson, versionId: this.state.mapState.versionId + 1 }); + } + + /** + * Get the estimated speed of the AP in mph + */ + private getSpeed(frameNum: number) { + if (!this.state.apConfig) return 0; + + const path = this.state.apConfig.path; + for (let i = 1; i < path.length; i++) { + if (frameNum <= path[i].frame) { + const distMi = distancePtoP(path[i - 1], path[i]); + const deltaTimeHr = (path[i].frame - path[i - 1].frame) / this.fps / 3600; + return distMi / deltaTimeHr; + } + } + return 0; + } + + render() { + return ( + + + + Run Mobile AP + + + + + + + +
    + +
    +
    +
    + {this.state.extraWarning && ( + this.setState({ extraWarning: undefined, extraWarningTitle: undefined })} + /> + } + > +
    {this.state.extraWarning}
    +
    + )} + {this.state.messageType === 'Info' && ( + + {this.state.messageValue} + + )} + {this.state.messageType === 'Error' && ( + +
    {this.state.messageValue}
    +
    + )} + {this.state.messageType === 'Warn' && ( + +
    {this.state.messageValue}
    +
    + )} +
    + + Time Line + + + {' '} + {' '} + {' '} + {this.state.frameIncrement + 'x'} + +
    +
    + this.displayFrame(Number.parseInt(event.target.value))} + /> +

    {Math.round(this.state.currentFrame / this.fps)} sec

    +

    {Math.round(this.getSpeed(this.state.currentFrame))} mph

    +
    +
    +
    +
    + +
    + {this.state.spectrums + .filter((spec) => !!spec) + .filter((_, i) => this.displaySpectrum(i, this.state.apConfig!.requests)) + .map((spec, i) => ( + +
    + + + + this.setMapState({ + dimensions: { width: contentRect.bounds!.width, height: contentRect.bounds!.height }, + }) + } + > + {({ measureRef }) => ( +
    + +
    + )} +
    +
    +
    + +
    + ))} +
    + ); + } +} + +export { MobileAP, MobileAPConfig }; diff --git a/src/web/src/app/MobileAP/PathProcessor.ts b/src/web/src/app/MobileAP/PathProcessor.ts new file mode 100644 index 0000000..772f641 --- /dev/null +++ b/src/web/src/app/MobileAP/PathProcessor.ts @@ -0,0 +1,343 @@ +import { clone, distancePtoPSI, distancePtoP, bearing } from '../Lib/Utils'; +import { emptyChannels } from '../Components/ChannelDisplay'; +import { MobileAPConfig } from './MobileAP'; +import { + PAWSRequest, + ColorEvent, + HeightType, + IndoorOutdoorType, + PAWSResponse, + SpectrumProfile, + ChannelData, +} from '../Lib/RatApiTypes'; +import { group } from 'console'; + +/** + * PathProcessor.tsx: additional mobile ap helper functions + * author: Sam Smucny + */ + +/** + * Path configuration object for Mobile AP + * @see https://jira.rkf-engineering.com/jira/wiki/p/RAT/view/mobile-ap-demo/176 + */ +export interface PathConfig { + path: { + mph: number; + lat: number; + lon: number; + }[]; + requestLeadTimeSec: number; + extraHandoffTimeSec: number; + serialNumber: string; + eccentricity: number; + fociBuffer: number; + antenna: { + height: number; + heightType: HeightType; + heightUncertainty: number; + }; + capabilities: { + indoorOutdoor: IndoorOutdoorType; + }; +} + +/** + * comuptes a timestamp based on multiple relative input conditions + * @param initialOffset offset to add before all others + * @param legs sections of path that add time + * @param finalOffset offset to add after all others + */ +const timeStamp = (initialOffset: number) => (legs: { frameSpan: number }[]) => (finalOffset: number) => + initialOffset + finalOffset + legs.map((l) => l.frameSpan).reduce((x, y) => x + y, 0); + +/** + * compute midpoint between two other cartesian points + * @param p1 point 1 + * @param p2 point 2 + */ +const meanPoint = (p1: { lat: number; lon: number }, p2: { lat: number; lon: number }) => ({ + latitude: (p1.lat + p2.lat) / 2, + longitude: (p1.lon + p2.lon) / 2, +}); + +/** + * computes the major axis of an ellipse in m + * @param eccentricity eccentricity of ellipse + * @param fociSeparation distance between ellipse foci in m + */ +const majorAxis = (eccentricity: number, fociSeparation: number) => + (fociSeparation / 2) * Math.sqrt(1 + 1 / (1 / (1 - eccentricity ** 2) - 1)); + +/** + * computes the minor axis of an ellipse in m + * @param eccentricity eccentricity of ellipse + * @param fociSeparation distance between ellipse foci in m + */ +const minorAxis = (eccentricity: number, fociSeparation: number) => + (fociSeparation / 2) * Math.sqrt(1 / (1 / (1 - eccentricity ** 2) - 1)); + +/** + * convert megahertz to hertz + * @param MHz megahertz + */ +const toHz = (MHz: number) => MHz * 1.0e6; + +/** + * lower <= x <= upper + * @param lower lower bound + * @param x value + * @param upper upper bound + */ +const between = (lower: number, x: number, upper: number) => lower <= x && x <= upper; + +/** + * Modifies a bandwidth group so that certain frequency bands are excluded + * @param bandwidthGroup group to modify + * @param lower1 lower bound of band 1 to exclude + * @param upper1 upper bound of band 1 to exclude + * @param lower2 lower bound of band 2 to exclude + * @param upper2 upper bound of band 2 to exclude + */ +const processGroup = ( + bandwidthGroup: { + resolutionBwHz: number; + profiles: SpectrumProfile[][]; + }, + lower1: number, + upper1: number, + lower2: number, + upper2: number, +) => { + const low1 = toHz(lower1); + const up1 = toHz(upper1); + const low2 = toHz(lower2); + const up2 = toHz(upper2); + for (let i = 0; i < bandwidthGroup.profiles.length; i++) { + for (let j = 0; j < bandwidthGroup.profiles[i].length - 1; j++) { + let curr = bandwidthGroup.profiles[i][j]; + let next = bandwidthGroup.profiles[i][j + 1]; + + if ( + (curr.hz !== next.hz && between(low1, curr.hz, up1) && between(low1, next.hz, up1)) || + (curr.hz !== next.hz && between(low2, curr.hz, up2) && between(low2, next.hz, up2)) + ) { + curr.dbm = -Infinity; + next.dbm = -Infinity; + j++; // we can skip next + } + } + } +}; + +/** + * removes U-NII-6/8 from channel list + * @param channelsIn channels to be processed + * @returns processed channels + */ +export const filterChannels = (channelsIn: ChannelData[]): ChannelData[] => { + let channels = clone(channelsIn); + for (let b = 0; b < channels.length; b++) { + let group = channels[b]; + for (let c = 0; c < group.channels.length; c++) { + let channel = group.channels[c]; + if (group.channelWidth === 20) { + if (between(97, Number(channel.name), 113) || between(185, Number(channel.name), 233)) { + channel.maxEIRP = -Infinity; + channel.color = 'grey'; + } + } else if (group.channelWidth === 40) { + if (between(99, Number(channel.name), 115) || between(187, Number(channel.name), 227)) { + channel.maxEIRP = -Infinity; + channel.color = 'grey'; + } + } else if (group.channelWidth === 80) { + if (between(103, Number(channel.name), 119) || between(183, Number(channel.name), 215)) { + channel.maxEIRP = -Infinity; + channel.color = 'grey'; + } + } else if (group.channelWidth === 160) { + if (between(111, Number(channel.name), 111) || between(175, Number(channel.name), 207)) { + channel.maxEIRP = -Infinity; + channel.color = 'grey'; + } + } + } + } + return channels; +}; + +/** + * modifies `channels` so that they have the correct colors and dbm values to match the given `prof` + * @param prof spectrum profile to use for reference + * @param channels channels to modify + * @param minEIRP min EIRP to use to color channels + * @param maxEIRP max EIRP to use to color channels + */ +const assignChannels = (prof: SpectrumProfile[], channels: ChannelData, minEIRP: number, maxEIRP: number): void => { + for (let i = 0; i < channels.channels.length; i++) { + let chan = channels.channels[i]; + let point = prof[i * 2]; + const dbm = point.dbm; + chan.color = + dbm === null + ? 'red' + : dbm === undefined + ? 'grey' + : dbm === -Infinity + ? 'grey' + : dbm >= maxEIRP + ? 'green' + : dbm >= minEIRP + ? 'yellow' + : 'red'; + chan.maxEIRP = dbm || undefined; + } +}; + +/** + * processes a paws response into channel data + * @param resp response to process + * @param minEIRP min EIRP + * @param maxEIRP max EIRP + * @returns the processed channel list + */ +export const pawsToChannels = (resp: PAWSResponse, minEIRP: number, maxEIRP: number): ChannelData[] => { + const channels = clone(emptyChannels); + const spectra = resp.spectrumSpecs[0].spectrumSchedules[0].spectra; + spectra.forEach((spectrum) => { + if (toHz(20) === spectrum.resolutionBwHz) { + assignChannels(spectrum.profiles[0], channels[0], minEIRP, maxEIRP); + } else if (toHz(40) === spectrum.resolutionBwHz) { + assignChannels(spectrum.profiles[0], channels[1], minEIRP, maxEIRP); + } else if (toHz(80) === spectrum.resolutionBwHz) { + assignChannels(spectrum.profiles[0], channels[2], minEIRP, maxEIRP); + } else if (toHz(160) === spectrum.resolutionBwHz) { + assignChannels(spectrum.profiles[0], channels[3], minEIRP, maxEIRP); + } + }); + return channels; +}; + +/** + * excludes U-NII-6 and U-NII-8 from channels + * @param request PAWSRequest to process + * @returns processed request + */ +export const filterUNII = (respIn: PAWSResponse): PAWSResponse => { + let resp = clone(respIn); + resp.spectrumSpecs.forEach((spec) => + spec.spectrumSchedules.forEach((spectrum) => + spectrum.spectra.forEach((bandwidthGroup) => { + if (bandwidthGroup.resolutionBwHz === toHz(160)) { + processGroup(bandwidthGroup, 6425, 6585, 6745, 7125); + } else if (bandwidthGroup.resolutionBwHz === toHz(80)) { + processGroup(bandwidthGroup, 6425, 6585, 6825, 7125); + } else if (bandwidthGroup.resolutionBwHz === toHz(40)) { + processGroup(bandwidthGroup, 6425, 6545, 6865, 7125); + } else if (bandwidthGroup.resolutionBwHz === toHz(20)) { + processGroup(bandwidthGroup, 6425, 6525, 6865, 7125); + } + }), + ), + ); + return resp; +}; + +/** + * Uses a `PathConfig` to build a more detailed `MobileAPConfig` which is used to run the demo + * @param pathConfig `PathConfig` to expand into a `MobileAPConfig` + * @param fps The frames per second to use to compute frame numbers from time information + */ +export function generateConfig(pathConfig: PathConfig, fps: number): MobileAPConfig { + const path = pathConfig.path; + const stationaryStartOffset = pathConfig.requestLeadTimeSec * fps; + const extraHandoffFrames = pathConfig.extraHandoffTimeSec * fps; + const fociBuffer = pathConfig.fociBuffer * 2; // multiply by 2 because we want the buffer on both sides + + const pathOffset = clone(path); + pathOffset.shift(); + + const timeStampOrigin = timeStamp(stationaryStartOffset); + + const legs = pathOffset.map((e, i) => ({ + start: path[i], + end: e, + distanceM: distancePtoPSI(path[i], e), + speedMph: path[i].mph, + frameSpan: (distancePtoP(path[i], e) / path[i].mph) * 3600 * fps, + })); + + // calculate axis lengths from eccentricity. check to make sure eccentricity is valid + let e = pathConfig.eccentricity; + if (e <= 0 || e >= 1) throw { message: 'Ecentricity is not valid. Must be in (0,1) for an ellipse.' }; + + const requests: { + request: PAWSRequest; + sendRequestFrame: number; + frameStart: number; + frameEnd: number; + }[] = legs.map((leg, i) => ({ + sendRequestFrame: timeStampOrigin(legs.filter((l, j) => j < i))(-stationaryStartOffset), + frameStart: timeStampOrigin(legs.filter((l, j) => j < i))(-stationaryStartOffset), + frameEnd: timeStampOrigin(legs.filter((l, j) => j <= i))(extraHandoffFrames), + request: { + type: 'AVAIL_SPECTRUM_REQ', + version: '1.0', + deviceDesc: { + serialNumber: pathConfig.serialNumber, + rulesetIds: ['AFC-6GHZ-DEMO-1.1'], + }, + antenna: pathConfig.antenna, + capabilities: pathConfig.capabilities, + location: { + point: { + // linear eccentricity = leg.distanceM / 2 + center: meanPoint(leg.start, leg.end), + semiMajorAxis: majorAxis(e, leg.distanceM + fociBuffer), + semiMinorAxis: minorAxis(e, leg.distanceM + fociBuffer), + orientation: bearing(leg.start, leg.end), + }, + }, + }, + })); + + const frammedPath = [{ lat: path[0].lat, lon: path[0].lon, frame: 0 }].concat( + path.map((stop, i) => ({ + lat: stop.lat, + lon: stop.lon, + frame: timeStampOrigin(legs.filter((leg, j) => j < i))(0), + })), + ); + + const colorEvents: ColorEvent[] = [ + { + colorA: 'yellow', + colorB: 'firebrick', + blinkPeriod: 5, + require: { + type: 'NO_SERVICE', + }, + startFrame: 0, + endFrame: frammedPath[frammedPath.length - 1].frame, + } as unknown as ColorEvent, + ].concat( + requests.map((req, i) => ({ + colorA: 'yellow', + colorB: 'green', + blinkPeriod: 7, + require: { + type: 'REQ_PEND', + requestIndex: i, + }, + startFrame: req.sendRequestFrame, + endFrame: req.frameEnd, + })), + ); + + return { + path: frammedPath, + colorEvents: colorEvents, + requests: requests, + }; +} diff --git a/src/web/src/app/NotFound/NotFound.tsx b/src/web/src/app/NotFound/NotFound.tsx new file mode 100644 index 0000000..f8c644a --- /dev/null +++ b/src/web/src/app/NotFound/NotFound.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { NavLink } from 'react-router-dom'; +import { Alert, PageSection } from '@patternfly/react-core'; + +// NotFound.tsx: 404 page +// author: Sam Smucny + +const NotFound: React.FunctionComponent = () => { + return ( + + +
    + + Back to Dashboard + +
    + ); +}; + +export { NotFound }; diff --git a/src/web/src/app/RatAfc/ElevationForm.tsx b/src/web/src/app/RatAfc/ElevationForm.tsx new file mode 100644 index 0000000..8d6ed4c --- /dev/null +++ b/src/web/src/app/RatAfc/ElevationForm.tsx @@ -0,0 +1,146 @@ +import * as React from 'react'; +// import ReactTooltip from 'react-tooltip'; +import { Location, LinearPolygon, RadialPolygon, Ellipse } from '../Lib/RatAfcTypes'; +import { + GalleryItem, + FormGroup, + InputGroup, + FormSelect, + FormSelectOption, + TextInput, + InputGroupText, +} from '@patternfly/react-core'; +import { ifNum } from '../Lib/Utils'; +import { PairList } from './PairList'; + +/** ElevationForm.tsx - Form component to display and create the Elevation object + * + * mgelman 2022-01-04 + */ + +/** + * possible height types + */ +enum HeightType { + AGL = 'AGL', + AMSL = 'AMSL', +} + +/** + * enumerated location types + */ +const heightTypes: string[] = [HeightType.AGL, HeightType.AMSL]; + +export interface ElevationFormParams { + elevation: { + height?: number; + heightType?: string; + verticalUncertainty?: number; + }; + onChange: (val: { height?: number; heightType?: string; verticalUncertainty?: number }) => void; +} + +export interface ElevationFormState { + height?: number; + heightType?: string; + verticalUncertainty?: number; +} + +export class ElevationForm extends React.PureComponent { + constructor(props: ElevationFormParams) { + super(props); + this.state = { + height: props.elevation.height, + heightType: props.elevation.heightType, + verticalUncertainty: props.elevation.verticalUncertainty, + }; + } + + private setHeight(n: number) { + this.props.onChange({ + height: n, + heightType: this.props.elevation.heightType, + verticalUncertainty: this.props.elevation.verticalUncertainty, + }); + } + + private setVerticalUncertainty(n: number) { + this.setState({ verticalUncertainty: n }, () => + this.props.onChange({ + height: this.props.elevation.height, + heightType: this.props.elevation.heightType, + verticalUncertainty: n, + }), + ); + } + + private setHeightType(s: string) { + this.setState({ heightType: s }, () => + this.props.onChange({ + height: this.props.elevation.height, + heightType: s, + verticalUncertainty: this.props.elevation.verticalUncertainty, + }), + ); + } + + render() { + return ( + <> + + + + this.setHeight(Number(x))} + type="number" + step="any" + id="horizontal-form-height" + name="horizontal-form-height" + isValid={this.props.elevation.height >= 0} + style={{ textAlign: 'right' }} + /> + meters + + + + + + + this.setHeightType(x)} + id="horzontal-form-height-type" + name="horizontal-form-height-type" + isValid={heightTypes.includes(this.props.elevation.heightType)} + style={{ textAlign: 'right' }} + > + + {heightTypes.map((option) => ( + + ))} + + + + + + + + this.setVerticalUncertainty(Number(x))} + type="number" + step="any" + id="horizontal-form-height-cert" + name="horizontal-form-height-cert" + isValid={this.props.elevation.verticalUncertainty >= 0} + style={{ textAlign: 'right' }} + /> + meters + + + + + ); + } +} diff --git a/src/web/src/app/RatAfc/InquiredFrequencyForm.tsx b/src/web/src/app/RatAfc/InquiredFrequencyForm.tsx new file mode 100644 index 0000000..4053f61 --- /dev/null +++ b/src/web/src/app/RatAfc/InquiredFrequencyForm.tsx @@ -0,0 +1,137 @@ +import * as React from 'react'; +// import ReactTooltip from 'react-tooltip'; +import { Table, TableHeader, TableBody, TableVariant, TableProps } from '@patternfly/react-table'; +import { GalleryItem, FormGroup, InputGroup, Radio, TextInput, InputGroupText, Button } from '@patternfly/react-core'; +import { FrequencyRange } from '../Lib/RatAfcTypes'; +/** InquiredFrequencyFormParams.tsx - Form component to display and create the list of frequency + * + * mgelman 2022-02-09 + */ + +export interface InquiredFrequencyFormParams { + inquiredFrequencyRange: FrequencyRange[]; + onChange: (val: { inquiredFrequencyRange: FrequencyRange[] }) => void; +} + +export interface InquiredFrequencyFormState { + newLowFreq?: number; + newHighFreq?: number; + columns?: string[]; +} + +export class InquiredFrequencyForm extends React.PureComponent< + InquiredFrequencyFormParams, + InquiredFrequencyFormState +> { + constructor(props: InquiredFrequencyFormParams) { + super(props); + this.state = { + newLowFreq: undefined, + newHighFreq: undefined, + columns: ['#', 'Low (MHz)', 'High (MHz)'], + }; + } + + // private setInclude(n: OperatingClassIncludeType) { + // this.props.onChange({ + // include: n, + // channels: this.props.operatingClass.channels, + // num: this.props.operatingClass.num + // } + // ); + // } + + actionResolver(data: any, extraData: any) { + return [ + { + title: 'Delete', + onClick: (event: any, rowId: any, rowData: any, extra: any) => this.removeFreqBand(rowId), + }, + ]; + } + + // private updateTableEntry = (freq :FrequencyRange) => { + // const {frequencyEditIndex, frequencyBands} = this.state; + // frequencyBands[frequencyEditIndex] = freq; + // this.setState({frequencyBands: frequencyBands}) + // } + + private freqBandToRow(band: FrequencyRange, index: number) { + return [String(index + 1), band.lowFrequency, band.highFrequency]; + } + + private removeFreqBand(index: number) { + var newRanges = this.props.inquiredFrequencyRange.slice(); + newRanges.splice(index, 1); + this.props.onChange({ inquiredFrequencyRange: newRanges }); + } + + private renderFrequencyTable = () => { + return ( + this.actionResolver(a, b)} + variant={TableVariant.compact} + cells={this.state.columns} + rows={this.props.inquiredFrequencyRange.map((band, index) => this.freqBandToRow(band, index))} + > + + +
    + ); + }; + + private submitBand() { + const low = this.state.newLowFreq; + const high = this.state.newHighFreq; + if (low && high && low < high) { + const newRanges = this.props.inquiredFrequencyRange + .slice() + .concat({ highFrequency: high, lowFrequency: low }) + .sort((a, b) => a.lowFrequency - b.lowFrequency); + this.props.onChange({ inquiredFrequencyRange: newRanges }); + this.setState({ newLowFreq: undefined, newHighFreq: undefined }); + } + } + + render() { + return ( + + + {this.renderFrequencyTable()} + + + 0} + onChange={(data) => this.setState({ newLowFreq: Number(data) })} + /> + + this.state.newLowFreq} + onChange={(data) => this.setState({ newHighFreq: Number(data) })} + className="upperInline" + /> + + + + + + + ); + } +} diff --git a/src/web/src/app/RatAfc/LocationForm.tsx b/src/web/src/app/RatAfc/LocationForm.tsx new file mode 100644 index 0000000..5f3c5c7 --- /dev/null +++ b/src/web/src/app/RatAfc/LocationForm.tsx @@ -0,0 +1,380 @@ +import * as React from 'react'; +// import ReactTooltip from 'react-tooltip'; +import { Location, LinearPolygon, RadialPolygon, Ellipse, Elevation } from '../Lib/RatAfcTypes'; +import { + GalleryItem, + FormGroup, + InputGroup, + FormSelect, + FormSelectOption, + TextInput, + InputGroupText, +} from '@patternfly/react-core'; +import { ifNum } from '../Lib/Utils'; +import { PairList } from './PairList'; +import { ElevationForm } from './ElevationForm'; + +/** + * LocationForm.tsx: Form component for location object in AP-AFC request + * author: Sam Smucny + */ + +/** + * Parameters for location form component + */ +export interface LocationFormParams { + location: { + ellipse?: Ellipse; + linearPolygon?: LinearPolygon; + radialPolygon?: RadialPolygon; + elevation?: Elevation; + }; + onChange: (val: { + ellipse?: Ellipse; + linearPolygon?: LinearPolygon; + radialPolygon?: RadialPolygon; + elevation?: Elevation; + }) => void; +} + +/** + * possible location types + */ +type LocationType = 'Ellipse' | 'Linear Polygon' | 'Radial Polygon'; + +/** + * enumerated location types + */ +const locationTypes: LocationType[] = ['Ellipse', 'Linear Polygon', 'Radial Polygon']; + +/** + * LocationForm component + */ +export class LocationForm extends React.PureComponent { + private lastType?: LocationType; + private outerBoundaryText?: string; + + constructor(props: LocationFormParams) { + super(props); + if (this.props.location.ellipse) { + this.lastType = 'Ellipse'; + } else if (this.props.location.linearPolygon) { + this.lastType = 'Linear Polygon'; + } else if (this.props.location.radialPolygon) { + this.lastType = 'Radial Polygon'; + } + } + + private setFormDisplay(dispType: 'Ellipse' | 'Linear Polygon' | 'Radial Polygon') { + if (dispType === 'Ellipse') { + this.props.onChange({ + ellipse: { + center: { latitude: undefined, longitude: undefined }, + majorAxis: undefined, + minorAxis: undefined, + orientation: undefined, + }, + linearPolygon: undefined, + radialPolygon: undefined, + elevation: this.props.location.elevation, + }); + } else if (dispType === 'Linear Polygon') { + this.props.onChange({ + ellipse: undefined, + linearPolygon: { + outerBoundary: [], + }, + radialPolygon: undefined, + elevation: this.props.location.elevation, + }); + } else if (dispType === 'Radial Polygon') { + this.props.onChange({ + ellipse: undefined, + linearPolygon: undefined, + radialPolygon: { + center: { latitude: undefined, longitude: undefined }, + outerBoundary: [], + }, + elevation: this.props.location.elevation, + }); + } + } + + private updateEllipse(ellipse: Partial) { + let ellipseCopy: Ellipse = this.props.location.ellipse || ({} as Ellipse); + Object.assign(ellipseCopy, ellipse); + this.props.onChange({ + ellipse: ellipseCopy, + elevation: this.props.location.elevation, + }); + } + + private updateLinearPoly(linearPoly: Partial) { + let linearCopy: LinearPolygon = this.props.location.linearPolygon || ({} as LinearPolygon); + Object.assign(linearCopy, linearPoly); + this.props.onChange({ + linearPolygon: linearCopy, + elevation: this.props.location.elevation, + }); + } + + private updateRadialPoly(radialPoly: Partial) { + let radialCopy: RadialPolygon = this.props.location.radialPolygon || ({} as RadialPolygon); + Object.assign(radialCopy, radialPoly); + this.props.onChange({ + radialPolygon: radialCopy, + elevation: this.props.location.elevation, + }); + } + + private updateElevation(elevation: Partial) { + let elevationCopy = this.props.location.elevation || ({} as Elevation); + Object.assign(elevationCopy, elevation); + this.props.onChange({ + ellipse: this.props.location.ellipse, + linearPolygon: this.props.location.linearPolygon, + radialPolygon: this.props.location.radialPolygon, + elevation: elevationCopy, + }); + } + + render() { + let locType: LocationType | undefined; + if (this.props.location.ellipse) { + locType = 'Ellipse'; + } else if (this.props.location.linearPolygon) { + locType = 'Linear Polygon'; + } else if (this.props.location.radialPolygon) { + locType = 'Radial Polygon'; + } else { + locType = undefined; + } + const ellipse: Ellipse | undefined = this.props.location.ellipse; + const linearPoly: LinearPolygon | undefined = this.props.location.linearPolygon; + const radialPolygon: RadialPolygon | undefined = this.props.location.radialPolygon; + + return ( + <> + + + + this.setFormDisplay(x)} + id="horzontal-form-location-type" + name="horizontal-form-location-type" + style={{ textAlign: 'right' }} + > + + {locationTypes.map((option) => ( + + ))} + + + + + {locType === 'Ellipse' && ellipse && ( + <> + + + + + ifNum(x, (n) => + this.updateEllipse({ center: { latitude: n, longitude: ellipse.center.longitude } }), + ) + } + type="number" + step="any" + id="horizontal-form-latitude" + name="horizontal-form-latitude" + isValid={ + ellipse.center.latitude !== undefined && + ellipse.center.latitude >= -90 && + ellipse.center.latitude <= 90 + } + style={{ textAlign: 'right' }} + /> + degrees + + + + + + + + ifNum(x, (n) => + this.updateEllipse({ center: { latitude: ellipse.center.latitude, longitude: n } }), + ) + } + type="number" + step="any" + id="horizontal-form-longitude" + name="horizontal-form-longitude" + isValid={ + ellipse.center.longitude !== undefined && + ellipse.center.longitude >= -180 && + ellipse.center.longitude <= 180 + } + style={{ textAlign: 'right' }} + /> + degrees + + + + + + + ifNum(x, (n) => this.updateEllipse({ majorAxis: n }))} + type="number" + step="any" + id="horizontal-form-maj-axis" + name="horizontal-form-maj-axis" + isValid={ellipse.majorAxis >= 0 && ellipse.majorAxis >= ellipse.minorAxis} + style={{ textAlign: 'right' }} + /> + meters + + + + + + + ifNum(x, (n) => this.updateEllipse({ minorAxis: n }))} + type="number" + step="any" + id="horizontal-form-min-axis" + name="horizontal-form-min-axis" + isValid={ellipse.minorAxis >= 0 && ellipse.majorAxis >= ellipse.minorAxis} + style={{ textAlign: 'right' }} + /> + meters + + + + + + + ifNum(x, (n) => this.updateEllipse({ orientation: n }))} + type="number" + step="any" + id="horizontal-form-orientation" + name="horizontal-form-orientation" + isValid={ellipse.orientation >= 0 && ellipse.orientation < 180} + style={{ textAlign: 'right' }} + /> + degrees + + + + + )} + {locType === 'Linear Polygon' && linearPoly && ( + + n >= -90 && n <= 90} + secondLabel="Longitude" + sndValid={(n) => n >= -180 && n <= 180} + values={linearPoly.outerBoundary.map((point) => [point.latitude, point.longitude])} + onChange={(values) => + this.updateLinearPoly({ outerBoundary: values.map((p) => ({ latitude: p[0], longitude: p[1] })) }) + } + /> + + )} + {locType === 'Radial Polygon' && radialPolygon && ( + <> + + + + + ifNum(x, (n) => + this.updateRadialPoly({ center: { latitude: n, longitude: radialPolygon.center.longitude } }), + ) + } + type="number" + step="any" + id="horizontal-form-latitude" + name="horizontal-form-latitude" + isValid={ + radialPolygon.center.latitude !== undefined && + radialPolygon.center.latitude >= -90 && + radialPolygon.center.latitude <= 90 + } + style={{ textAlign: 'right' }} + /> + degrees + + + + + + + + ifNum(x, (n) => + this.updateRadialPoly({ center: { latitude: radialPolygon.center.latitude, longitude: n } }), + ) + } + type="number" + step="any" + id="horizontal-form-longitude" + name="horizontal-form-longitude" + isValid={ + radialPolygon.center.longitude !== undefined && + radialPolygon.center.longitude >= -180 && + radialPolygon.center.longitude <= 180 + } + style={{ textAlign: 'right' }} + /> + degrees + + + + + n >= 0 && n <= 360} + secondLabel="Radius (m)" + sndValid={(n) => n > 0} + values={radialPolygon.outerBoundary.map((point) => [point.angle, point.length])} + onChange={(values) => + this.updateRadialPoly({ outerBoundary: values.map((p) => ({ angle: p[0], length: p[1] })) }) + } + /> + + + )} + this.updateElevation(x)} /> + + ); + } +} diff --git a/src/web/src/app/RatAfc/PairList.tsx b/src/web/src/app/RatAfc/PairList.tsx new file mode 100644 index 0000000..271f111 --- /dev/null +++ b/src/web/src/app/RatAfc/PairList.tsx @@ -0,0 +1,140 @@ +import * as React from 'react'; +import { Table, TableHeader, TableBody, HeaderCell } from '@patternfly/react-table'; +import { TextInput, Button, InputGroup, FormGroup } from '@patternfly/react-core'; +import { ifNum } from '../Lib/Utils'; +import { Tooltip } from '@patternfly/react-core'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; + +const createHeaderWithTooltip = (text: string, tooltip) => { + return ( + + {text} {createTooltip(tooltip)} + + ); +}; + +const createTooltip = (tooltip: string) => { + return ( + +

    {tooltip}

    + + } + > + +
    + ); +}; + +/** + * PairList.tsx: component for interacting with a list with 2 number columns + * author: Sam Smucny + */ + +/** + * params for pair list component + */ +interface PairListParams { + tableName: string; + firstLabel: string; + secondLabel: string; + firstHelperText: string; + secondHelperText: string; + values: [number, number][]; + fstValid: (x: number) => boolean; + sndValid: (x: number) => boolean; + onChange: (values: [number, number][]) => void; +} + +interface PairListState { + first?: number; + second?: number; +} + +/** + * pair list component for interacting with a list with two number columns + */ +export class PairList extends React.Component { + private rowClickHandler: (event: any, row: { index: number; cells: string[] }) => void; + + constructor(props: PairListParams) { + super(props); + + this.state = {}; + + /** + * rows are removed when clicked + */ + this.rowClickHandler = (_, row) => { + console.log(row); + const valueCopy = this.props.values; + valueCopy.splice(row.index, 1); + this.props.onChange(valueCopy); + }; + } + + /** + * add a row + */ + private addPoint() { + const valueCopy = this.props.values; + if ( + this.state.first !== undefined && + this.props.fstValid(this.state.first) && + this.state.second !== undefined && + this.props.sndValid(this.state.second) + ) { + valueCopy.push([this.state.first, this.state.second]); + this.props.onChange(valueCopy); + } + } + + render() { + const { firstLabel, firstHelperText, secondLabel, secondHelperText } = this.props; + + // Render either the plain column names, or with a tooltips if specified + const columns = [ + firstHelperText ? { title: createHeaderWithTooltip(firstLabel, firstHelperText) } : { title: firstLabel }, + secondHelperText ? { title: createHeaderWithTooltip(secondLabel, secondHelperText) } : { title: secondLabel }, + ]; + + const rows = this.props.values.map((row, i) => ({ index: i, cells: row })); + return ( + <> + + + +
    + + ifNum(x, (n) => this.setState({ first: n }))} + type="number" + step="any" + id="horizontal-form-lat-point" + name="horizontal-form-lat-point" + isValid={this.state.first && this.props.fstValid(this.state.first)} + style={{ textAlign: 'right' }} + /> + ifNum(x, (n) => this.setState({ second: n }))} + type="number" + step="any" + id="horizontal-form-lon-point" + name="horizontal-form-lon-point" + isValid={this.state.second && this.props.sndValid(this.state.second)} + style={{ textAlign: 'right' }} + /> + + + + ); + } +} diff --git a/src/web/src/app/RatAfc/RatAfc.tsx b/src/web/src/app/RatAfc/RatAfc.tsx new file mode 100644 index 0000000..da33c11 --- /dev/null +++ b/src/web/src/app/RatAfc/RatAfc.tsx @@ -0,0 +1,616 @@ +import * as React from 'react'; +import { + PageSection, + Title, + Card, + CardBody, + Alert, + AlertActionCloseButton, + Button, + Modal, + TextArea, +} from '@patternfly/react-core'; +import { spectrumInquiryRequest, downloadMapData, spectrumInquiryRequestByString } from '../Lib/RatAfcApi'; +import { getCacheItem, cacheItem } from '../Lib/RatApi'; +import { ResError, AFCConfigFile, RatResponse, error, ChannelData, GeoJson } from '../Lib/RatApiTypes'; +import { SpectrumDisplayAFC, SpectrumDisplayLineAFC } from '../Components/SpectrumDisplay'; +import { ChannelDisplay, emptyChannels } from '../Components/ChannelDisplay'; +import { JsonRawDisp } from '../Components/JsonRawDisp'; +import { MapContainer, MapProps } from '../Components/MapContainer'; +import DownloadContents from '../Components/DownloadContents'; +import LoadLidarBounds from '../Components/LoadLidarBounds'; +import LoadRasBounds from '../Components/LoadRasBounds'; +import { + AvailableSpectrumInquiryRequest, + AvailableSpectrumInquiryResponse, + AvailableChannelInfo, + Ellipse, + Point, + AvailableSpectrumInquiryResponseMessage, +} from '../Lib/RatAfcTypes'; +import { Timer } from '../Components/Timer'; +import { logger } from '../Lib/Logger'; +import { RatAfcForm } from './RatAfcForm'; +import { clone } from '../Lib/Utils'; +import Measure from 'react-measure'; +import { Limit } from '../Lib/Admin'; +import { rotate, meterOffsetToDeg } from '../Lib/Utils'; +import { hasRole } from '../Lib/User'; + +/** + * RatAfc.tsx: virtual AP page for AP-AFC specification + * author: Sam Smucny + */ + +/** + * Properties for RatAfc + */ +interface RatAfcProps { + afcConfig: RatResponse; + limit: RatResponse; + rulesetIds: RatResponse; +} + +/** + * State for RatAfc + */ +interface RatAfcState { + response?: AvailableSpectrumInquiryResponse; + status?: 'Success' | 'Info' | 'Error'; + err?: ResError; + extraWarning?: string; + extraWarningTitle?: string; + minEirp: number; + maxEirp: number; + width: number; + mapState: MapState; + mapCenter: { + lat: number; + lng: number; + }; + kml?: Blob; + includeMap: boolean; + clickedMapPoint?: Point; + fullJsonResponse?: string; + redChannels?: AvailableChannelInfo[]; + blackChannels?: AvailableChannelInfo[]; + showSendDirect: boolean; // shows the send direct box used for legacy support - turned off (false) for now + sendDirectModalOpen: boolean; + sendDirectValue?: string; +} + +const mapProps: MapProps = { + geoJson: { + type: 'FeatureCollection', + features: [], + }, + center: { + lat: 40, + lng: -100, + }, + mode: 'Point', + zoom: 2, + versionId: 0, +}; + +interface MapState { + val: GeoJson; + text: string; + valid: boolean; + dimensions: { + width: number; + height: number; + }; + isModalOpen: boolean; + versionId: number; +} + +/** + * generates channel data to be displayed from AP-AFC `AvailableChannelInfo` results + * @param channelClasses AP-AFC formatted channels + * @param minEirp min eirp to use for coloring + * @param maxEirp maz eirp to use for coloring + */ +const generateChannelData = ( + channelClasses: AvailableChannelInfo[], + minEirp: number, + maxEirp: number, + blackChannels?: AvailableChannelInfo[], + redChannels?: AvailableChannelInfo[], +): ChannelData[] => { + let channelData = clone(emptyChannels); + channelClasses.forEach((channelClass) => + channelData.forEach((channelGroup) => + channelGroup.channels.forEach((channel) => { + for (let i = 0; i < channelClass.channelCfi.length; i++) { + if (channel.name === String(channelClass.channelCfi[i])) { + channel.maxEIRP = channelClass.maxEirp[i]; + // RAS Exclusion will produce null channel EIRP + if (channel.maxEIRP == null || channel.maxEIRP === undefined) { + channel.color = 'black'; + } else if (channel.maxEIRP >= maxEirp) { + channel.color = 'green'; + } else if (channel.maxEIRP >= minEirp) { + channel.color = 'yellow'; + } else { + channel.color = 'red'; + } + } + } + }), + ), + ); + + if (!!blackChannels) { + blackChannels.forEach((channelClass) => + channelData.forEach((channelGroup) => + channelGroup.channels.forEach((channel) => { + for (let i = 0; i < channelClass.channelCfi.length; i++) { + if (channel.name === String(channelClass.channelCfi[i])) { + channel.color = 'black'; + channel.maxEIRP = undefined; + } + } + }), + ), + ); + } + + if (!!redChannels) { + redChannels.forEach((channelClass) => + channelData.forEach((channelGroup) => + channelGroup.channels.forEach((channel) => { + for (let i = 0; i < channelClass.channelCfi.length; i++) { + if (channel.name === String(channelClass.channelCfi[i])) { + channel.color = 'red'; + channel.maxEIRP = undefined; + } + } + }), + ), + ); + } + + return channelData; +}; + +// There is another rasterizeEllipse that uses the wrong Ellipse type so reimpliment here +export const rasterizeEllipse = (e: Ellipse, n: number): [number, number][] => { + const omega = (2 * Math.PI) / n; // define the angle increment in angle to use in raster + return Array(n + 1) + .fill(undefined) + .map((_, i) => { + const alpha = omega * i; // define angle to transform for this step + // use ellipse parameterization to generate offset point before rotation + return [e.majorAxis * Math.sin(alpha), e.minorAxis * Math.cos(alpha)] as [number, number]; + }) + .map(rotate((e.orientation * Math.PI) / 180)) // rotate offset in meter coordinates by orientation (- sign gives correct behavior with sin/cos) + .map(meterOffsetToDeg(e.center.latitude)) // transform offset point in meter coordinates to degree offset + .map(([dLng, dLat]) => [dLng + e.center.longitude, dLat + e.center.latitude]); // add offset to center point +}; + +/** + * RatAfc component class + */ +export class RatAfc extends React.Component { + constructor(props: RatAfcProps) { + super(props); + + if (props.afcConfig.kind === 'Error') { + this.state = { + width: 500, + minEirp: 0, + maxEirp: 30, + extraWarning: undefined, + extraWarningTitle: undefined, + status: hasRole('AP') ? 'Error' : undefined, + err: hasRole('AP') ? error('AFC config was not loaded properly. Try refreshing the page.') : undefined, + mapState: { + isModalOpen: false, + val: mapProps.geoJson, + text: '', + valid: false, + dimensions: { width: 0, height: 0 }, + versionId: 0, + }, + mapCenter: { + lat: 40, + lng: -100, + }, + includeMap: false, + clickedMapPoint: { + latitude: 40, + longitude: -100, + }, + sendDirectModalOpen: false, + showSendDirect: false, + }; + } else { + this.state = { + width: 500, + minEirp: props.afcConfig.result.minEIRP, + maxEirp: props.afcConfig.result.maxEIRP, + extraWarning: undefined, + extraWarningTitle: undefined, + mapState: { + isModalOpen: false, + val: mapProps.geoJson, + text: '', + valid: false, + dimensions: { width: 0, height: 0 }, + versionId: 0, + }, + mapCenter: { + lat: 40, + lng: -100, + }, + includeMap: props.afcConfig.kind === 'Success' ? props.afcConfig.result.enableMapInVirtualAp ?? false : false, + clickedMapPoint: { + latitude: 40, + longitude: -100, + }, + sendDirectModalOpen: false, + showSendDirect: false, + }; + } + this.changeMapLocationChild = React.createRef(); + } + + private changeMapLocationChild: any; + + private styles: Map = new Map([ + ['BLDB', { fillOpacity: 0, strokeColor: 'blue' }], + ['RLAN', { strokeColor: 'blue', fillColor: 'lightblue' }], + ]); + + componentDidMount() { + const st = getCacheItem('ratAfcCache') as RatAfcState; + if (st !== undefined) { + st.includeMap = + this.props.afcConfig.kind === 'Success' ? this.props.afcConfig.result.enableMapInVirtualAp ?? false : false; + if (st.mapState.val.features.length <= 646) { + this.setState(st); + } else { + this.setState({ + ...st, + mapState: { + isModalOpen: false, + val: mapProps.geoJson, + text: '', + valid: false, + dimensions: { width: 0, height: 0 }, + versionId: 0, + }, + extraWarningTitle: 'Google Map API Error', + extraWarning: + 'Due to a limitation of the Google Maps API, map data could not be saved. Run again to see map data.', + }); + } + } + } + + componentWillUnmount() { + // before removing object, let's cache the state in case we want to come back + const state = this.state; + cacheItem('ratAfcCache', state); + } + + private setMapState(obj: any) { + this.setState({ mapState: Object.assign(this.state.mapState, obj) }); + } + + private setKml(kml: Blob) { + this.setState({ kml: kml }); + } + + //Leaving this in for later addition of marker move functionality + private onMarkerUpdate(lat: number, lon: number) { + var newGeoJson = { ...this.state.mapState.val }; + + this.setState({ clickedMapPoint: { latitude: lat, longitude: lon } }); + this.setMapState({ val: newGeoJson, versionId: this.state.mapState.versionId + 1 }); + this.changeMapLocationChild.current.setEllipseCenter({ latitude: lat, longitude: lon }); + } + + /** + * make a request to AFC Engine + * @param request request to send + */ + private sendRequest = async (request: AvailableSpectrumInquiryRequest) => { + // make api call + this.setState({ status: 'Info' }); + const rlanLoc = this.getLatLongFromRequest(request); + this.setState({ + mapCenter: rlanLoc, + clickedMapPoint: { latitude: rlanLoc.lat, longitude: rlanLoc.lng }, + }); + try { + const resp = await spectrumInquiryRequest(request); + return this.processResponse(resp, request, rlanLoc); + } catch (error) { + this.setState({ + status: 'Error', + err: { description: 'Unable to sumbit request: ' + error, kind: 'Error', body: error }, + }); + } + }; + + private getLatLongFromRequest(request: AvailableSpectrumInquiryRequest): { lat: number; lng: number } | undefined { + if (request.location.ellipse) { + return { lat: request.location.ellipse.center.latitude, lng: request.location.ellipse.center.longitude }; + } else if (request.location.linearPolygon) { + return { + lat: + request.location.linearPolygon.outerBoundary.map((x) => x.latitude).reduce((a, b) => a + b) / + request.location.linearPolygon.outerBoundary.length, + lng: + request.location.linearPolygon.outerBoundary.map((x) => x.longitude).reduce((a, b) => a + b) / + request.location.linearPolygon.outerBoundary.length, + }; + } else if (request.location.radialPolygon) { + return { + lat: request.location.radialPolygon.center.latitude, + lng: request.location.radialPolygon.center.longitude, + }; + } else return undefined; + } + + private processResponse( + resp: RatResponse, + request?: AvailableSpectrumInquiryRequest, + rlanLoc?, + ) { + if (resp.kind == 'Success') { + const response = resp.result.availableSpectrumInquiryResponses[0]; + if (response.response.responseCode === 0) { + const minEirp = request?.minDesiredPower || this.state.minEirp; + + if (!!rlanLoc) { + this.setState({ + status: 'Success', + response: response, + mapCenter: rlanLoc, + clickedMapPoint: { latitude: rlanLoc.lat, longitude: rlanLoc.lng }, + minEirp: minEirp, + fullJsonResponse: JSON.stringify(resp.result, (k, v) => (k == 'kmzFile' ? 'Removed binary data' : v), 2), + }); + } else { + this.setState({ + status: 'Success', + response: response, + minEirp: minEirp, + fullJsonResponse: JSON.stringify(resp.result, (k, v) => (k == 'kmzFile' ? 'Removed binary data' : v), 2), + }); + } + + if ( + response.vendorExtensions && + response.vendorExtensions.length > 0 && + response.vendorExtensions.findIndex((x) => x.extensionId == 'openAfc.redBlackData') >= 0 + ) { + let extraChannels = response.vendorExtensions.find((x) => x.extensionId == 'openAfc.redBlackData').parameters; + this.setState({ + redChannels: extraChannels['redChannelInfo'], + blackChannels: extraChannels['blackChannelInfo'], + }); + } else { + this.setState({ redChannels: undefined, blackChannels: undefined }); + } + + if ( + this.state.includeMap && + response.vendorExtensions && + response.vendorExtensions.length > 0 && + response.vendorExtensions.findIndex((x) => x.extensionId == 'openAfc.mapinfo') >= 0 + ) { + //Get the KML file and load it into the state.kml parameters; get the GeoJson if present + let kml_filename = response.vendorExtensions.find((x) => x.extensionId == 'openAfc.mapinfo').parameters[ + 'kmzFile' + ]; + let geoJson_filename = response.vendorExtensions.find((x) => x.extensionId == 'openAfc.mapinfo').parameters[ + 'geoJsonFile' + ]; + this.setKml(kml_filename); + let geojson = JSON.parse(geoJson_filename); + if (request?.location.ellipse && geojson && geojson.geoJson) { + geojson.geoJson.features.push({ + type: 'Feature', + properties: { + kind: 'RLAN', + FSLonLat: [this.state.mapCenter.lng, this.state.mapCenter.lat], + }, + geometry: { + type: 'Polygon', + coordinates: [rasterizeEllipse(request.location.ellipse, 32)], + }, + }); + this.setMapState({ val: geojson.geoJson, valid: true, versionId: this.state.mapState.versionId + 1 }); + } + } + } + } else if (!resp.kind || resp.kind == 'Error') { + this.setState({ status: 'Error', err: error(resp.description, resp.errorCode, resp.body), response: resp.body }); + } + } + + private showDirectSendModal(b: boolean): void { + this.setState({ sendDirectModalOpen: b }); + } + + /*** + * Sends a json string to an eariler version of the API directly (not using the controls on the page to + * generate the values). Used to support legacy values when/if supported during transitions. Uses the + * showSendDirect state property to hide/show + */ + private sendDirect(jsonString: string | undefined) { + if (!!jsonString) { + this.setState({ sendDirectModalOpen: false, status: 'Info' }); + return spectrumInquiryRequestByString('1.3', jsonString) + .then((resp) => this.processResponse(resp)) + .catch((error) => + this.setState({ + status: 'Error', + err: { description: 'Unable to sumbit request: ' + error, kind: 'Error', body: error }, + }), + ); + } + } + + render() { + return ( + + AFC AP + + + this.sendRequest(req)} + ellipseCenterPoint={!this.state.clickedMapPoint ? undefined : this.state.clickedMapPoint} + rulesetIds={this.props.rulesetIds.kind == 'Success' ? this.props.rulesetIds.result : ['No rulesets']} + /> + {this.state.showSendDirect && ( + <> + + this.showDirectSendModal(false)} + actions={[ + , + , + ]} + > + + + + )} + + +
    + {this.state.status === 'Success' && ( + + {'Request completed successfully.'} + + )} + {this.state.extraWarning && ( + this.setState({ extraWarning: undefined, extraWarningTitle: undefined })} + /> + } + > +
    {this.state.extraWarning}
    +
    + )} + {this.state.status === 'Info' && ( + + {'Your request has been submitted. '} + + + )} + {this.state.status === 'Error' && ( + +
    {this.state.err?.description}
    + {this.state.err?.body?.response?.supplementalInfo && ( +
    {JSON.stringify(this.state.err?.body?.response?.supplementalInfo)}
    + )} +
    + )} + +
    + {this.state.includeMap ? ( + <> + + +
    + {' '} + this.setMapState({ val: data, versionId: this.state.mapState.versionId + 1 })} + /> + this.setMapState({ val: data, versionId: this.state.mapState.versionId + 1 })} + /> + this.onMarkerUpdate(lat, lon)} + markerPosition={{ + lat: this.state.clickedMapPoint.latitude, + lng: this.state.clickedMapPoint.longitude, + }} + geoJson={this.state.mapState.val} + styles={this.styles} + center={mapProps.center} + zoom={mapProps.zoom} + versionId={this.state.mapState.versionId} + /> +
    + {this.state.response?.response && this.state.kml && ( + this.state.kml!} fileName="results.kmz" /> + )} +
    +
    +
    + + ) : ( + <> + )} + + + this.setState({ width: contentRect.bounds!.width })}> + {({ measureRef }) => ( +
    + +
    + )} +
    +
    +
    +
    + + {this.state.response?.availableFrequencyInfo && } +
    + {this.state.response?.availableChannelInfo && } +
    + +
    + ); + } +} diff --git a/src/web/src/app/RatAfc/RatAfcForm.tsx b/src/web/src/app/RatAfc/RatAfcForm.tsx new file mode 100644 index 0000000..71737e9 --- /dev/null +++ b/src/web/src/app/RatAfc/RatAfcForm.tsx @@ -0,0 +1,702 @@ +import * as React from 'react'; +import { + AvailableSpectrumInquiryRequest, + LinearPolygon, + RadialPolygon, + Ellipse, + DeploymentEnum, + Elevation, + CertificationId, + Channels, + FrequencyRange, + Point, + VendorExtension, +} from '../Lib/RatAfcTypes'; +import { logger } from '../Lib/Logger'; +import { + Modal, + Button, + ClipboardCopy, + ClipboardCopyVariant, + Alert, + Gallery, + GalleryItem, + FormGroup, + TextInput, + InputGroup, + InputGroupText, + FormSelect, + FormSelectOption, + ChipGroup, + Chip, + SelectOption, + Tooltip, + TooltipPosition, +} from '@patternfly/react-core'; +import { getCacheItem, cacheItem } from '../Lib/RatApi'; +import { AFCConfigFile, RatResponse } from '../Lib/RatApiTypes'; +import { ifNum } from '../Lib/Utils'; +import { LocationForm } from './LocationForm'; +import { Limit } from '../Lib/Admin'; +import { OperatingClass, OperatingClassIncludeType } from '../Lib/RatAfcTypes'; +import { OperatingClassForm } from '../Components/OperatingClassForm'; +import { InquiredFrequencyForm } from './InquiredFrequencyForm'; +import { hasRole } from '../Lib/User'; +import { VendorExtensionForm } from './VendorExtensionForm'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; + +interface RatAfcFormParams { + onSubmit: (a: AvailableSpectrumInquiryRequest) => Promise; + config: RatResponse; + limit: Limit; + ellipseCenterPoint?: Point; + rulesetIds: string[]; +} + +interface RatAfcFormState { + isModalOpen: boolean; + status?: 'Info' | 'Error'; + message: string; + submitting: boolean; + + location: { + ellipse?: Ellipse; + linearPolygon?: LinearPolygon; + radialPolygon?: RadialPolygon; + elevation?: Elevation; + }; + + serialNumber?: string; + certificationId: CertificationId[]; + newCertificationId?: string; + newCertificationRulesetId?: string; + indoorDeployment?: DeploymentEnum; + minDesiredPower?: number; + inquiredFrequencyRange?: FrequencyRange[]; + newChannel?: number; + + operatingClasses: OperatingClass[]; + + vendorExtensions: VendorExtension[]; +} + +export class RatAfcForm extends React.Component { + constructor(props: RatAfcFormParams) { + super(props); + + this.state = { + isModalOpen: false, + status: props.config.kind === 'Error' && hasRole('AP') ? 'Error' : undefined, + message: props.config.kind === 'Error' && hasRole('AP') ? 'AFC config file could not be loaded' : '', + submitting: false, + location: { + elevation: {}, + ellipse: !props.ellipseCenterPoint + ? undefined + : { center: props.ellipseCenterPoint, majorAxis: 0, minorAxis: 0, orientation: 0 }, + }, + serialNumber: undefined, + certificationId: [], + indoorDeployment: undefined, + minDesiredPower: undefined, + inquiredFrequencyRange: [], + operatingClasses: [ + { + num: 131, + include: OperatingClassIncludeType.None, + }, + + { + num: 132, + include: OperatingClassIncludeType.None, + }, + + { + num: 133, + include: OperatingClassIncludeType.None, + }, + + { + num: 134, + include: OperatingClassIncludeType.None, + }, + { + num: 136, + include: OperatingClassIncludeType.None, + }, + { + num: 137, + include: OperatingClassIncludeType.None, + }, + ], + newCertificationRulesetId: this.props.rulesetIds[0], + vendorExtensions: [], + }; + } + + componentDidMount() { + const st: RatAfcFormState = getCacheItem('ratAfcFormCache'); + if (st !== undefined) { + this.setState(st); + } + } + + componentWillUnmount() { + // before removing object, let's cache the state in case we want to come back + const state = this.state; + cacheItem('ratAfcFormCache', state); + } + + private setConfig(value: string) { + try { + const params = JSON.parse(value) as AvailableSpectrumInquiryRequest; + // set params + const location = params?.location; + const ellipse = location?.ellipse; + const elevation = location?.elevation; + this.setState({ + serialNumber: params?.deviceDescriptor?.serialNumber, + certificationId: params?.deviceDescriptor?.certificationId || [], + location: location, + indoorDeployment: location?.indoorDeployment, + minDesiredPower: params?.minDesiredPower, + operatingClasses: this.toOperatingClassArray(params?.inquiredChannels), + inquiredFrequencyRange: params.inquiredFrequencyRange, + vendorExtensions: params.vendorExtensions, + }); + } catch (e) { + logger.error('Pasted value is not valid JSON {' + value + '}'); + } + } + + deleteCertifcationId(currentCid: string): void { + const copyOfcertificationId = this.state.certificationId.filter((x) => x.id != currentCid); + this.setState({ certificationId: copyOfcertificationId }); + } + addCertificationId(newCertificationId: CertificationId): void { + const copyOfcertificationId = this.state.certificationId.slice(); + copyOfcertificationId.push({ id: newCertificationId.id, rulesetId: newCertificationId.rulesetId }); + this.setState({ + certificationId: copyOfcertificationId, + newCertificationId: '', + newCertificationRulesetId: this.props.rulesetIds[0], + }); + } + + resetCertificationId(newCertificationId: CertificationId): void { + const copyOfcertificationId = [{ id: newCertificationId.id, rulesetId: newCertificationId.rulesetId }]; + this.setState({ + certificationId: copyOfcertificationId, + newCertificationId: '', + newCertificationRulesetId: this.props.rulesetIds[0], + }); + } + + private updateOperatingClass(e: OperatingClass, i: number) { + var opClassesCopy = this.state.operatingClasses.slice(); + opClassesCopy.splice(i, 1, e); + this.setState({ operatingClasses: opClassesCopy }); + } + + private getParamsJSON = () => + ({ + requestId: '0', + deviceDescriptor: { + serialNumber: this.state.serialNumber!, + certificationId: this.state.certificationId!, + }, + location: { + ellipse: this.state.location.ellipse!, + linearPolygon: this.state.location.linearPolygon!, + radialPolygon: this.state.location.radialPolygon!, + elevation: this.state.location.elevation!, + indoorDeployment: this.state.indoorDeployment!, + }, + minDesiredPower: this.state.minDesiredPower!, + vendorExtensions: this.state.vendorExtensions, + inquiredChannels: this.state.operatingClasses + .filter((x) => x.include != OperatingClassIncludeType.None) + .map((x) => this.fromOperatingClass(x)), + + inquiredFrequencyRange: + this.state.inquiredFrequencyRange && this.state.inquiredFrequencyRange.length > 0 + ? this.state.inquiredFrequencyRange + : [], + }) as AvailableSpectrumInquiryRequest; + + private fromOperatingClass(oc: OperatingClass) { + switch (oc.include) { + case OperatingClassIncludeType.None: + return {}; + case OperatingClassIncludeType.All: + return { + globalOperatingClass: oc.num, + }; + case OperatingClassIncludeType.Some: + return { + globalOperatingClass: oc.num, + channelCfi: oc.channels, + }; + default: + break; + } + } + + private toOperatingClassArray(c: Channels[]) { + var empties = [ + { + num: 131, + channels: [], + include: OperatingClassIncludeType.None, + }, + { + num: 132, + channels: [], + include: OperatingClassIncludeType.None, + }, + { + num: 133, + channels: [], + include: OperatingClassIncludeType.None, + }, + { + num: 134, + channels: [], + include: OperatingClassIncludeType.None, + }, + { + num: 136, + channels: [], + include: OperatingClassIncludeType.None, + }, + { + num: 137, + channels: [], + include: OperatingClassIncludeType.None, + }, + ]; + + if (!c) return empties; + + var converted = c.map((v) => this.toOperatingClass(v)); + var convertedClasses = converted.map((x) => x.num); + var emptiesNeeded = empties.filter((x) => !convertedClasses.includes(x.num)); + var merge = converted.concat(emptiesNeeded).sort((a, b) => a.num - b.num); + + return merge; + } + + private toOperatingClass(c: Channels) { + var include = OperatingClassIncludeType.None; + if (c.channelCfi && c.channelCfi.length > 0) { + include = OperatingClassIncludeType.Some; + } else { + include = OperatingClassIncludeType.All; + } + + var oc: OperatingClass = { + num: c.globalOperatingClass, + channels: c.channelCfi || [], + include: include, + }; + return oc; + } + + private copyPasteClick() { + this.setState({ isModalOpen: true }); + } + + /** + * Validates form data + */ + private validInputs() { + return true; + // Per RAT-285, move all error checking to the engine + + // return !!this.state.serialNumber + // && this.state.certificationId && this.state.certificationId.length + // && this.locationValid(this.state.location.ellipse, this.state.location.linearPolygon, this.state.location.radialPolygon) + // && this.state.elevation.height >= 0 + // && this.state.indoorDeployment !== undefined + // && this.state.elevation.verticalUncertainty >= 0 + // && (this.state.minDesiredPower == undefined || this.state.minDesiredPower >= 0) + // && ((this.state.globalOperatingClass >= 0 && Number.isInteger(this.state.globalOperatingClass)) + // || (this.state.minFreqMHz >= 0 && this.state.maxFreqMHz >= 0 && this.state.minFreqMHz <= this.state.maxFreqMHz) + // ) && (this.props.limit.enforce ? this.state.minDesiredPower === undefined || + // this.state.minDesiredPower >= this.props.limit.limit : this.state.minDesiredPower === undefined || this.state.minDesiredPower >= 0) + } + + /** + * validates location form data. only one of the location types needs to be present and valid + * @param ellipse ellipse form data + * @param linPoly linear polygon form data + * @param radPoly radial polygon form data + */ + private locationValid(ellipse?: Ellipse, linPoly?: LinearPolygon, radPoly?: RadialPolygon) { + if (ellipse !== undefined) { + return ( + ellipse.center && + ellipse.center.latitude >= -90 && + ellipse.center.latitude <= 90 && + ellipse.center.longitude >= -180 && + ellipse.center.longitude <= 180 && + ellipse.majorAxis >= 0 && + ellipse.minorAxis >= 0 && + ellipse.majorAxis >= ellipse.minorAxis && + ellipse.orientation >= 0 && + ellipse.orientation < 180 + ); + } else if (linPoly !== undefined) { + return linPoly.outerBoundary.length >= 3; + } else if (radPoly !== undefined) { + return ( + radPoly.center && + radPoly.center.latitude >= -90 && + radPoly.center.latitude <= 90 && + radPoly.center.longitude >= -180 && + radPoly.center.longitude <= 180 && + radPoly.outerBoundary.length >= 3 + ); + } else { + return false; + } + } + + /** + * submit form data after validating it to make the api request + */ + private submit() { + console.log('value for validInput' + this.validInputs()); + if (!this.validInputs()) { + this.setState({ status: 'Error', message: 'One or more inputs are invalid.' }); + } else { + this.setState({ status: undefined, message: '' }); + const params = this.getParamsJSON(); + this.props.onSubmit(params).then(() => this.setState({ submitting: false })); + } + } + + setEllipseCenter = (center: Point) => { + if (!!this.state.location.ellipse) { + const location = { ...this.state.location }; + location.ellipse = { + center: center, + majorAxis: location.ellipse.majorAxis, + minorAxis: location.ellipse.minorAxis, + orientation: location.ellipse.orientation, + }; + this.setState({ location: location }); + } + }; + + setVendorExtensions = (newVendorsExtensions: VendorExtension[]) => { + this.setState({ vendorExtensions: newVendorsExtensions }); + }; + + render() { + return ( + <> + this.setState({ isModalOpen: false })} + actions={[ + , + ]} + > + this.setConfig(String(text).trim())} + aria-label="text area" + > + {JSON.stringify(this.getParamsJSON())} + + + + + + {' '} + +

    The following Serial Number and Certification ID pair can be used for any rulesetID:

    +
      +
    • Serial Number=TestSerialNumber
    • + +
    • CertificationId=TestCertificationId
    • +
    +

    + {' '} + Note that this device is certified as an indoor Access Point and as such if in the AP request + message, the indoorDeployment field is set to Unknow or if it is missing, it will be treated as + indoor (i.e. BEL will be applied). If the indoorDeployment field is set to Outdoor, no BEL will be + applied. +

    + + } + > + +
    + {hasRole('Trial') ? ( + + ) : ( + <> + )} + this.setState({ serialNumber: x })} + isValid={!!this.state.serialNumber} + style={{ textAlign: 'right' }} + /> +
    +
    + + } //This is not supported in our version of Patternfly + validated={this.state.certificationId.length > 0 ? 'success' : 'error'} + > + {' '} + +

    The following Serial Number and Certification ID pair can be used for any rulesetID:

    +
      +
    • Serial Number=TestSerialNumber
    • + +
    • CertificationId=TestCertificationId
    • +
    + Note that this device is certified as an indoor Access Point and as such if in the AP request + message, the indoorDeployment field is set to Unknow or if it is missing, it will be treated as + indoor (i.e. BEL will be applied). If the indoorDeployment field is set to Outdoor, no BEL will be + applied. + + } + > + +
    + {hasRole('Trial') ? ( + + ) : ( + <> + )} + + {this.state.certificationId.map((currentCid) => ( + this.deleteCertifcationId(currentCid.id)}> + {currentCid.rulesetId + ' ' + currentCid.id} + + ))} + +
    + {' '} + +
    +
    + this.setState({ newCertificationRulesetId: x })} + type="text" + step="any" + id="horizontal-form-certification-nra" + name="horizontal-form-certification-nra" + style={{ textAlign: 'left' }} + placeholder="Ruleset" + > + {this.props.rulesetIds.map((x) => ( + + ))} + + + + this.setState({ newCertificationId: x })} + type="text" + step="any" + id="horizontal-form-certification-list" + name="horizontal-form-certification-list" + style={{ textAlign: 'left' }} + placeholder="Id" + /> +
    +
    +
    + this.setState({ location: x })} /> + + + + ifNum(x, (n) => this.setState({ minDesiredPower: n }))} + type="number" + step="any" + id="horizontal-form-min-eirp" + name="horizontal-form-min-eirp" + isValid={ + this.props.limit.enforce + ? this.state.minDesiredPower === undefined || this.state.minDesiredPower >= this.props.limit.limit + : this.state.minDesiredPower === undefined || this.state.minDesiredPower >= 0 + } + style={{ textAlign: 'right' }} + /> + dBm + + + + + + + x !== undefined && this.setState({ indoorDeployment: Number.parseInt(x) })} + isValid={this.state.indoorDeployment !== undefined} + id="horizontal-form-indoor-deployment" + name="horizontal-form-indoor-deployment" + > + + + + + + + + + + this.setState({ inquiredFrequencyRange: x.inquiredFrequencyRange })} + > + {/* + + ifNum(x, n => this.setState({ minFreqMHz: n === 0 ? undefined : n }))} + type="number" + step="any" + id="horizontal-form-min-freq" + name="horizontal-form-min-freq" + style={{ textAlign: "right" }} + /> + MHz + + + + + + + ifNum(x, n => this.setState({ maxFreqMHz: n === 0 ? undefined : n }))} + type="number" + step="any" + id="horizontal-form-max-freq" + name="horizontal-form-max-freq" + style={{ textAlign: "right" }} + /> + MHz + + */} + +
    + + + {this.state.operatingClasses.map((e, i) => ( + this.updateOperatingClass(x, i)} + > + ))} + + + + + + +
    + {this.state.status === 'Error' && ( + +
    {this.state.message}
    +
    + )} +
    + <> + {' '} + + + + ); + } +} diff --git a/src/web/src/app/RatAfc/VendorExtensionForm.tsx b/src/web/src/app/RatAfc/VendorExtensionForm.tsx new file mode 100644 index 0000000..aab3237 --- /dev/null +++ b/src/web/src/app/RatAfc/VendorExtensionForm.tsx @@ -0,0 +1,140 @@ +import * as React from 'react'; +// import ReactTooltip from 'react-tooltip'; +import { + GalleryItem, + FormGroup, + InputGroup, + Radio, + TextInput, + InputGroupText, + Select, + SelectOption, + SelectVariant, + CheckboxSelectGroup, + Checkbox, + Gallery, + Button, + Label, + ClipboardCopy, + ClipboardCopyVariant, +} from '@patternfly/react-core'; +import { VendorExtension } from '../Lib/RatAfcTypes'; +/** VendorExtensionForm.tsx - Form component to display and create the VendorExtension object + * + * mgelman 2022-01-04 + */ + +export interface VendorExtensionFormParams { + VendorExtensions: VendorExtension[]; + onChange: (val: VendorExtension[]) => void; +} + +export interface VendorExtensionFormState { + VendorExtensions: VendorExtension[]; + parseValidationMessages: string[]; +} + +export class VendorExtensionForm extends React.PureComponent { + constructor(props: VendorExtensionFormParams) { + super(props); + let len = this.props.VendorExtensions.length; + this.state = { + VendorExtensions: this.props.VendorExtensions, + parseValidationMessages: Array(len).fill(''), + }; + } + + setExtensionId(x: string, idx: number): void { + var newVeList = this.state.VendorExtensions.slice(); + newVeList[idx].extensionId = x; + this.setState({ VendorExtensions: newVeList }, () => { + this.props.onChange(this.state.VendorExtensions); + }); + } + + setParameters(x: string, idx: number): void { + try { + let cleanedString = x.replace(/\s+/g, ' ').trim(); + let newParamObj = JSON.parse(cleanedString); + var newVeList = this.state.VendorExtensions.slice(); + newVeList[idx].parameters = newParamObj; + let msgArray = this.state.parseValidationMessages.slice(); + msgArray[idx] = ''; + this.setState({ VendorExtensions: newVeList, parseValidationMessages: msgArray }, () => { + this.props.onChange(this.state.VendorExtensions); + }); + } catch (err) { + if (err instanceof SyntaxError) { + let msgArray = this.state.parseValidationMessages.slice(); + msgArray[idx] = 'Parameter format not correct: ' + err.message; + this.setState({ parseValidationMessages: msgArray }); + } + } + } + + deleteVendorExtension(idx: number): void { + var newVeList = this.state.VendorExtensions.slice(); + let msgArray = this.state.parseValidationMessages.slice(); + newVeList.splice(idx, 1); + msgArray.splice(idx, 1); + this.setState({ VendorExtensions: newVeList, parseValidationMessages: msgArray }, () => { + this.props.onChange(this.state.VendorExtensions); + }); + } + + addVendorExtension(): void { + let newVe = { extensionId: '', parameters: {} }; + var newVeList = this.state.VendorExtensions.slice(); + let msgArray = this.state.parseValidationMessages.slice(); + newVeList.push(newVe); + msgArray.push(''); + this.setState({ VendorExtensions: newVeList, parseValidationMessages: msgArray }, () => { + this.props.onChange(this.state.VendorExtensions); + }); + } + + render(): React.ReactNode { + return ( + + + {this.state.VendorExtensions.map((ve, idx) => ( + + + this.setExtensionId(x, idx)} + id={'vendor-extension-id-' + idx} + name={'vendor-extension-id-' + idx} + style={{ textAlign: 'left' }} + isValid={!!ve.extensionId} + /> + + + this.setParameters(x as string, idx)} + > + {JSON.stringify(ve.parameters, null, ' ')} + + + + + ))} + + + + ); + } +} diff --git a/src/web/src/app/Replay/Replay.tsx b/src/web/src/app/Replay/Replay.tsx new file mode 100644 index 0000000..64f820f --- /dev/null +++ b/src/web/src/app/Replay/Replay.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { AFCConfigFile, RatResponse } from '../Lib/RatApiTypes'; +import { + CardBody, + PageSection, + Card, + CardHead, + TextInput, + Alert, + AlertActionCloseButton, +} from '@patternfly/react-core'; +import DownloadContents from '../Components/DownloadContents'; +import { exportCache, putAfcConfigFile, importCache, guiConfig } from '../Lib/RatApi'; +import { logger } from '../Lib/Logger'; + +export class Replay extends React.Component { + state = { + data: '', + analysisType: '', + location: '', + response: '', + }; + + constructor(props) { + super(props); + } + + private Replay() { + try { + fetch('../ratapi/v1/replay', { + method: 'GET', + }).then((res) => { + this.setState({ + response: res.headers.get('AnalysisType'), + data: res.json(), + location: this.state.data['location'], + }); + }); + } catch (e) { + this.setState({ response: 'No File Found' }); + } + } + + render() { + return ( + + + Export + + +
    +

    {this.state.response}

    + {console.log(this.state.data)} + {console.log(this.state.location)} + {console.log(this.state.response)} +
    +
    +
    +
    + ); + } +} diff --git a/src/web/src/app/Support/Support.tsx b/src/web/src/app/Support/Support.tsx new file mode 100644 index 0000000..358d0b5 --- /dev/null +++ b/src/web/src/app/Support/Support.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { + PageSection, + Title, + Button, + EmptyState, + EmptyStateVariant, + EmptyStateIcon, + EmptyStateBody, + EmptyStateSecondaryActions, +} from '@patternfly/react-core'; + +// Support.tsx: Empty component for possible future expansion +// author: patternfly seed + +const Support: React.FunctionComponent = () => { + return ; +}; + +export { Support }; diff --git a/src/web/src/app/UserAccount/UserAccount.tsx b/src/web/src/app/UserAccount/UserAccount.tsx new file mode 100644 index 0000000..c2e9d52 --- /dev/null +++ b/src/web/src/app/UserAccount/UserAccount.tsx @@ -0,0 +1,245 @@ +/** + * Portions copyright © 2022 Broadcom. All rights reserved. + * The term "Broadcom" refers solely to the Broadcom Inc. corporate + * affiliate that owns the software below. + * This work is licensed under the OpenAFC Project License, a copy + * of which is included with this software program. + */ + +import * as React from 'react'; +import { + Card, + CardHead, + CardHeader, + CardBody, + PageSection, + FormGroup, + TextInput, + Title, + ActionGroup, + Button, + Alert, + AlertActionCloseButton, + InputGroupText, + InputGroup, + Checkbox, +} from '@patternfly/react-core'; +import { error, success } from '../Lib/RatApiTypes'; +import { getUser, updateUser } from '../Lib/Admin'; +import { logger } from '../Lib/Logger'; +import { UserContext, UserState, isAdmin, isEditCredential } from '../Lib/User'; + +// UserAccount.tsx: Page where user properties can be modified +// author: Sam Smucny + +interface UserAccountProps { + userId: number; + onSuccess?: () => void; +} + +interface UserAccountState { + userId: number; + email?: string; + password?: string; + passwordConf?: string; + active: boolean; + messageType?: 'danger' | 'success'; + messageValue: string; + editCredential: boolean; +} + +export class UserAccount extends React.Component { + constructor(props: UserAccountProps) { + super(props); + + this.state = { + userId: 0, + active: false, + messageValue: '', + email: '', + password: '', + passwordConf: '', + editCredential: false, + }; + + getUser(props.userId).then((res) => + res.kind === 'Success' + ? this.setState({ + email: res.result.email, + active: res.result.active, + messageType: undefined, + messageValue: '', + editCredential: isEditCredential(), + } as UserAccountState) + : this.setState({ + messageType: 'danger', + messageValue: res.description, + }), + ); + } + + private upperLower = (p: string) => /[A-Z]/.test(p) && /[a-z]/.test(p); + private hasSymbol = (p: string) => /[-!$%^&*()_+|~=`{}\[\]:";'<>@#\)\(\{\}?,.\/\\]/.test(p); + + private validEmail = (e?: string) => { + if (!this.state.editCredential) return true; + return !!e && /(\w+)@(\w+).(\w+)/.test(e); + }; + + private validPass = (p?: string) => { + if (!this.state.editCredential) return true; + if (!p) return false; + if (p.length < 8) return false; + if (!this.upperLower(p)) return false; + if (!/\d/.test(p)) return false; + if (!this.hasSymbol(p)) return false; + return true; + }; + + private validPassConf = (p?: string) => { + if (!this.state.editCredential) return true; + return p === this.state.password; + }; + + private updateUser = () => { + if (this.state.editCredential) { + if (!this.validEmail(this.state.email)) { + this.setState({ messageType: 'danger', messageValue: 'Invalid email' }); + return; + } + if (!this.validPass(this.state.password)) { + this.setState({ + messageType: 'danger', + messageValue: + 'Invalid password:\n Password must contain: minimum 8 characters, a number, upper and lower case letters, and a special character.', + }); + return; + } + if (!this.validPassConf(this.state.passwordConf)) { + this.setState({ messageType: 'danger', messageValue: 'Passwords must match' }); + return; + } + } + + logger.info('Editing user: ', this.state.userId); + updateUser({ + id: this.props.userId, + email: this.state.email!, + active: this.state.active, + password: this.state.password!, + editCredential: this.state.editCredential, + }).then((res) => { + if (res.kind === 'Success') { + if (this.props.onSuccess) { + this.props.onSuccess(); + return; + } + this.setState({ messageType: 'success', messageValue: 'User updated' }); + } else { + this.setState({ messageType: 'danger', messageValue: res.description }); + } + }); + }; + + render = () => { + return ( + + + {this.state.messageType && ( + <> + this.setState({ messageType: undefined })} />} + /> +
    + + )} + {this.state.editCredential ? ( + + + this.setState({ email: x })} + type="email" + id="form-email" + name="form-email" + isValid={this.validEmail(this.state.email)} + /> + + + ) : ( + + + + + + )} + {this.state.editCredential && ( + + + this.setState({ password: x })} + type="password" + id="form-pass" + name="form-pass" + isValid={this.validPass(this.state.password)} + /> + + + )} + + {this.state.editCredential && ( + + + this.setState({ passwordConf: x })} + type="password" + id="form-pass-c" + name="form-pass-c" + isValid={this.validPassConf(this.state.passwordConf)} + /> + + + )} + +
    + + {(user: UserState) => + isAdmin() && ( + <> + this.setState({ active: c })} + /> +
    + + ) + } +
    + +
    +
    + ); + }; +} + +/** + * wrapper for ap list when it is not embedded in another page + */ +export const UserAccountPage = () => ( + + Edit User + + {(u: UserState) => u.data.loggedIn && } + + +); diff --git a/src/web/src/app/app.css b/src/web/src/app/app.css new file mode 100644 index 0000000..7545ee6 --- /dev/null +++ b/src/web/src/app/app.css @@ -0,0 +1,136 @@ +:root { + --myApp-global--spacer--md: 30px; +} + +html, +body, +#root { + height: 100%; +} + +pre { + white-space: pre-wrap; /* Since CSS 2.1 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} + +.app-container { + display: flex; + align-items: center; + justify-content: center; + margin-top: var(--myApp-global--spacer--md); +} + +.notification-container { + flex: 0 1 auto; +} + +.pf-c-page__main { + background: #ededed; +} + +#nav-list-simple { + height: 85hv; +} + +#nav-footer { + padding-left: 10px; + padding-right: 10px; + padding-bottom: 5px; + padding-top: 10px; + color: gray; + height: inherit * 0.1; +} + +.pf-c-tooltip__content { + text-align: left !important; +} + +.slider { + -webkit-appearance: none; + width: 100%; + height: 15px; + border-radius: 5px; + background: #d3d3d3; + outline: none; + opacity: 0.7; + -webkit-transition: 0.2s; + transition: opacity 0.2s; +} + +.slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 25px; + height: 25px; + border-radius: 50%; + background: #004bed; + cursor: pointer; +} + +.slider::-moz-range-thumb { + width: 25px; + height: 25px; + border-radius: 50%; + background: #004bed; + cursor: pointer; +} + +#horizontal-form-certification-nra { + max-width: 50%; + display: inline; +} + +#horizontal-form-certification-list { + max-width: 50%; + display: inline; +} + +.nestedGallery { + border: 1px solid darkgray; +} + +.inquiredChannelsSection { + margin-top: 20px; +} + +.vendorParamsEntry { + width: 600px; +} + +.lowerInline, +.upperInline { + max-width: 40%; + display: inline; +} + +.btnInline { + max-width: 15%; + display: inline; +} + +.button-blue { + background-color: #1b52a7; + color: white; + border: 2px solid #0082ba; +} + +.custom-tooltip { + background-color: white; +} + +#aboutAccordion { + --pf-global--Color--100: white; + --pf-global--BackgroundColor--100: black; + --pf-c-accordion--BackgroundColor: black; + --pf-c-accordion__toggle--hover--BackgroundColor: darkgrey; + --pf-c-accordion__toggle--focus--BackgroundColor: darkgrey; + --pf-c-accordion__toggle--active--BackgroundColor: darkgrey; + --pf-c-accordion__expanded-content--Color: white; +} + +#parametersList { + font-size: large; +} diff --git a/src/web/src/app/app.test.tsx b/src/web/src/app/app.test.tsx new file mode 100644 index 0000000..135b035 --- /dev/null +++ b/src/web/src/app/app.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { App } from '@app/index'; +import { mount, shallow } from 'enzyme'; +import { Button } from '@patternfly/react-core'; +import { GuiConfig } from './Lib/RatApiTypes'; +import { getDefaultAfcConf, guiConfig } from './Lib/RatApi'; + +Object.assign(guiConfig, { + paws_url: '/dummy/paws', + afcconfig_defaults: '/dummy/afc-config', + google_apikey: 'invalid-key', +}); + +describe('App tests', () => { + test('should render default App component', () => { + const view = shallow(); + }); + + it('should render a nav-toggle button', () => { + const wrapper = mount(); + const button = wrapper.find(Button); + expect(button.exists()).toBe(true); + }); + + it('should hide the sidebar when clicking the nav-toggle button', () => { + const wrapper = mount(); + const button = wrapper.find('#nav-toggle').hostNodes(); + expect(wrapper.find('#page-sidebar').hasClass('pf-m-expanded')); + button.simulate('click'); + expect(wrapper.find('#page-sidebar').hasClass('pf-m-collapsed')); + }); +}); diff --git a/src/web/src/app/index.tsx b/src/web/src/app/index.tsx new file mode 100644 index 0000000..46cc881 --- /dev/null +++ b/src/web/src/app/index.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import '@patternfly/react-core/dist/styles/base.css'; +import { PageSection } from '@patternfly/react-core'; +import { HashRouter } from 'react-router-dom'; +import { AppLayout } from '@app/AppLayout/AppLayout'; +import { AppRoutes } from '@app/routes'; +import '@app/app.css'; +import { logger } from './Lib/Logger'; +import { login, UserContext, configure, UserState, retrieveUserData } from './Lib/User'; +import { clone } from './Lib/Utils'; + +// index.tsx: definition of app component +// author: Sam Smucny + +class App extends React.Component<{ conf: Promise }, { isReady: boolean; user: UserState }> { + constructor(props: Readonly<{ conf: Promise }>) { + super(props); + logger.info('configuring user context'); + this.state = { isReady: false, user: { data: { loggedIn: false } } }; + configure( + () => this.state.user, + (x) => this.setState({ user: clone(x) }), + ); + + // when the configuration promise resolves then set the app config and render the app + props.conf.then(async (x) => { + logger.info('configuration loaded'); + await retrieveUserData(); + this.setState({ isReady: true }); + }); + } + + render() { + return ( + // wrap application in user provider to give components access to login info + + + {this.state.isReady ? : } + + + ); + } +} + +export { App }; diff --git a/src/web/src/app/routes.tsx b/src/web/src/app/routes.tsx new file mode 100644 index 0000000..66c02e6 --- /dev/null +++ b/src/web/src/app/routes.tsx @@ -0,0 +1,459 @@ +import * as React from 'react'; +import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { DynamicImport } from './DynamicImport'; +import { NotFound } from './NotFound/NotFound'; +import { Dashboard } from './Dashboard/Dashboard'; +import { PageSection, Card, CardHeader } from '@patternfly/react-core'; +import { + getAfcConfigFile, + getAllowedRanges, + getRegions, + getAboutAfc, + getAboutSiteKey, + getRulesetIds, +} from './Lib/RatApi'; +import { getUlsFiles, getAntennaPatterns, getUlsFilesCsv } from './Lib/FileApi'; +import AppLoginPage from './AppLayout/AppLogin'; +import { UserAccountPage } from './UserAccount/UserAccount'; +import { getUsers, getMinimumEIRP } from './Lib/Admin'; +import { Replay } from './Replay/Replay'; +import { getLastUsedRegionFromCookie } from './Lib/Utils'; +import { hasRole, isLoggedIn } from './Lib/User'; + +/** + * routes.tsx: definition of app routes + * author: Sam Smucny + */ + +// these define dynamic modules which use the DynamicImport module to load routes + +/** + * Helper + */ +const getSupportModuleAsync = () => { + return () => import(/* webpackChunkName: "support" */ './Support/Support'); +}; +const Support = () => { + return ( + + {(Component: any) => { + return Component === null ? ( + + + Loading... + + + ) : ( + + ); + }} + + ); +}; + +const getRatAfcModuleAsync = () => { + return () => import(/* webpackChunkName: "ap-afc" */ './RatAfc/RatAfc'); +}; +const ratAfcResolves = async () => ({ + conf: await getAfcConfigFile(getLastUsedRegionFromCookie()), + limit: await getMinimumEIRP(), + rulesetIds: await getRulesetIds(), +}); + +const RatAfc = () => { + return ( + + {(Component: any, resolve) => { + return Component === null ? ( + + + Loading... + + + ) : ( + + ); + }} + + ); +}; + +const getAboutAfcModuleAsync = () => { + return () => import(/* webpackChunkName: "about" */ './About/About'); +}; + +const ratAboutAfcResolves = async () => ({ + content: await getAboutAfc(), + sitekey: getAboutSiteKey(), +}); + +const About = () => { + return ( + + {(Component: any, resolve) => { + return Component === null ? ( + + + Loading... + + + ) : ( + + ); + }} + + ); +}; + +const getMobileAPModuleAsync = () => { + return () => import(/* webpackChunkName: "mobile-ap" */ './MobileAP/MobileAP'); +}; +const MobileAP = () => { + return ( + + {(Component: any) => { + return Component === null ? ( + + + Loading... + + + ) : ( + + ); + }} + + ); +}; + +const getAfcConfigModuleAsync = () => { + return () => import(/* webpackChunkName: "afcconfig" */ './AFCConfig/AFCConfig'); +}; + +const afcConfigResolves = async () => { + var lastRegFromCookie = getLastUsedRegionFromCookie(); + + return { + conf: await getAfcConfigFile(lastRegFromCookie!), + ulsFiles: await getUlsFiles(), + antennaPatterns: await getAntennaPatterns(), + regions: await getRegions(), + limit: await getMinimumEIRP(), + frequencyBands: await getAllowedRanges(), + }; +}; +const AFCConfig = () => { + return ( + + {(Component: any, resolve) => { + return Component === null ? ( + + + Loading... + + + ) : ( + + ); + }} + + ); +}; + +const getConvertModuleAsync = () => { + return () => import(/* webpackChunkName: "convert" */ './Convert/Convert'); +}; +const convertResolves = async () => ({ + ulsFilesCsv: await getUlsFilesCsv(), +}); +const Convert = () => { + return ( + + {(Component: any, resolve) => { + return Component === null ? ( + + + Loading... + + + ) : ( + + ); + }} + + ); +}; + +const getExclusionZoneModuleAsync = () => { + return () => import(/* webpackChunkName: "exclusionZone" */ './ExclusionZone/ExclusionZone'); +}; +const limitResolves = async () => ({ + limit: await getMinimumEIRP(), +}); +const ExclusionZone = () => { + return ( + + {(Component: any, resolve) => { + return Component === null ? ( + + + Loading... + + + ) : ( + + ); + }} + + ); +}; + +const heatMapResolves = async () => ({ + limit: await getMinimumEIRP(), + rulesetIds: await getRulesetIds(), +}); +const getHeatMapModuleAsync = () => { + return () => import(/* webpackChunkName: "heatMap" */ './HeatMap/HeatMap'); +}; +const HeatMap = () => { + return ( + + {(Component: any, resolve) => { + return Component === null ? ( + + + Loading... + + + ) : ( + + ); + }} + + ); +}; + +const getAdminModuleAsync = () => { + return () => import(/* webpackChunkName: "admin" */ './Admin/Admin'); +}; +const adminResolves = async () => ({ + users: await getUsers(), + limit: await getMinimumEIRP(), + frequencyBands: await getAllowedRanges(), + regions: await getRegions(), +}); +const Admin = () => { + return ( + + {(Component: any, resolve) => { + return Component === null ? ( + + + Loading... + + + ) : ( + + ); + }} + + ); +}; + +const getMTLSModuleAsync = () => { + return () => import(/* webpackChunkName: "mtlsList" */ './MTLS/MTLS'); +}; + +const getDRListModuleAsync = () => { + return () => import(/* webpackChunkName: "drList" */ './DeniedRules/DRList'); +}; + +const MTLSPage = () => { + return ( + + {(Component: any) => { + return Component === null ? ( + + + Loading... + + + ) : ( + + ); + }} + + ); +}; + +const drResolves = async () => ({ + regions: await getRegions(), +}); + +const DRListPage = () => { + return ( + + {(Component: any, resolve) => { + return Component === null ? ( + + + Loading... + + + ) : ( + + ); + }} + + ); +}; + +const getReplayModuleAsync = () => { + return () => import(/* webpackChunkName: "replay" */ './Replay/Replay'); +}; +const ReplayPage = () => { + return ( + + {(Component: any) => { + return Component === null ? ( + + + Loading... + + + ) : ( + + ); + }} + + ); +}; + +export interface IAppRoute { + label: string; + component: React.ComponentType> | React.ComponentType; + icon: any; + exact?: boolean; + path: string; +} + +// definition of app routes +const routes: IAppRoute[] = [ + { + component: Dashboard, // Currently empty + exact: true, + icon: null, + label: 'Dashboard', + path: '/dashboard', + }, + { + component: AppLoginPage, // Currently empty + exact: true, + icon: null, + label: 'Login', + path: '/login', + }, + { + component: Support, // currently empty + exact: true, + icon: null, + label: 'Support', + path: '/support', // not pointed to anywhere + }, + { + component: UserAccountPage, + exact: true, + icon: null, + label: 'Account', + path: '/account', + }, + { + component: About, + exact: true, + icon: null, + label: 'About AFC', + path: '/about', + }, +]; + +const AppRoutes = () => ( + + {routes.map(({ path, exact, component }, idx) => ( + + ))} + + {isLoggedIn() && (hasRole('Trial') || hasRole('AP')) ? ( + + ) : ( + + )} + + {isLoggedIn() && hasRole('AP') ? ( + + ) : ( + + )} + + {isLoggedIn() && (hasRole('AP') || hasRole('Analysis') || hasRole('Admin')) ? ( + + ) : ( + + )} + + {isLoggedIn() && hasRole('Admin') ? ( + + ) : ( + + )} + + {isLoggedIn() && hasRole('Analysis') ? ( + + ) : ( + + )} + + {isLoggedIn() && hasRole('Analysis') ? ( + + ) : ( + + )} + + {isLoggedIn() && hasRole('Admin') ? ( + + ) : ( + + )} + + {isLoggedIn() && hasRole('Admin') ? ( + + ) : ( + + )} + + {isLoggedIn() && hasRole('Admin') ? ( + + ) : ( + + )} + + + + + + + +); + +export { AppRoutes, routes }; diff --git a/src/web/src/index.html b/src/web/src/index.html new file mode 100644 index 0000000..341a7f7 --- /dev/null +++ b/src/web/src/index.html @@ -0,0 +1,15 @@ + + + + + <%= htmlWebpackPlugin.options.title %> + + + + + + + +
    + + diff --git a/src/web/src/index.tsx b/src/web/src/index.tsx new file mode 100644 index 0000000..0d02c47 --- /dev/null +++ b/src/web/src/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { App } from './app/index'; +import { getGuiConfig } from './app/Lib/RatApi'; +import '@patternfly/react-core/dist/styles/base.css'; + +/** + * index.tsx: root react file that is entry point + * author: Sam Smucny + */ + +if (process.env.NODE_ENV !== 'production') { + // tslint:disable-next-line + const axe = require('react-axe'); + axe(React, ReactDOM, 1000); +} + +// Load gui config from server. This returns a promise which is resolved in the App. +const conf = getGuiConfig(); + +ReactDOM.render(, document.getElementById('root') as HTMLElement); diff --git a/src/web/src/typings.d.ts b/src/web/src/typings.d.ts new file mode 100644 index 0000000..83cf402 --- /dev/null +++ b/src/web/src/typings.d.ts @@ -0,0 +1,14 @@ +declare module '*.png'; +declare module '*.jpg'; +declare module '*.jpeg'; +declare module '*.gif'; +declare module '*.svg'; +declare module '*.css'; +declare module '*.wav'; +declare module '*.mp3'; +declare module '*.m4a'; +declare module '*.rdf'; +declare module '*.ttl'; +declare module '*.pdf'; + +declare module 'webdav'; diff --git a/src/web/test-setup.js b/src/web/test-setup.js new file mode 100644 index 0000000..6f413a4 --- /dev/null +++ b/src/web/test-setup.js @@ -0,0 +1,4 @@ +import { configure } from "enzyme"; +import Adapter from "enzyme-adapter-react-16"; + +configure({ adapter: new Adapter() }); diff --git a/src/web/tsconfig.json b/src/web/tsconfig.json new file mode 100644 index 0000000..7464058 --- /dev/null +++ b/src/web/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "rootDir": ".", + "outDir": "dist", + "module": "esnext", + "target": "es5", + "lib": ["es2019", "dom"], + "sourceMap": true, + "jsx": "react", + "moduleResolution": "node", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "resolveJsonModule": true, + "noEmitOnError": true, + "paths": { + "@app/*": ["src/app/*"], + "@assets/*": ["node_modules/@patternfly/react-core/dist/styles/assets/*"] + } + }, + "include": [ + "**/*.ts" + ], + "exclude": ["node_modules"] +} diff --git a/src/web/tslint.json b/src/web/tslint.json new file mode 100644 index 0000000..251b283 --- /dev/null +++ b/src/web/tslint.json @@ -0,0 +1,95 @@ +{ + "extends": [ + "tslint-react", + "tslint-eslint-rules", + "tslint-config-prettier" + ], + "linterOptions": { + "exclude": [ + "**/node_modules/**/*.ts", + "**/PawsApi.ts" + ] + }, + "rules": { + "ordered-imports": [ + false + ], + "interface-name": false, + "max-line-length": [ + 120 + ], + "ban-types": { + "options": [ + [ + "Object", + "Avoid using the `Object` type. Did you mean `object`?" + ], + [ + "Function", + "Avoid using the `Function` type. Prefer a specific function type, like `() => void`." + ], + [ + "Boolean", + "Avoid using the `Boolean` type. Did you mean `boolean`?" + ], + [ + "Number", + "Avoid using the `Number` type. Did you mean `number`?" + ], + [ + "String", + "Avoid using the `String` type. Did you mean `string`?" + ], + [ + "Symbol", + "Avoid using the `Symbol` type. Did you mean `symbol`?" + ] + ] + }, + "jsx-no-lambda": false, + "callable-types": true, + "class-name": true, + "comment-format": { + "options": [ + "check-space" + ] + }, + "import-spacing": true, + "indent": { + "options": [ + "spaces" + ] + }, + "no-debugger": true, + "no-shadowed-variable": true, + "prefer-const": true, + "quotemark": { + "options": [ + "double", + "avoid-escape" + ] + }, + "triple-equals": { + "options": [ + "allow-null-check" + ] + }, + "variable-name": { + "options": [ + "ban-keywords", + "check-format", + "allow-pascal-case" + ] + }, + "whitespace": { + "options": [ + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type", + "check-typecast" + ] + } + } +} \ No newline at end of file diff --git a/src/web/webpack.common.in b/src/web/webpack.common.in new file mode 100644 index 0000000..1b0be02 --- /dev/null +++ b/src/web/webpack.common.in @@ -0,0 +1,122 @@ +const path = require("path"); +const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); + +const BG_IMAGES_DIRNAME = "bgimages"; + +module.exports = { + entry: { + app: "@ADD_DIST_LIB_SOURCECOPY@/src/index.tsx" + }, + module: { + rules: [ + { + test: /\.(tsx|ts)?$/, + include: "@ADD_DIST_LIB_SOURCECOPY@/src", + use: [ + { + loader: "ts-loader", + options: { + transpileOnly: true, + experimentalWatchApi: true, + } + } + ] + }, + { + test: /\.(svg|ttf|eot|woff|woff2)$/, + // only process modules with this loader + // if they live under a "fonts" or "pficon" directory + include: [ + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/patternfly/dist/fonts", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/react-core/dist/styles/assets/fonts", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/react-core/dist/styles/assets/pficon", + ], + use: { + loader: "file-loader", + options: { + // Limit at 50k. larger files emited into separate files + limit: 5000, + outputPath: "fonts", + name: "[name].[ext]", + } + } + }, + { + test: /\.svg$/, + include: input => input.indexOf("background-filter.svg") > 1, + use: [ + { + loader: "url-loader", + options: { + limit: 5000, + outputPath: "svgs", + name: "[name].[ext]", + } + } + ] + }, + { + test: /\.svg$/, + // only process SVG modules with this loader if they live under a "bgimages" directory + // this is primarily useful when applying a CSS background using an SVG + include: input => input.indexOf(BG_IMAGES_DIRNAME) > -1, + use: { + loader: "svg-url-loader", + options: {} + } + }, + { + test: /\.svg$/, + // only process SVG modules with this loader when they don"t live under a "bgimages", + // "fonts", or "pficon" directory, those are handled with other loaders + include: input => ( + (input.indexOf(BG_IMAGES_DIRNAME) === -1) && + (input.indexOf("fonts") === -1) && + (input.indexOf("background-filter") === -1) && + (input.indexOf("pficon") === -1) + ), + use: { + loader: "raw-loader", + options: {} + } + }, + { + test: /\.(jpg|jpeg|png|gif)$/i, + include: [ + "@ADD_DIST_LIB_SOURCECOPY@/src", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/patternfly", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/patternfly/assets", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/react-core/dist/styles/assets/images", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/react-styles/css/assets/images", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly" + ], + use: [ + { + loader: "url-loader", + options: { + limit: 5000, + outputPath: "images", + name: "[name].[ext]", + } + } + ] + } + ] + }, + output: { + filename: "[name].bundle.js", + path: "@CMAKE_CURRENT_BINARY_DIR@/www", + publicPath: "" + }, + resolve: { + extensions: [".ts", ".tsx", ".js"], + plugins: [ + new TsconfigPathsPlugin({ + configFile: "@ADD_DIST_LIB_SOURCECOPY@/tsconfig.json" + }) + ], + symlinks: false, + cacheWithContext: false + }, +}; + diff --git a/src/web/webpack.dev.in b/src/web/webpack.dev.in new file mode 100644 index 0000000..12e3633 --- /dev/null +++ b/src/web/webpack.dev.in @@ -0,0 +1,79 @@ +const path = require("path"); +const merge = require("webpack-merge"); +const common = require("@ADD_DIST_LIB_SOURCECOPY@/webpack.common.js"); +const HardSourceWebpackPlugin = require("hard-source-webpack-plugin"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); + +const HOST = process.env.HOST || "localhost"; +const PORT = process.env.PORT || "9000"; + +module.exports = merge(common, { + mode: "development", + devtool: "source-map", + devServer: { + contentBase: "@CMAKE_CURRENT_BINARY_DIR@", + host: HOST, + port: PORT, + compress: true, + inline: true, + historyApiFallback: true, + hot: true, + overlay: true, + open: true + }, + plugins: [ + new HtmlWebpackPlugin({ + template: "@ADD_DIST_LIB_SOURCECOPY@/src/index.html", + title: 'AFC Development' + }), + new HardSourceWebpackPlugin({ + // Either an absolute path or relative to webpack's options.context. + cacheDirectory: '@CMAKE_CURRENT_BINARY_DIR@/hard-source-cache/[confighash]', + // Either a string of object hash function given a webpack config. + configHash: function(webpackConfig) { + // node-object-hash on npm can be used to build this. + return require('node-object-hash')({sort: false}).hash(webpackConfig); + }, + // Either false, a string, an object, or a project hashing function. + environmentHash: { + root: process.cwd(), + directories: [], + files: ['package-lock.json', 'yarn.lock'], + }, + // An object. + info: { + // 'none' or 'test'. + mode: 'none', + // 'debug', 'log', 'info', 'warn', or 'error'. + level: 'debug', + }, + // Clean up large, old caches automatically. + cachePrune: { + // Caches younger than `maxAge` are not considered for deletion. They must + // be at least this (default: 2 days) old in milliseconds. + maxAge: 2 * 24 * 60 * 60 * 1000, + // All caches together must be larger than `sizeThreshold` before any + // caches will be deleted. Together they must be at least this + // (default: 50 MB) big in bytes. + sizeThreshold: 50 * 1024 * 1024 + }, + }) + ], + module: { + rules: [ + { + test: /\.css$/, + include: [ + "@ADD_DIST_LIB_SOURCECOPY@/src", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/patternfly", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/patternfly", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/react-styles/css", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/react-core/dist/styles/base.css", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/react-core/dist/esm/@patternfly/patternfly", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/react-core" + ], + use: ["style-loader", "css-loader"] + } + ] + } +}); diff --git a/src/web/webpack.prod.in b/src/web/webpack.prod.in new file mode 100644 index 0000000..dc2b89d --- /dev/null +++ b/src/web/webpack.prod.in @@ -0,0 +1,50 @@ +const path = require("path"); +const merge = require("webpack-merge"); +const common = require("@ADD_DIST_LIB_SOURCECOPY@/webpack.common.js"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); +const TerserPlugin = require("terser-webpack-plugin"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); + + +module.exports = merge(common, { + mode: "production", + optimization: { + minimizer: [ + new OptimizeCSSAssetsPlugin({}), + new TerserPlugin({ + parallel: true, + terserOptions: { + mangle: true + } + }) + ], + }, + plugins: [ + new HtmlWebpackPlugin({ + template: "@ADD_DIST_LIB_SOURCECOPY@/src/index.html", + title: "AFC" + }), + new MiniCssExtractPlugin({ + filename: "[name].css", + chunkFilename: "[name].bundle.css" + }) + ], + module: { + rules: [ + { + test: /\.css$/, + include: [ + "@ADD_DIST_LIB_SOURCECOPY@/src", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/patternfly", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/patternfly", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/react-core/dist/styles/base.css", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/react-core/dist/esm/@patternfly/patternfly", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/react-styles/css", + "@ADD_DIST_LIB_SOURCECOPY@/node_modules/@patternfly/react-core" + ], + use: [MiniCssExtractPlugin.loader, "css-loader"] + } + ] + } +}); diff --git a/src/web/yarn.lock b/src/web/yarn.lock new file mode 100644 index 0000000..91bc1f3 --- /dev/null +++ b/src/web/yarn.lock @@ -0,0 +1,10890 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.0.tgz#8c98d4ac29d6f80f28127b1bc50970a72086c5ac" + integrity sha512-AN2IR/wCUYsM+PdErq6Bp3RFTXl8W0p9Nmymm7zkpsCmh+r/YYcckaCGpU8Q/mEKmST19kkGRaG42A/jxOWwBA== + dependencies: + "@babel/highlight" "^7.8.0" + +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + +"@babel/core@^7.1.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.0.tgz#fd273d4faf69cc20ee3ccfd32d42df916bb4a15c" + integrity sha512-3rqPi/bv/Xfu2YzHvBz4XqMI1fKVwnhntPA1/fjoECrSjrhbOCxlTrbVu5gUtr8zkxW+RpkDOa/HCW93gzS2Dw== + dependencies: + "@babel/code-frame" "^7.8.0" + "@babel/generator" "^7.8.0" + "@babel/helpers" "^7.8.0" + "@babel/parser" "^7.8.0" + "@babel/template" "^7.8.0" + "@babel/traverse" "^7.8.0" + "@babel/types" "^7.8.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/generator@^7.4.0", "@babel/generator@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.0.tgz#40a1244677be58ffdc5cd01e22634cd1d5b29edf" + integrity sha512-2Lp2e02CV2C7j/H4n4D9YvsvdhPVVg9GDIamr6Tu4tU35mL3mzOrzl1lZ8ZJtysfZXh+y+AGORc2rPS7yHxBUg== + dependencies: + "@babel/types" "^7.8.0" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.0.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.0.tgz#076edda55d8cd39c099981b785ce53f4303b967e" + integrity sha512-ylY9J6ZxEcjmJaJ4P6aVs/fZdrZVctCGnxxfYXwCnSMapqd544zT8lWK2qI/vBPjE5gS0o2jILnH+AkpsPauEQ== + dependencies: + "@babel/types" "^7.8.0" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.0.0-beta.48", "@babel/helper-plugin-utils@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.0.tgz#59ec882d43c21c544ccb51decaecb306b34a8231" + integrity sha512-+hAlRGdf8fHQAyNnDBqTHQhwdLURLdrCROoWaEQYiQhk2sV9Rhs+GoFZZfMJExTq9HG8o2NX3uN2G90bFtmFdA== + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/helpers@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.8.0.tgz#3d3e6e08febf5edbbf63b1cf64395525aa3ece37" + integrity sha512-srWKpjAFbiut5JoCReZJ098hLqoZ9HufOnKZPggc7j74XaPuQ+9b3RYPV1M/HfjL63lCNd8uI1O487qIWxAFNA== + dependencies: + "@babel/template" "^7.8.0" + "@babel/traverse" "^7.8.0" + "@babel/types" "^7.8.0" + +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/highlight@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.0.tgz#4cc003dc10359919e2e3a1d9459150942913dd1a" + integrity sha512-OsdTJbHlPtIk2mmtwXItYrdmalJ8T0zpVzNAbKSkHshuywj7zb29Y09McV/jQsQunc/nEyHiPV2oy9llYMLqxw== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.0.tgz#54682775f1fb25dd29a93a02315aab29a6a292bb" + integrity sha512-VVtsnUYbd1+2A2vOVhm4P2qNXQE8L/W859GpUHfUcdhX8d3pEKThZuIr6fztocWx9HbK+00/CR0tXnhAggJ4CA== + +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + +"@babel/plugin-syntax-object-rest-spread@^7.0.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.0.tgz#9b37d580d459682364d8602494c69145b394fd4c" + integrity sha512-dt89fDlkfkTrQcy5KavMQPyF2A6tR0kYp8HAnIoQv5hO34iAUffHghP/hMGd7Gf/+uYTmLQO0ar7peX1SUWyIA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.0.tgz#8c81711517c56b3d00c6de706b0fb13dc3531549" + integrity sha512-Z7ti+HB0puCcLmFE3x90kzaVgbx6TRrYIReaygW6EkBEnJh1ajS4/inhF7CypzWeDV3NFl1AfWj0eMtdihojxw== + dependencies: + regenerator-runtime "^0.13.2" + +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/template@^7.4.0", "@babel/template@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.0.tgz#a32f57ad3be89c0fa69ae87b53b4826844dc6330" + integrity sha512-0NNMDsY2t3ltAVVK1WHNiaePo3tXPUeJpCX4I3xSKFoEl852wJHG8mrgHVADf8Lz1y+8al9cF7cSSfzSnFSYiw== + dependencies: + "@babel/code-frame" "^7.8.0" + "@babel/parser" "^7.8.0" + "@babel/types" "^7.8.0" + +"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.8.0": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.0.tgz#1a2039a028057a2c888b668d94c98e61ea906e7f" + integrity sha512-1RF84ehyx9HH09dMMwGWl3UTWlVoCPtqqJPjGuC4JzMe1ZIVDJ2DT8mv3cPv/A7veLD6sgR7vi95lJqm+ZayIg== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + +"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + +"@cnakazawa/watch@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" + integrity sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA== + dependencies: + exec-sh "^0.3.2" + minimist "^1.2.0" + +"@emotion/babel-utils@^0.6.4": + version "0.6.10" + resolved "https://registry.yarnpkg.com/@emotion/babel-utils/-/babel-utils-0.6.10.tgz#83dbf3dfa933fae9fc566e54fbb45f14674c6ccc" + integrity sha512-/fnkM/LTEp3jKe++T0KyTszVGWNKPNOUJfjNKLO17BzQ6QPxgbg3whayom1Qr2oLFH3V92tDymU+dT5q676uow== + dependencies: + "@emotion/hash" "^0.6.6" + "@emotion/memoize" "^0.6.6" + "@emotion/serialize" "^0.9.1" + convert-source-map "^1.5.1" + find-root "^1.1.0" + source-map "^0.7.2" + +"@emotion/hash@^0.6.2", "@emotion/hash@^0.6.6": + version "0.6.6" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.6.6.tgz#62266c5f0eac6941fece302abad69f2ee7e25e44" + integrity sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ== + +"@emotion/memoize@^0.6.1", "@emotion/memoize@^0.6.6": + version "0.6.6" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b" + integrity sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ== + +"@emotion/serialize@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.9.1.tgz#a494982a6920730dba6303eb018220a2b629c145" + integrity sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ== + dependencies: + "@emotion/hash" "^0.6.6" + "@emotion/memoize" "^0.6.6" + "@emotion/unitless" "^0.6.7" + "@emotion/utils" "^0.8.2" + +"@emotion/stylis@^0.7.0": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5" + integrity sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ== + +"@emotion/unitless@^0.6.2", "@emotion/unitless@^0.6.7": + version "0.6.7" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.6.7.tgz#53e9f1892f725b194d5e6a1684a7b394df592397" + integrity sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg== + +"@emotion/utils@^0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc" + integrity sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw== + +"@fortawesome/fontawesome-common-types@^0.2.26": + version "0.2.26" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.26.tgz#6e0b13a752676036f8196f8a1500d53a27b4adc1" + integrity sha512-CcM/fIFwZlRdiWG/25xE/wHbtyUuCtqoCTrr6BsWw7hH072fR++n4L56KPydAr3ANgMJMjT8v83ZFIsDc7kE+A== + +"@fortawesome/free-brands-svg-icons@^5.8.1": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.12.0.tgz#b0c78627f811ac030ee0ac88df376567cf74119d" + integrity sha512-50uCFzVUki3wfmFmrMNLFhOt8dP6YZ53zwR4dK9FR7Lwq1IVHXnSBb8MtGLe3urLJ2sA+CSu7Pc7s3i6/zLxmA== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.26" + +"@jest/console@^24.7.1", "@jest/console@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" + integrity sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ== + dependencies: + "@jest/source-map" "^24.9.0" + chalk "^2.0.1" + slash "^2.0.0" + +"@jest/core@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.9.0.tgz#2ceccd0b93181f9c4850e74f2a9ad43d351369c4" + integrity sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A== + dependencies: + "@jest/console" "^24.7.1" + "@jest/reporters" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-changed-files "^24.9.0" + jest-config "^24.9.0" + jest-haste-map "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-resolve-dependencies "^24.9.0" + jest-runner "^24.9.0" + jest-runtime "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + jest-watcher "^24.9.0" + micromatch "^3.1.10" + p-each-series "^1.0.0" + realpath-native "^1.1.0" + rimraf "^2.5.4" + slash "^2.0.0" + strip-ansi "^5.0.0" + +"@jest/environment@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.9.0.tgz#21e3afa2d65c0586cbd6cbefe208bafade44ab18" + integrity sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ== + dependencies: + "@jest/fake-timers" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + +"@jest/fake-timers@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.9.0.tgz#ba3e6bf0eecd09a636049896434d306636540c93" + integrity sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A== + dependencies: + "@jest/types" "^24.9.0" + jest-message-util "^24.9.0" + jest-mock "^24.9.0" + +"@jest/reporters@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.9.0.tgz#86660eff8e2b9661d042a8e98a028b8d631a5b43" + integrity sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.2" + istanbul-lib-coverage "^2.0.2" + istanbul-lib-instrument "^3.0.1" + istanbul-lib-report "^2.0.4" + istanbul-lib-source-maps "^3.0.1" + istanbul-reports "^2.2.6" + jest-haste-map "^24.9.0" + jest-resolve "^24.9.0" + jest-runtime "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.6.0" + node-notifier "^5.4.2" + slash "^2.0.0" + source-map "^0.6.0" + string-length "^2.0.0" + +"@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714" + integrity sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.1.15" + source-map "^0.6.0" + +"@jest/test-result@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca" + integrity sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA== + dependencies: + "@jest/console" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/istanbul-lib-coverage" "^2.0.0" + +"@jest/test-sequencer@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz#f8f334f35b625a4f2f355f2fe7e6036dad2e6b31" + integrity sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A== + dependencies: + "@jest/test-result" "^24.9.0" + jest-haste-map "^24.9.0" + jest-runner "^24.9.0" + jest-runtime "^24.9.0" + +"@jest/transform@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.9.0.tgz#4ae2768b296553fadab09e9ec119543c90b16c56" + integrity sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^24.9.0" + babel-plugin-istanbul "^5.1.0" + chalk "^2.0.1" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.1.15" + jest-haste-map "^24.9.0" + jest-regex-util "^24.9.0" + jest-util "^24.9.0" + micromatch "^3.1.10" + pirates "^4.0.1" + realpath-native "^1.1.0" + slash "^2.0.0" + source-map "^0.6.1" + write-file-atomic "2.4.1" + +"@jest/types@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" + integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^13.0.0" + +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@mapbox/point-geometry@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2" + integrity sha1-ioP5M1x4YO/6Lu7KJUMyqgru2PI= + +"@nodelib/fs.scandir@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== + dependencies: + "@nodelib/fs.stat" "2.0.3" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + dependencies: + "@nodelib/fs.scandir" "2.1.3" + fastq "^1.6.0" + +"@patternfly/patternfly@2.46.1": + version "2.46.1" + resolved "https://registry.yarnpkg.com/@patternfly/patternfly/-/patternfly-2.46.1.tgz#05e60d8766dc31a9a1f5b04c3ba1b498bc864ca4" + integrity sha512-3lReQMQvedwEhKOcOw7rE3RPRXMtRit+Yj1IOO7fl5EHaZaNqA1/3w9mWNCpx52M+WD8scBkgqtVx74OU7Jemw== + +"@patternfly/react-charts@5.2.9": + version "5.2.9" + resolved "https://registry.yarnpkg.com/@patternfly/react-charts/-/react-charts-5.2.9.tgz#af28e35e6aaf7223262c86a51eeebe7a8451dd39" + integrity sha512-PQBnugQVE6/SGwdCfUDx4UD8pq7oXhutZ5DGHhNSFHCnZcP43YvKdlRVMwSOMrfNqcftttqGo8DrY2lsO5hVJQ== + dependencies: + "@patternfly/patternfly" "2.46.1" + "@patternfly/react-styles" "^3.6.15" + "@patternfly/react-tokens" "^2.7.14" + hoist-non-react-statics "^3.3.0" + lodash "^4.17.15" + victory "^33.0.5" + victory-core "^33.0.1" + victory-legend "^33.0.1" + +"@patternfly/react-core@3.129.3", "@patternfly/react-core@^3.129.3": + version "3.129.3" + resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-3.129.3.tgz#e54e135af9b15a3286ef17acd35997b8fdcf32b8" + integrity sha512-QiTTUqA0y55YbDtzjlzKmZ6pGQqxyCF14TBQFH3rXI2RV8Z4C6HyyILm09BD/D/ITQIhT82dp+6nRY/mQOqlkw== + dependencies: + "@patternfly/react-icons" "^3.14.28" + "@patternfly/react-styles" "^3.6.15" + "@patternfly/react-tokens" "^2.7.14" + emotion "^9.2.9" + exenv "^1.2.2" + focus-trap-react "^4.0.1" + tippy.js "5.1.2" + +"@patternfly/react-icons@3.14.30", "@patternfly/react-icons@^3.14.28": + version "3.14.30" + resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-3.14.30.tgz#86ee33f47cac6cab0b4ee39a29ab782f9dde6491" + integrity sha512-HGtdsht66XKExZ4ew5ZWMXEvDAxQwQxW6sjX8h1diya1VDPpeKU6S1jclDPciSForsNMtfsXoku/jE2N3rUCwg== + dependencies: + "@fortawesome/free-brands-svg-icons" "^5.8.1" + +"@patternfly/react-styles@3.6.15", "@patternfly/react-styles@^3.6.15": + version "3.6.15" + resolved "https://registry.yarnpkg.com/@patternfly/react-styles/-/react-styles-3.6.15.tgz#b58201014570df0eb91984d5cf1d5d58dba11e15" + integrity sha512-9phudtz138QV82o60XvbNkeYPzLgz0DekEeu8cIX2A2yO1WzZbgXL5VPWB8bF/y+9EFyl+w8tu3ReQcvh7ULEw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0-beta.48" + camel-case "^3.0.0" + css "^2.2.3" + cssom "^0.3.4" + cssstyle "^0.3.1" + emotion "^9.2.9" + emotion-server "^9.2.9" + fbjs-scripts "^0.8.3" + fs-extra "^6.0.1" + jsdom "^15.1.0" + relative "^3.0.2" + resolve-from "^4.0.0" + typescript "3.4.5" + +"@patternfly/react-table@2.24.64": + version "2.24.64" + resolved "https://registry.yarnpkg.com/@patternfly/react-table/-/react-table-2.24.64.tgz#32b839d61dddcb1a8d88c5e28cb4b8bbfa53fda5" + integrity sha512-7DVJM5tYXHAH/RZOkkirhlr/GTFTLC45MOaRgvWU2xQzsMlAABya0ESaPBQfdP5Zj1juRQCxn/opANbcCAAH/w== + dependencies: + "@patternfly/patternfly" "2.46.1" + "@patternfly/react-core" "^3.129.3" + "@patternfly/react-icons" "^3.14.28" + "@patternfly/react-styles" "^3.6.15" + "@patternfly/react-tokens" "^2.7.14" + classnames "^2.2.5" + exenv "^1.2.2" + lodash "^4.17.15" + +"@patternfly/react-tokens@2.7.14", "@patternfly/react-tokens@^2.7.14": + version "2.7.14" + resolved "https://registry.yarnpkg.com/@patternfly/react-tokens/-/react-tokens-2.7.14.tgz#88ee42018e92c4bc51cce7e7e606cd3d68853e5e" + integrity sha512-HVa1fe7H4NRRv6lmezpvW2TfIDF7bSbKvhMmCVqBk80Fd3wfLcPhacnWdt6PLWq7WX4dVx7dF7+v4sFh8RczSg== + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@types/anymatch@*": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" + integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== + +"@types/babel__core@^7.1.0": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30" + integrity sha512-8fBo0UR2CcwWxeX7WIIgJ7lXjasFxoYgRnFHUj+hRvKkpiBJbxhdAPTCY6/ZKM0uxANFVzt4yObSLuTiTnazDA== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.1" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.1.tgz#4901767b397e8711aeb99df8d396d7ba7b7f0e04" + integrity sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307" + integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.0.8" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.8.tgz#479a4ee3e291a403a1096106013ec22cf9b64012" + integrity sha512-yGeB2dHEdvxjP0y4UbRtQaSkXJ9649fYCmIdRoul5kfAoGCwxuCbMhag0k3RPfnuh9kPGm8x89btcfDEXdVWGw== + dependencies: + "@babel/types" "^7.3.0" + +"@types/cheerio@*": + version "0.22.15" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.15.tgz#69040ffa92c309beeeeb7e92db66ac3f80700c0b" + integrity sha512-UGiiVtJK5niCqMKYmLEFz1Wl/3L5zF/u78lu8CwoUywWXRr9LDimeYuOzXVLXBMO758fcTdFtgjvqlztMH90MA== + dependencies: + "@types/node" "*" + +"@types/d3-path@*": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.8.tgz#48e6945a8ff43ee0a1ce85c8cfa2337de85c7c79" + integrity sha512-AZGHWslq/oApTAHu9+yH/Bnk63y9oFOMROtqPAtxl5uB6qm1x2lueWdVEjsjjV3Qc2+QfuzKIwIR5MvVBakfzA== + +"@types/d3-shape@*": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.2.tgz#a41d9d6b10d02e221696b240caf0b5d0f5a588ec" + integrity sha512-LtD8EaNYCaBRzHzaAiIPrfcL3DdIysc81dkGlQvv7WQP3+YXV7b0JJTtR1U3bzeRieS603KF4wUo+ZkJVenh8w== + dependencies: + "@types/d3-path" "*" + +"@types/enzyme-adapter-react-16@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.5.tgz#1bf30a166f49be69eeda4b81e3f24113c8b4e9d5" + integrity sha512-K7HLFTkBDN5RyRmU90JuYt8OWEY2iKUn43SDWEoBOXd/PowUWjLZ3Q6qMBiQuZeFYK/TOstaZxsnI0fXoAfLpg== + dependencies: + "@types/enzyme" "*" + +"@types/enzyme@*", "@types/enzyme@3.10.4": + version "3.10.4" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.4.tgz#dd4961042381a7c0f6637ce25fec3f773ce489dd" + integrity sha512-P5XpxcIt9KK8QUH4al4ttfJfIHg6xmN9ZjyUzRSzAsmDYwRXLI05ng/flZOPXrEXmp8ZYiN8/tEXYK5KSOQk3w== + dependencies: + "@types/cheerio" "*" + "@types/react" "*" + +"@types/events@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + +"@types/glob@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + +"@types/google-map-react@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@types/google-map-react/-/google-map-react-1.1.3.tgz#5724fadb0467900d57a13c093fbbd18f5758081c" + integrity sha512-2qF4l8alBNqs0fJOqubhuOpWFHAFOgq2uLfypXRq/5WmbaLeVPvH0d8WIObr7EfUFPBJHeQONB0QVwlfVeOJRw== + dependencies: + "@types/react" "*" + +"@types/history@*": + version "4.7.3" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.3.tgz#856c99cdc1551d22c22b18b5402719affec9839a" + integrity sha512-cS5owqtwzLN5kY+l+KgKdRJ/Cee8tlmQoGQuIE9tWnSmS3JMKzmxo2HIAk2wODMifGwO20d62xZQLYz+RLfXmw== + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" + integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + +"@types/istanbul-lib-report@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#e5471e7fa33c61358dd38426189c037a58433b8c" + integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" + integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + +"@types/jest@24.0.25": + version "24.0.25" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.25.tgz#2aba377824ce040114aa906ad2cac2c85351360f" + integrity sha512-hnP1WpjN4KbGEK4dLayul6lgtys6FPz0UfxMeMQCv0M+sTnzN3ConfiO72jHgLxl119guHgI8gLqDOrRLsyp2g== + dependencies: + jest-diff "^24.3.0" + +"@types/json-schema@^7.0.5": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + +"@types/minimatch@*", "@types/minimatch@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + +"@types/node@*", "@types/node@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.6.tgz#076028d0b0400be8105b89a0a55550c86684ffec" + integrity sha512-Jg1F+bmxcpENHP23sVKkNuU3uaxPnsBMW0cLjleiikFKomJQbsn0Cqk2yDvQArqzZN6ABfBkZ0To7pQ8sLdWDg== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + +"@types/q@^1.5.1": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" + integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== + +"@types/react-dom@16.9.4": + version "16.9.4" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.4.tgz#0b58df09a60961dcb77f62d4f1832427513420df" + integrity sha512-fya9xteU/n90tda0s+FtN5Ym4tbgxpq/hb/Af24dvs6uYnYn+fspaxw5USlw0R8apDNwxsqumdRoCoKitckQqw== + dependencies: + "@types/react" "*" + +"@types/react-measure@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/react-measure/-/react-measure-2.0.5.tgz#c1d304e3cab3a1c393342bf377b040628e6c29a8" + integrity sha512-T1Bpt8FlWbDhoInUaNrjTOiVRpRJmrRcqhFJxLGBq1VjaqBLHCvUPapgdKMWEIX4Oqsa1SSKjtNkNJGy6WAAZg== + dependencies: + "@types/react" "*" + +"@types/react-router-dom@5.1.3": + version "5.1.3" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196" + integrity sha512-pCq7AkOvjE65jkGS5fQwQhvUp4+4PVD9g39gXLZViP2UqFiFzsEpB3PKf0O6mdbKsewSK8N14/eegisa/0CwnA== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.4" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.4.tgz#7d70bd905543cb6bcbdcc6bd98902332054f31a6" + integrity sha512-PZtnBuyfL07sqCJvGg3z+0+kt6fobc/xmle08jBiezLS8FrmGeiGkJnuxL/8Zgy9L83ypUhniV5atZn/L8n9MQ== + dependencies: + "@types/history" "*" + "@types/react" "*" + +"@types/react@*", "@types/react@16.9.17": + version "16.9.17" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.17.tgz#58f0cc0e9ec2425d1441dd7b623421a867aa253e" + integrity sha512-UP27In4fp4sWF5JgyV6pwVPAQM83Fj76JOcg02X5BZcpSu5Wx+fP9RMqc2v0ssBoQIFvD5JdKY41gjJJKmw6Bg== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + +"@types/recharts-scale@*": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/recharts-scale/-/recharts-scale-1.0.0.tgz#348c9220d6d9062c44a9d585d686644a97f7e25d" + integrity sha512-HR/PrCcxYb2YHviTqH7CMdL1TUhUZLTUKzfrkMhxm1HTa5mg/QtP8XMiuSPz6dZ6wecazAOu8aYZ5DqkNlgHHQ== + +"@types/recharts@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@types/recharts/-/recharts-1.8.5.tgz#5436ae742485cd033910c611eeb9e4c13cbf55f7" + integrity sha512-3h/hVNnyOxDyAGhzRpa5y/QBp30vqDw7mUyV0JNoxWnjKM1h8SdgoYru3robamxsXkVa18rz/SVJ+MWDQw8GLg== + dependencies: + "@types/d3-shape" "*" + "@types/react" "*" + "@types/recharts-scale" "*" + +"@types/source-list-map@*": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" + integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== + +"@types/stack-utils@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" + integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== + +"@types/tapable@*": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.5.tgz#9adbc12950582aa65ead76bffdf39fe0c27a3c02" + integrity sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ== + +"@types/uglify-js@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" + integrity sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ== + dependencies: + source-map "^0.6.1" + +"@types/victory@33.1.2": + version "33.1.2" + resolved "https://registry.yarnpkg.com/@types/victory/-/victory-33.1.2.tgz#1fa28e1bef3e62674232cb20e2b966c118d45c96" + integrity sha512-3qymcFLx8hDuHXW6Dx08z4RwUVzBnHeXbFLGXWI+miVEDmlxkvbr4lJ2wxMIcGGj9vNGyQI4g0p7Rweue3Fmiw== + dependencies: + "@types/react" "*" + +"@types/webpack-sources@*": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92" + integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w== + dependencies: + "@types/node" "*" + "@types/source-list-map" "*" + source-map "^0.6.1" + +"@types/webpack@4.41.2": + version "4.41.2" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.2.tgz#c6faf0111de27afdffe1158dac559e447c273516" + integrity sha512-DNMQOfEvwzWRRyp6Wy9QVCgJ3gkelZsuBE2KUD318dg95s9DKGiT5CszmmV58hq8jk89I9NClre48AEy1MWAJA== + dependencies: + "@types/anymatch" "*" + "@types/node" "*" + "@types/tapable" "*" + "@types/uglify-js" "*" + "@types/webpack-sources" "*" + source-map "^0.6.0" + +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^13.0.0": + version "13.0.5" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.5.tgz#18121bfd39dc12f280cee58f92c5b21d32041908" + integrity sha512-CF/+sxTO7FOwbIRL4wMv0ZYLCRfMid2HQpzDRyViH7kSpfoAFiMdGqKIxb1PxWfjtQXQhnQuD33lvRHNwr809Q== + dependencies: + "@types/yargs-parser" "*" + +"@webassemblyjs/ast@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" + integrity sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ== + dependencies: + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/wast-parser" "1.8.5" + +"@webassemblyjs/floating-point-hex-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz#1ba926a2923613edce496fd5b02e8ce8a5f49721" + integrity sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ== + +"@webassemblyjs/helper-api-error@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz#c49dad22f645227c5edb610bdb9697f1aab721f7" + integrity sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA== + +"@webassemblyjs/helper-buffer@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz#fea93e429863dd5e4338555f42292385a653f204" + integrity sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q== + +"@webassemblyjs/helper-code-frame@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz#9a740ff48e3faa3022b1dff54423df9aa293c25e" + integrity sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ== + dependencies: + "@webassemblyjs/wast-printer" "1.8.5" + +"@webassemblyjs/helper-fsm@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz#ba0b7d3b3f7e4733da6059c9332275d860702452" + integrity sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow== + +"@webassemblyjs/helper-module-context@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz#def4b9927b0101dc8cbbd8d1edb5b7b9c82eb245" + integrity sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g== + dependencies: + "@webassemblyjs/ast" "1.8.5" + mamacro "^0.0.3" + +"@webassemblyjs/helper-wasm-bytecode@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz#537a750eddf5c1e932f3744206551c91c1b93e61" + integrity sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ== + +"@webassemblyjs/helper-wasm-section@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz#74ca6a6bcbe19e50a3b6b462847e69503e6bfcbf" + integrity sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + +"@webassemblyjs/ieee754@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz#712329dbef240f36bf57bd2f7b8fb9bf4154421e" + integrity sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.5.tgz#044edeb34ea679f3e04cd4fd9824d5e35767ae10" + integrity sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.5.tgz#a8bf3b5d8ffe986c7c1e373ccbdc2a0915f0cedc" + integrity sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw== + +"@webassemblyjs/wasm-edit@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz#962da12aa5acc1c131c81c4232991c82ce56e01a" + integrity sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/helper-wasm-section" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + "@webassemblyjs/wasm-opt" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + "@webassemblyjs/wast-printer" "1.8.5" + +"@webassemblyjs/wasm-gen@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz#54840766c2c1002eb64ed1abe720aded714f98bc" + integrity sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/ieee754" "1.8.5" + "@webassemblyjs/leb128" "1.8.5" + "@webassemblyjs/utf8" "1.8.5" + +"@webassemblyjs/wasm-opt@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz#b24d9f6ba50394af1349f510afa8ffcb8a63d264" + integrity sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + +"@webassemblyjs/wasm-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz#21576f0ec88b91427357b8536383668ef7c66b8d" + integrity sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-api-error" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/ieee754" "1.8.5" + "@webassemblyjs/leb128" "1.8.5" + "@webassemblyjs/utf8" "1.8.5" + +"@webassemblyjs/wast-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz#e10eecd542d0e7bd394f6827c49f3df6d4eefb8c" + integrity sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/floating-point-hex-parser" "1.8.5" + "@webassemblyjs/helper-api-error" "1.8.5" + "@webassemblyjs/helper-code-frame" "1.8.5" + "@webassemblyjs/helper-fsm" "1.8.5" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/wast-printer@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz#114bbc481fd10ca0e23b3560fa812748b0bae5bc" + integrity sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/wast-parser" "1.8.5" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abab@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" + integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== + +abab@^2.0.3, abab@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-globals@^4.1.0, acorn-globals@^4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" + integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== + dependencies: + acorn "^6.0.1" + acorn-walk "^6.0.1" + +acorn-globals@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" + integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== + dependencies: + acorn "^7.1.1" + acorn-walk "^7.1.1" + +acorn-walk@^6.0.1, acorn-walk@^6.1.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" + integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== + +acorn-walk@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" + integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== + +acorn@^5.5.3: + version "5.7.4" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" + integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== + +acorn@^6.0.1, acorn@^6.0.7, acorn@^6.2.1: + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== + +acorn@^7.1.0, acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +acorn@^8.2.4: + version "8.8.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" + integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +aggregate-error@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" + integrity sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +airbnb-prop-types@^2.15.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz#5287820043af1eb469f5b0af0d6f70da6c52aaef" + integrity sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA== + dependencies: + array.prototype.find "^2.1.0" + function.prototype.name "^1.1.1" + has "^1.0.3" + is-regex "^1.0.4" + object-is "^1.0.1" + object.assign "^4.1.0" + object.entries "^1.1.0" + prop-types "^15.7.2" + prop-types-exact "^1.2.0" + react-is "^16.9.0" + +ajv-errors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" + integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== + +ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.5.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +alphanum-sort@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= + +ansi-colors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" + integrity sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA== + dependencies: + ansi-wrap "^0.1.0" + +ansi-colors@^3.0.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== + +ansi-cyan@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873" + integrity sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM= + dependencies: + ansi-wrap "0.1.0" + +ansi-escapes@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + +ansi-gray@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" + integrity sha1-KWLPVOyXksSFEKPetSRDaGHvclE= + dependencies: + ansi-wrap "0.1.0" + +ansi-html@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" + integrity sha512-JoAxEa1DfP9m2xfB/y2r/aKcwXNlltr4+0QSBC4TrLfcxyvepX2Pv0t/xpgGV5bGsDzCYV8SzjWgyCW0T9yYbA== + +ansi-red@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-red/-/ansi-red-0.1.1.tgz#8c638f9d1080800a353c9c28c8a81ca4705d946c" + integrity sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw= + dependencies: + ansi-wrap "0.1.0" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== + +ansi-regex@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" + integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== + +ansi-regex@^4.0.0, ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-wrap@0.1.0, ansi-wrap@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" + integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +aproba@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-1.1.0.tgz#687c32758163588fef7de7b36fabe495eb1a399a" + integrity sha1-aHwydYFjWI/vfeezb6vklesaOZo= + dependencies: + arr-flatten "^1.0.1" + array-slice "^0.2.3" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.0.1, arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-2.1.0.tgz#20f9eab5ec70f5c7d215b1077b1c39161d292c7d" + integrity sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0= + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= + +array-filter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" + integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-flatten@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-slice@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" + integrity sha1-3Tz7gO15c6dRF82sabC5nshhhvU= + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= + dependencies: + array-uniq "^1.0.1" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +array.prototype.find@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.0.tgz#630f2eaf70a39e608ac3573e45cf8ccd0ede9ad7" + integrity sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.13.0" + +array.prototype.flat@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b" + integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +asn1.js@^4.0.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" + integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1.js@^5.2.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assert@^1.1.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" + integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== + dependencies: + object-assign "^4.1.1" + util "0.10.3" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +async@^2.6.2: + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== + dependencies: + lodash "^4.17.14" + +async@^3.2.3: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.0.tgz#24390e6ad61386b0a747265754d2a17219de862c" + integrity sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A== + +axe-core@^3.3.2: + version "3.4.1" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.4.1.tgz#e42623918bb85b5ef674633852cb9029db0309c5" + integrity sha512-+EhIdwR0hF6aeMx46gFDUy6qyCfsL0DmBrV3Z+LxYbsOd8e1zBaPHa3f9Rbjsz2dEwSBkLw6TwML/CAIIAqRpw== + +axios@^0.19.0: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + +axios@^v1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102" + integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-core@^6.26.0, babel-core@^6.7.2: + version "6.26.3" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" + integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== + dependencies: + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.1" + debug "^2.6.9" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.8" + slash "^1.0.0" + source-map "^0.5.7" + +babel-generator@^6.26.0: + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" + integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.7" + trim-right "^1.0.1" + +babel-helper-builder-react-jsx@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz#39ff8313b75c8b65dceff1f31d383e0ff2a408a0" + integrity sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA= + dependencies: + babel-runtime "^6.26.0" + babel-types "^6.26.0" + esutils "^2.0.2" + +babel-helper-call-delegate@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" + integrity sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340= + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-define-map@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" + integrity sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8= + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-function-name@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" + integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk= + dependencies: + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-get-function-arity@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" + integrity sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0= + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-hoist-variables@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" + integrity sha1-HssnaJydJVE+rbyZFKc/VAi+enY= + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-optimise-call-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" + integrity sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc= + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-replace-supers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" + integrity sha1-v22/5Dk40XNpohPKiov3S2qQqxo= + dependencies: + babel-helper-optimise-call-expression "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI= + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54" + integrity sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw== + dependencies: + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/babel__core" "^7.1.0" + babel-plugin-istanbul "^5.1.0" + babel-preset-jest "^24.9.0" + chalk "^2.4.2" + slash "^2.0.0" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-check-es2015-constants@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" + integrity sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-emotion@^9.2.11: + version "9.2.11" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz#319c005a9ee1d15bb447f59fe504c35fd5807728" + integrity sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@emotion/babel-utils" "^0.6.4" + "@emotion/hash" "^0.6.2" + "@emotion/memoize" "^0.6.1" + "@emotion/stylis" "^0.7.0" + babel-plugin-macros "^2.0.0" + babel-plugin-syntax-jsx "^6.18.0" + convert-source-map "^1.5.0" + find-root "^1.1.0" + mkdirp "^0.5.1" + source-map "^0.5.7" + touch "^2.0.1" + +babel-plugin-istanbul@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854" + integrity sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + find-up "^3.0.0" + istanbul-lib-instrument "^3.3.0" + test-exclude "^5.2.3" + +babel-plugin-jest-hoist@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz#4f837091eb407e01447c8843cbec546d0002d756" + integrity sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw== + dependencies: + "@types/babel__traverse" "^7.0.6" + +babel-plugin-macros@^2.0.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" + integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== + dependencies: + "@babel/runtime" "^7.7.2" + cosmiconfig "^6.0.0" + resolve "^1.12.0" + +babel-plugin-syntax-class-properties@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" + integrity sha1-1+sjt5oxf4VDlixQW4J8fWysJ94= + +babel-plugin-syntax-flow@^6.18.0, babel-plugin-syntax-flow@^6.8.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" + integrity sha1-TDqyCiryaqIM0lmVw5jE63AxDI0= + +babel-plugin-syntax-jsx@^6.18.0, babel-plugin-syntax-jsx@^6.8.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY= + +babel-plugin-syntax-object-rest-spread@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + integrity sha1-/WU28rzhODb/o6VFjEkDpZe7O/U= + +babel-plugin-syntax-trailing-function-commas@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" + integrity sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM= + +babel-plugin-transform-class-properties@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" + integrity sha1-anl2PqYdM9NvN7YRqp3vgagbRqw= + dependencies: + babel-helper-function-name "^6.24.1" + babel-plugin-syntax-class-properties "^6.8.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-arrow-functions@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" + integrity sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoped-functions@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" + integrity sha1-u8UbSflk1wy42OC5ToICRs46YUE= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoping@^6.8.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" + integrity sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8= + dependencies: + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-plugin-transform-es2015-classes@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" + integrity sha1-WkxYpQyclGHlZLSyo7+ryXolhNs= + dependencies: + babel-helper-define-map "^6.24.1" + babel-helper-function-name "^6.24.1" + babel-helper-optimise-call-expression "^6.24.1" + babel-helper-replace-supers "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-computed-properties@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" + integrity sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM= + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-destructuring@^6.8.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" + integrity sha1-mXux8auWf2gtKwh2/jWNYOdlxW0= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-for-of@^6.8.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" + integrity sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-function-name@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" + integrity sha1-g0yJhTvDaxrw86TF26qU/Y6sqos= + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-literals@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" + integrity sha1-T1SgLWzWbPkVKAAZox0xklN3yi4= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-modules-commonjs@^6.8.0: + version "6.26.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" + integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q== + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" + +babel-plugin-transform-es2015-object-super@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" + integrity sha1-JM72muIcuDp/hgPa0CH1cusnj40= + dependencies: + babel-helper-replace-supers "^6.24.1" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-parameters@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" + integrity sha1-V6w1GrScrxSpfNE7CfZv3wpiXys= + dependencies: + babel-helper-call-delegate "^6.24.1" + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-shorthand-properties@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" + integrity sha1-JPh11nIch2YbvZmkYi5R8U3jiqA= + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-spread@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" + integrity sha1-1taKmfia7cRTbIGlQujdnxdG+NE= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-template-literals@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" + integrity sha1-qEs0UPfp+PH2g51taH2oS7EjbY0= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es3-member-expression-literals@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es3-member-expression-literals/-/babel-plugin-transform-es3-member-expression-literals-6.22.0.tgz#733d3444f3ecc41bef8ed1a6a4e09657b8969ebb" + integrity sha1-cz00RPPsxBvvjtGmpOCWV7iWnrs= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es3-property-literals@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es3-property-literals/-/babel-plugin-transform-es3-property-literals-6.22.0.tgz#b2078d5842e22abf40f73e8cde9cd3711abd5758" + integrity sha1-sgeNWELiKr9A9z6M3pzTcRq9V1g= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-flow-strip-types@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf" + integrity sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988= + dependencies: + babel-plugin-syntax-flow "^6.18.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-object-rest-spread@^6.8.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" + integrity sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY= + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.26.0" + +babel-plugin-transform-react-display-name@^6.8.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1" + integrity sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz#840a028e7df460dfc3a2d29f0c0d91f6376e66a3" + integrity sha1-hAoCjn30YN/DotKfDA2R9jduZqM= + dependencies: + babel-helper-builder-react-jsx "^6.24.1" + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g= + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-preset-fbjs@^2.1.2: + version "2.3.0" + resolved "https://registry.yarnpkg.com/babel-preset-fbjs/-/babel-preset-fbjs-2.3.0.tgz#92ff81307c18b926895114f9828ae1674c097f80" + integrity sha512-ZOpAI1/bN0Y3J1ZAK9gRsFkHy9gGgJoDRUjtUCla/129LC7uViq9nIK22YdHfey8szohYoZY3f9L2lGOv0Edqw== + dependencies: + babel-plugin-check-es2015-constants "^6.8.0" + babel-plugin-syntax-class-properties "^6.8.0" + babel-plugin-syntax-flow "^6.8.0" + babel-plugin-syntax-jsx "^6.8.0" + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-plugin-syntax-trailing-function-commas "^6.8.0" + babel-plugin-transform-class-properties "^6.8.0" + babel-plugin-transform-es2015-arrow-functions "^6.8.0" + babel-plugin-transform-es2015-block-scoped-functions "^6.8.0" + babel-plugin-transform-es2015-block-scoping "^6.8.0" + babel-plugin-transform-es2015-classes "^6.8.0" + babel-plugin-transform-es2015-computed-properties "^6.8.0" + babel-plugin-transform-es2015-destructuring "^6.8.0" + babel-plugin-transform-es2015-for-of "^6.8.0" + babel-plugin-transform-es2015-function-name "^6.8.0" + babel-plugin-transform-es2015-literals "^6.8.0" + babel-plugin-transform-es2015-modules-commonjs "^6.8.0" + babel-plugin-transform-es2015-object-super "^6.8.0" + babel-plugin-transform-es2015-parameters "^6.8.0" + babel-plugin-transform-es2015-shorthand-properties "^6.8.0" + babel-plugin-transform-es2015-spread "^6.8.0" + babel-plugin-transform-es2015-template-literals "^6.8.0" + babel-plugin-transform-es3-member-expression-literals "^6.8.0" + babel-plugin-transform-es3-property-literals "^6.8.0" + babel-plugin-transform-flow-strip-types "^6.8.0" + babel-plugin-transform-object-rest-spread "^6.8.0" + babel-plugin-transform-react-display-name "^6.8.0" + babel-plugin-transform-react-jsx "^6.8.0" + +babel-preset-jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz#192b521e2217fb1d1f67cf73f70c336650ad3cdc" + integrity sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg== + dependencies: + "@babel/plugin-syntax-object-rest-spread" "^7.0.0" + babel-plugin-jest-hoist "^24.9.0" + +babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" + integrity sha1-btAhFz4vy0htestFxgCahW9kcHE= + dependencies: + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + home-or-tmp "^2.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + source-map-support "^0.4.15" + +babel-runtime@^6.22.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@^6.24.1, babel-template@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.24.1, babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.24.1, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== + +backbone@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/backbone/-/backbone-1.4.0.tgz#54db4de9df7c3811c3f032f34749a4cd27f3bd12" + integrity sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ== + dependencies: + underscore ">=1.8.3" + +balanced-match@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + integrity sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base-64@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" + integrity sha1-eAqZyE59YAJgNhURxId2E78k9rs= + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +bfj@^6.1.1: + version "6.1.2" + resolved "https://registry.yarnpkg.com/bfj/-/bfj-6.1.2.tgz#325c861a822bcb358a41c78a33b8e6e2086dde7f" + integrity sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw== + dependencies: + bluebird "^3.5.5" + check-types "^8.0.3" + hoopy "^0.1.4" + tryer "^1.0.1" + +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bluebird@^3.5.5: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +bn.js@^4.0.0, bn.js@^4.1.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== + +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.0.0, bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^2.3.1, braces@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +brorand@^1.0.1, brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= + +browser-process-hrtime@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4" + integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw== + +browser-process-hrtime@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" + integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== + +browser-resolve@^1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" + integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== + dependencies: + resolve "1.1.7" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-rsa@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" + integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== + dependencies: + bn.js "^5.0.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.2.tgz#e78d4b69816d6e3dd1c747e64e9947f9ad79bc7e" + integrity sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg== + dependencies: + bn.js "^5.2.1" + browserify-rsa "^4.1.0" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.4" + inherits "^2.0.4" + parse-asn1 "^5.1.6" + readable-stream "^3.6.2" + safe-buffer "^5.2.1" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + dependencies: + pako "~1.0.5" + +browserslist@^4.0.0: + version "4.21.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a" + integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ== + dependencies: + caniuse-lite "^1.0.30001370" + electron-to-chromium "^1.4.202" + node-releases "^2.0.6" + update-browserslist-db "^1.0.5" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@1.x: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-from@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-0.1.2.tgz#15f4b9bcef012044df31142c14333caf6e0260d0" + integrity sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg== + +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= + +buffer@^4.3.0: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cacache@^12.0.2: + version "12.0.3" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.3.tgz#be99abba4e1bf5df461cd5a2c1071fc432573390" + integrity sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw== + dependencies: + bluebird "^3.5.5" + chownr "^1.1.1" + figgy-pudding "^3.5.1" + glob "^7.1.4" + graceful-fs "^4.1.15" + infer-owner "^1.0.3" + lru-cache "^5.1.1" + mississippi "^3.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.3" + ssri "^6.0.1" + unique-filename "^1.1.1" + y18n "^4.0.0" + +cacache@^13.0.1: + version "13.0.1" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-13.0.1.tgz#a8000c21697089082f85287a1aec6e382024a71c" + integrity sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w== + dependencies: + chownr "^1.1.2" + figgy-pudding "^3.5.1" + fs-minipass "^2.0.0" + glob "^7.1.4" + graceful-fs "^4.2.2" + infer-owner "^1.0.4" + lru-cache "^5.1.1" + minipass "^3.0.0" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + p-map "^3.0.0" + promise-inflight "^1.0.1" + rimraf "^2.7.1" + ssri "^7.0.0" + unique-filename "^1.1.1" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= + dependencies: + caller-callsite "^2.0.0" + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camel-case@3.0.x, camel-case@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" + integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= + dependencies: + no-case "^2.2.0" + upper-case "^1.1.1" + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + integrity sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw== + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0: + version "1.0.30001020" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001020.tgz#3f04c1737500ffda78be9beb0b5c1e2070e15926" + integrity sha512-yWIvwA68wRHKanAVS1GjN8vajAv7MBFshullKCeq/eKpK7pJBVDgFFEqvgWTkcP2+wIDeQGYFRXECjKZnLkUjA== + +caniuse-lite@^1.0.30001370: + version "1.0.30001375" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz#8e73bc3d1a4c800beb39f3163bf0190d7e5d7672" + integrity sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw== + +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== + dependencies: + rsvp "^4.8.4" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +check-types@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552" + integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ== + +cheerio@^1.0.0-rc.3: + version "1.0.0-rc.3" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" + integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA== + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.1" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash "^4.15.0" + parse5 "^3.0.1" + +chokidar@^2.0.2, chokidar@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chownr@^1.1.1, chownr@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" + integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== + +chrome-trace-event@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" + integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== + dependencies: + tslib "^1.9.0" + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +classnames@^2.2.5: + version "2.2.6" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" + integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== + +clean-css@4.2.x: + version "4.2.1" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17" + integrity sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g== + dependencies: + source-map "~0.6.0" + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cliui@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +coa@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" + integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== + dependencies: + "@types/q" "^1.5.1" + chalk "^2.4.1" + q "^1.1.2" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0, color-convert@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.5.2: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +color@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" + integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@2.17.x: + version "2.17.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" + integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== + +commander@^2.12.1, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@~2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-stream@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +connect-history-api-fallback@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + +console-browserify@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" + integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1, convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + dependencies: + safe-buffer "~5.1.1" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= + +core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0: + version "2.6.11" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" + integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== + +core-js@^2.6.10: + version "2.6.12" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" + integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cosmiconfig@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.13.1" + parse-json "^4.0.0" + +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + +create-ecdh@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" + integrity sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw== + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-emotion-server@^9.2.12: + version "9.2.12" + resolved "https://registry.yarnpkg.com/create-emotion-server/-/create-emotion-server-9.2.12.tgz#30d82507bfe440bfb3dd6c9b5c8faf24597ee954" + integrity sha512-ET+E6A5MkQTEBNDYAnjh6+0cB33qStFXhtflkZNPEaOmvzYlB/xcPnpUk4J7ul3MVa8PCQx2Ei5g2MGY/y1n+g== + dependencies: + html-tokenize "^2.0.0" + multipipe "^1.0.2" + through "^2.3.8" + +create-emotion@^9.2.12: + version "9.2.12" + resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-9.2.12.tgz#0fc8e7f92c4f8bb924b0fef6781f66b1d07cb26f" + integrity sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA== + dependencies: + "@emotion/hash" "^0.6.2" + "@emotion/memoize" "^0.6.1" + "@emotion/stylis" "^0.7.0" + "@emotion/unitless" "^0.6.2" + csstype "^2.5.2" + stylis "^3.5.0" + stylis-rule-sheet "^0.0.10" + +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@6.0.5, cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +css-color-names@0.0.4, css-color-names@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= + +css-declaration-sorter@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" + integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA== + dependencies: + postcss "^7.0.1" + timsort "^0.3.0" + +css-loader@3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.4.2.tgz#d3fdb3358b43f233b78501c5ed7b1c6da6133202" + integrity sha512-jYq4zdZT0oS0Iykt+fqnzVLRIeiPWhka+7BqPn+oSIpWJAHak5tmB/WZrJ2a21JhCeFyNnnlroSl8c+MtVndzA== + dependencies: + camelcase "^5.3.1" + cssesc "^3.0.0" + icss-utils "^4.1.1" + loader-utils "^1.2.3" + normalize-path "^3.0.0" + postcss "^7.0.23" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^3.0.2" + postcss-modules-scope "^2.1.1" + postcss-modules-values "^3.0.0" + postcss-value-parser "^4.0.2" + schema-utils "^2.6.0" + +css-select-base-adapter@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" + integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== + +css-select@^1.1.0, css-select@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-select@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" + integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== + dependencies: + boolbase "^1.0.0" + css-what "^3.2.1" + domutils "^1.7.0" + nth-check "^1.0.2" + +css-tree@1.0.0-alpha.37: + version "1.0.0-alpha.37" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" + integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== + dependencies: + mdn-data "2.0.4" + source-map "^0.6.1" + +css-what@2.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== + +css-what@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1" + integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw== + +css@^2.2.3: + version "2.2.4" + resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" + integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== + dependencies: + inherits "^2.0.3" + source-map "^0.6.1" + source-map-resolve "^0.5.2" + urix "^0.1.0" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76" + integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA== + dependencies: + css-declaration-sorter "^4.0.1" + cssnano-util-raw-cache "^4.0.1" + postcss "^7.0.0" + postcss-calc "^7.0.1" + postcss-colormin "^4.0.3" + postcss-convert-values "^4.0.1" + postcss-discard-comments "^4.0.2" + postcss-discard-duplicates "^4.0.2" + postcss-discard-empty "^4.0.1" + postcss-discard-overridden "^4.0.1" + postcss-merge-longhand "^4.0.11" + postcss-merge-rules "^4.0.3" + postcss-minify-font-values "^4.0.2" + postcss-minify-gradients "^4.0.2" + postcss-minify-params "^4.0.2" + postcss-minify-selectors "^4.0.2" + postcss-normalize-charset "^4.0.1" + postcss-normalize-display-values "^4.0.2" + postcss-normalize-positions "^4.0.2" + postcss-normalize-repeat-style "^4.0.2" + postcss-normalize-string "^4.0.2" + postcss-normalize-timing-functions "^4.0.2" + postcss-normalize-unicode "^4.0.1" + postcss-normalize-url "^4.0.1" + postcss-normalize-whitespace "^4.0.2" + postcss-ordered-values "^4.1.2" + postcss-reduce-initial "^4.0.3" + postcss-reduce-transforms "^4.0.2" + postcss-svgo "^4.0.2" + postcss-unique-selectors "^4.0.1" + +cssnano-util-get-arguments@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" + integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8= + +cssnano-util-get-match@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" + integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0= + +cssnano-util-raw-cache@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" + integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA== + dependencies: + postcss "^7.0.0" + +cssnano-util-same-parent@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" + integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== + +cssnano@^4.1.0: + version "4.1.10" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2" + integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ== + dependencies: + cosmiconfig "^5.0.0" + cssnano-preset-default "^4.0.7" + is-resolvable "^1.0.0" + postcss "^7.0.0" + +csso@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.2.tgz#e5f81ab3a56b8eefb7f0092ce7279329f454de3d" + integrity sha512-kS7/oeNVXkHWxby5tHVxlhjizRCSv8QdU7hB2FpdAibDU8FjTAolhNjKNTiLzXtUrKT6HwClE81yXwEk1309wg== + dependencies: + css-tree "1.0.0-alpha.37" + +cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0", cssom@^0.3.4, cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssom@^0.4.1, cssom@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" + integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== + +cssstyle@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.3.1.tgz#6da9b4cff1bc5d716e6e5fe8e04fcb1b50a49adf" + integrity sha512-tNvaxM5blOnxanyxI6panOsnfiyLRj3HV4qjqqS45WPNS1usdYWRUQjqTEEELK73lpeP/1KoIGYUwrBn/VcECA== + dependencies: + cssom "0.3.x" + +cssstyle@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1" + integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA== + dependencies: + cssom "0.3.x" + +cssstyle@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.0.0.tgz#911f0fe25532db4f5d44afc83f89cc4b82c97fe3" + integrity sha512-QXSAu2WBsSRXCPjvI43Y40m6fMevvyRm8JVAuF9ksQz5jha4pWP1wpaK7Yu5oLFc6+XAY+hj8YhefyXcBB53gg== + dependencies: + cssom "~0.3.6" + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + +csstype@^2.2.0, csstype@^2.5.2: + version "2.6.8" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.8.tgz#0fb6fc2417ffd2816a418c9336da74d7f07db431" + integrity sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA== + +cyclist@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" + integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= + +d3-array@^1.2.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" + integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== + +d3-collection@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" + integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A== + +d3-color@1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.0.tgz#89c45a995ed773b13314f06460df26d60ba0ecaf" + integrity sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg== + +d3-ease@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.6.tgz#ebdb6da22dfac0a22222f2d4da06f66c416a0ec0" + integrity sha512-SZ/lVU7LRXafqp7XtIcBdxnWl8yyLpgOmzAk0mWBI9gXNzLDx5ybZgnRbH9dN/yY5tzVBqCQ9avltSnqVwessQ== + +d3-format@1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.3.tgz#4e8eb4dff3fdcb891a8489ec6e698601c41b96f1" + integrity sha512-mm/nE2Y9HgGyjP+rKIekeITVgBtX97o1nrvHCWX8F/yBYyevUTvu9vb5pUnKwrcSw7o7GuwMOWjS9gFDs4O+uQ== + +d3-interpolate@1, d3-interpolate@^1.1.1, d3-interpolate@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" + integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== + dependencies: + d3-color "1" + +d3-path@1: + version "1.0.9" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" + integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== + +d3-scale@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d" + integrity sha512-KvU92czp2/qse5tUfGms6Kjig0AhHOwkzXG0+PqIJB3ke0WUv088AHMZI0OssO9NCkXt4RP8yju9rpH8aGB7Lw== + dependencies: + d3-array "^1.2.0" + d3-collection "1" + d3-color "1" + d3-format "1" + d3-interpolate "1" + d3-time "1" + d3-time-format "2" + +d3-scale@^2.1.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f" + integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw== + dependencies: + d3-array "^1.2.0" + d3-collection "1" + d3-format "1" + d3-interpolate "1" + d3-time "1" + d3-time-format "2" + +d3-shape@^1.0.0, d3-shape@^1.2.0: + version "1.3.7" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" + integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== + dependencies: + d3-path "1" + +d3-time-format@2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.2.3.tgz#0c9a12ee28342b2037e5ea1cf0b9eb4dd75f29cb" + integrity sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA== + dependencies: + d3-time "1" + +d3-time@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" + integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== + +d3-timer@^1.0.0: + version "1.0.10" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" + integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== + +d3-voronoi@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297" + integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg== + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +data-uri-to-buffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b" + integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA== + +data-urls@^1.0.0, data-urls@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" + integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== + dependencies: + abab "^2.0.0" + whatwg-mimetype "^2.2.0" + whatwg-url "^7.0.0" + +data-urls@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" + integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== + dependencies: + abab "^2.0.3" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.0.0" + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.1.0, debug@^4.1.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^3.1.1, debug@^3.2.5: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decimal.js-light@^2.4.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.0.tgz#ca7faf504c799326df94b0ab920424fdfc125348" + integrity sha512-b3VJCbd2hwUpeRGG3Toob+CRo8W22xplipNhP3tN7TSVB/cyMX71P1vM2Xjc9H74uV6dS2hDDmo/rHq8L87Upg== + +decimal.js@^10.2.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" + integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== + +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +deep-equal@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +default-gateway@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" + integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== + dependencies: + execa "^1.0.0" + ip-regex "^2.1.0" + +define-properties@^1.1.2, define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +del@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" + integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== + dependencies: + "@types/glob" "^7.1.1" + globby "^6.1.0" + is-path-cwd "^2.0.0" + is-path-in-cwd "^2.0.0" + p-map "^2.0.0" + pify "^4.0.1" + rimraf "^2.6.3" + +delaunator@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-4.0.1.tgz#3d779687f57919a7a418f8ab947d3bddb6846957" + integrity sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag== + +delaunay-find@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/delaunay-find/-/delaunay-find-0.0.5.tgz#5fb37e6509da934881b4b16c08898ac89862c097" + integrity sha512-7yAJ/wmKWj3SgqjtkGqT/RCwI0HWAo5YnHMoF5nYXD8cdci+YSo23iPmgrZUNOpDxRWN91PqxUvMMr2lKpjr+w== + dependencies: + delaunator "^4.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +des.js@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" + integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= + dependencies: + repeating "^2.0.0" + +detect-indent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" + integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= + +detect-newline@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" + integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= + +detect-node@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + +diff-sequences@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" + integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +discontinuous-range@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" + integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo= + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= + +dns-packet@^1.3.1: + version "1.3.4" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f" + integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA== + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= + dependencies: + buffer-indexof "^1.0.0" + +doctrine@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-0.7.2.tgz#7cb860359ba3be90e040b26b729ce4bfa654c523" + integrity sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM= + dependencies: + esutils "^1.1.6" + isarray "0.0.1" + +dom-converter@^0.2: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-helpers@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== + dependencies: + "@babel/runtime" "^7.1.2" + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +dom-serializer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" + +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== + +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" + integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== + +domexception@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== + dependencies: + webidl-conversions "^4.0.2" + +domexception@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" + integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== + dependencies: + webidl-conversions "^5.0.0" + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^1.5.1, domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +dot-prop@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +duplexer2@^0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= + dependencies: + readable-stream "^2.0.2" + +duplexer@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= + +duplexify@^3.4.2, duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +ejs@^2.6.1: + version "2.7.4" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" + integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== + +ejs@^3.1.7: + version "3.1.8" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" + integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ== + dependencies: + jake "^10.8.5" + +electron-to-chromium@^1.4.202: + version "1.4.213" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.213.tgz#a0d0f535e4fbddc25196c91ff2964b5660932297" + integrity sha512-+3DbGHGOCHTVB/Ms63bGqbyC1b8y7Fk86+7ltssB8NQrZtSCvZG6eooSl9U2Q0yw++fL2DpHKOdTU0NVEkFObg== + +elliptic@^6.0.0, elliptic@^6.5.4: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= + +emotion-server@^9.2.9: + version "9.2.12" + resolved "https://registry.yarnpkg.com/emotion-server/-/emotion-server-9.2.12.tgz#aaaaa04843108943d1ce5a796e0bc40b06a3223e" + integrity sha512-Bhjdl7eNoIeiAVa2QPP5d+1nP/31SiO/K1P/qI9cdXCydg91NwGYmteqhhge8u7PF8fLGTEVQfcPwj21815eBw== + dependencies: + create-emotion-server "^9.2.12" + +emotion@^9.2.9: + version "9.2.12" + resolved "https://registry.yarnpkg.com/emotion/-/emotion-9.2.12.tgz#53925aaa005614e65c6e43db8243c843574d1ea9" + integrity sha512-hcx7jppaI8VoXxIWEhxpDW7I+B4kq9RNzQLmsrF6LY8BGKqe2N+gFAQr0EfuFucFlPs2A9HM4+xNj4NeqEWIOQ== + dependencies: + babel-plugin-emotion "^9.2.11" + create-emotion "^9.2.12" + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= + dependencies: + iconv-lite "~0.4.13" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" + integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + tapable "^1.0.0" + +enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66" + integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + +entities@^1.1.1, entities@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +entities@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" + integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== + +enzyme-adapter-react-16@1.15.2: + version "1.15.2" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.2.tgz#b16db2f0ea424d58a808f9df86ab6212895a4501" + integrity sha512-SkvDrb8xU3lSxID8Qic9rB8pvevDbLybxPK6D/vW7PrT0s2Cl/zJYuXvsd1EBTz0q4o3iqG3FJhpYz3nUNpM2Q== + dependencies: + enzyme-adapter-utils "^1.13.0" + enzyme-shallow-equal "^1.0.1" + has "^1.0.3" + object.assign "^4.1.0" + object.values "^1.1.1" + prop-types "^15.7.2" + react-is "^16.12.0" + react-test-renderer "^16.0.0-0" + semver "^5.7.0" + +enzyme-adapter-utils@^1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.0.tgz#01c885dde2114b4690bf741f8dc94cee3060eb78" + integrity sha512-YuEtfQp76Lj5TG1NvtP2eGJnFKogk/zT70fyYHXK2j3v6CtuHqc8YmgH/vaiBfL8K1SgVVbQXtTcgQZFwzTVyQ== + dependencies: + airbnb-prop-types "^2.15.0" + function.prototype.name "^1.1.2" + object.assign "^4.1.0" + object.fromentries "^2.0.2" + prop-types "^15.7.2" + semver "^5.7.1" + +enzyme-shallow-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.1.tgz#7afe03db3801c9b76de8440694096412a8d9d49e" + integrity sha512-hGA3i1so8OrYOZSM9whlkNmVHOicJpsjgTzC+wn2JMJXhq1oO4kA4bJ5MsfzSIcC71aLDKzJ6gZpIxrqt3QTAQ== + dependencies: + has "^1.0.3" + object-is "^1.0.2" + +enzyme-to-json@3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.4.3.tgz#ed4386f48768ed29e2d1a2910893542c34e7e0af" + integrity sha512-jqNEZlHqLdz7OTpXSzzghArSS3vigj67IU/fWkPyl1c0TCj9P5s6Ze0kRkYZWNEoCqCR79xlQbigYlMx5erh8A== + dependencies: + lodash "^4.17.15" + +enzyme@3.11.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28" + integrity sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw== + dependencies: + array.prototype.flat "^1.2.3" + cheerio "^1.0.0-rc.3" + enzyme-shallow-equal "^1.0.1" + function.prototype.name "^1.1.2" + has "^1.0.3" + html-element-map "^1.2.0" + is-boolean-object "^1.0.1" + is-callable "^1.1.5" + is-number-object "^1.0.4" + is-regex "^1.0.5" + is-string "^1.0.5" + is-subset "^0.1.1" + lodash.escape "^4.0.1" + lodash.isequal "^4.5.0" + object-inspect "^1.7.0" + object-is "^1.0.2" + object.assign "^4.1.0" + object.entries "^1.1.1" + object.values "^1.1.1" + raf "^3.4.1" + rst-selector-parser "^2.2.3" + string.prototype.trim "^1.2.1" + +errno@^0.1.3, errno@~0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" + integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== + dependencies: + prr "~1.0.1" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.13.0, es-abstract@^1.17.0-next.1: + version "1.17.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.0.tgz#f42a517d0036a5591dbb2c463591dc8bb50309b1" + integrity sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.1.5" + is-regex "^1.0.5" + object-inspect "^1.7.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimleft "^2.1.1" + string.prototype.trimright "^2.1.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escodegen@^1.11.1, escodegen@^1.9.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.1.tgz#08770602a74ac34c7a90ca9229e7d51e379abc76" + integrity sha512-Q8t2YZ+0e0pc7NRVj3B4tSQ9rim1oi4Fh46k2xhJ2qOiEwhQfdjyEQddWdj7ZFaKmU+5104vn1qrcjEPWq+bgQ== + dependencies: + esprima "^3.1.3" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +eslint-scope@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" + integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +esprima@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + dependencies: + estraverse "^4.1.0" + +estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.1.6.tgz#c01ccaa9ae4b897c6d0c3e210ae52f3c7a844375" + integrity sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U= + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + integrity sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg= + +eventemitter3@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" + integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== + +events@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" + integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg== + +eventsource@^1.0.7: + version "1.1.1" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.1.tgz#4544a35a57d7120fba4fa4c86cb4023b2c09df2f" + integrity sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA== + dependencies: + original "^1.0.0" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +exec-sh@^0.3.2: + version "0.3.4" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" + integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exenv@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" + integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50= + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= + dependencies: + homedir-polyfill "^1.0.1" + +expect@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" + integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== + dependencies: + "@jest/types" "^24.9.0" + ansi-styles "^3.2.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.9.0" + +express@^4.16.3, express@^4.17.1: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-1.1.4.tgz#19d6bf94dfc09d76ba711f39b872d21ff4dd9071" + integrity sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE= + dependencies: + kind-of "^1.1.0" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fancy-log@^1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" + integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== + dependencies: + ansi-gray "^0.1.1" + color-support "^1.1.3" + parse-node-version "^1.0.0" + time-stamp "^1.0.0" + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.1.1.tgz#87ee30e9e9f3eb40d6f254a7997655da753d7c82" + integrity sha512-nTCREpBY8w8r+boyFYAx21iL6faSsQynliPHM4Uf56SbkyohCNxpVPEH9xrF5TXKy+IsjkPUHDKiUkzBVRXn9g== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fastq@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.0.tgz#4ec8a38f4ac25f21492673adb7eae9cfef47d1c2" + integrity sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA== + dependencies: + reusify "^1.0.0" + +faye-websocket@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ= + dependencies: + websocket-driver ">=0.5.1" + +faye-websocket@~0.11.1: + version "0.11.3" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" + integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + dependencies: + bser "2.1.1" + +fbjs-scripts@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/fbjs-scripts/-/fbjs-scripts-0.8.3.tgz#b854de7a11e62a37f72dab9aaf4d9b53c4a03174" + integrity sha512-aUJ/uEzMIiBYuj/blLp4sVNkQQ7ZEB/lyplG1IzzOmZ83meiWecrGg5jBo4wWrxXmO4RExdtsSV1QkTjPt2Gag== + dependencies: + ansi-colors "^1.0.1" + babel-core "^6.7.2" + babel-preset-fbjs "^2.1.2" + core-js "^2.4.1" + cross-spawn "^5.1.0" + fancy-log "^1.3.2" + object-assign "^4.0.1" + plugin-error "^0.1.2" + semver "^5.1.0" + through2 "^2.0.0" + +fbjs@^0.8.16: + version "0.8.17" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" + integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90= + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.18" + +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + +figgy-pudding@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" + integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== + +file-loader@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-5.0.2.tgz#7f3d8b4ac85a5e8df61338cfec95d7405f971caa" + integrity sha512-QMiQ+WBkGLejKe81HU8SZ9PovsU/5uaLo0JdTCEXOYv7i7jfAjHZi1tcwp9tSASJPOmmHZtbdCervFmXMH/Dcg== + dependencies: + loader-utils "^1.2.3" + schema-utils "^2.5.0" + +file-loader@~4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-4.3.0.tgz#780f040f729b3d18019f20605f723e844b8a58af" + integrity sha512-aKrYPYjF1yG3oX0kWRrqrSMfgftm7oJW5M+m4owoldH5C51C0RkIwB++JbRvEW3IU6/ZG5n8UvEcdgwOt2UOWA== + dependencies: + loader-utils "^1.2.3" + schema-utils "^2.5.0" + +file-type@^12.0.0: + version "12.4.2" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-12.4.2.tgz#a344ea5664a1d01447ee7fb1b635f72feb6169d9" + integrity sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg== + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +filelist@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + +filesize@^3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" + integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg== + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + dependencies: + commondir "^1.0.1" + make-dir "^2.0.0" + pkg-dir "^3.0.0" + +find-cache-dir@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.2.0.tgz#e7fe44c1abc1299f516146e563108fd1006c1874" + integrity sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.0" + pkg-dir "^4.1.0" + +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +findup-sync@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" + integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + +flush-write-stream@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" + integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== + dependencies: + inherits "^2.0.3" + readable-stream "^2.3.6" + +focus-trap-react@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/focus-trap-react/-/focus-trap-react-4.0.1.tgz#3cffd39341df3b2f546a4a2fe94cfdea66154683" + integrity sha512-UUZKVEn5cFbF6yUnW7lbXNW0iqN617ShSqYKgxctUvWw1wuylLtyVmC0RmPQNnJ/U+zoKc/djb0tZMs0uN/0QQ== + dependencies: + focus-trap "^3.0.0" + +focus-trap@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-3.0.0.tgz#4d2ee044ae66bf7eb6ebc6c93bd7a1039481d7dc" + integrity sha512-jTFblf0tLWbleGjj2JZsAKbgtZTdL1uC48L8FcmSDl4c2vDoU4NycN1kgV5vJhuq1mxNFkw7uWZ1JAGlINWvyw== + dependencies: + tabbable "^3.1.0" + xtend "^4.0.1" + +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + +follow-redirects@^1.0.0, follow-redirects@^1.15.0: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-extra@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-6.0.1.tgz#8abc128f7946e310135ddc93b98bddb410e7a34b" + integrity sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-minipass@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.0.0.tgz#a6415edab02fae4b9e9230bc87ee2e4472003cd1" + integrity sha512-40Qz+LFXmd9tzYVnnBmZvFfvAADfUA14TXPK1s7IfElJTIZ97rA8w4Kin7Wt5JBrC3ShnnFJO/5vPjPEeJIq9A== + dependencies: + minipass "^3.0.0" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^1.2.7: + version "1.2.11" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.11.tgz#67bf57f4758f02ede88fb2a1712fef4d15358be3" + integrity sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw== + dependencies: + bindings "^1.5.0" + nan "^2.12.1" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +function.prototype.name@^1.1.1, function.prototype.name@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.2.tgz#5cdf79d7c05db401591dfde83e3b70c5123e9a45" + integrity sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + functions-have-names "^1.2.0" + +functions-have-names@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.0.tgz#83da7583e4ea0c9ac5ff530f73394b033e0bf77d" + integrity sha512-zKXyzksTeaCSw5wIX79iCA40YAa6CJMJgNg9wdkU/ERBrIdPSimPICYiLp65lRbSBqtiHql/HZfS2DyI/AH6tQ== + +gensync@^1.0.0-beta.1: + version "1.0.0-beta.1" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" + integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== + +get-caller-file@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-node-dimensions@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823" + integrity sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ== + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA== + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== + +globby@^10.0.0: + version "10.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543" + integrity sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg== + dependencies: + "@types/glob" "^7.1.1" + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.0.3" + glob "^7.1.3" + ignore "^5.1.1" + merge2 "^1.2.3" + slash "^3.0.0" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +google-map-react@1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/google-map-react/-/google-map-react-1.1.5.tgz#ad88caa88085c6292dbb1f890c70ff75fbee3d47" + integrity sha512-VgqEIo1DGEvSa8aP6Iy7F41r3K+h0h8hLO2Rqv5hKGO4TE2fRErWvGDr5oVGPJWBaNrXe1rmNrw772AqLgjUsg== + dependencies: + "@mapbox/point-geometry" "^0.1.0" + eventemitter3 "^1.1.0" + prop-types "^15.5.6" + scriptjs "^2.5.7" + +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= + +gud@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" + integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== + +gzip-size@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" + integrity sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA== + dependencies: + duplexer "^0.1.1" + pify "^4.0.1" + +handle-thing@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" + integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== + +handlebars@^4.7.0: + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +hard-source-webpack-plugin@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/hard-source-webpack-plugin/-/hard-source-webpack-plugin-0.13.1.tgz#a99071e25b232f1438a5bc3c99f10a3869e4428e" + integrity sha512-r9zf5Wq7IqJHdVAQsZ4OP+dcUSvoHqDMxJlIzaE2J0TZWn3UjMMrHqwDHR8Jr/pzPfG7XxSe36E7Y8QGNdtuAw== + dependencies: + chalk "^2.4.1" + find-cache-dir "^2.0.0" + graceful-fs "^4.1.11" + lodash "^4.15.0" + mkdirp "^0.5.1" + node-object-hash "^1.2.0" + parse-json "^4.0.0" + pkg-dir "^3.0.0" + rimraf "^2.6.2" + semver "^5.6.0" + tapable "^1.0.0-beta.5" + webpack-sources "^1.0.1" + write-json-file "^2.3.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.0, has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.0, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +he@1.2.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hex-color-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" + integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== + +highlight.js@^11.6.0: + version "11.6.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.6.0.tgz#a50e9da05763f1bb0c1322c8f4f755242cff3f5a" + integrity sha512-ig1eqDzJaB0pqEvlPVIpSSyMaO92bH1N2rJpLMN/nX396wTpDA4Eq0uK+7I/2XG17pFaaKE0kjV/XPeGt7Evjw== + +highlight.js@^9.17.1: + version "9.18.5" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825" + integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA== + +history@^4.9.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== + dependencies: + "@babel/runtime" "^7.1.2" + loose-envify "^1.2.0" + resolve-pathname "^3.0.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^1.0.1" + +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#101685d3aff3b23ea213163f6e8e12f4f111e19f" + integrity sha512-wbg3bpgA/ZqWrZuMOeJi8+SKMhr7X9TesL/rXMjTzh0p0JUBo3II8DHboYbuIXWRlttrUFxwcu/5kygrCw8fJw== + dependencies: + react-is "^16.7.0" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +homedir-polyfill@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== + dependencies: + parse-passwd "^1.0.0" + +hoopy@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" + integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== + +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + +hot-patcher@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/hot-patcher/-/hot-patcher-0.5.0.tgz#9d401424585aaf3a91646b816ceff40eb6a916b9" + integrity sha512-2Uu2W0s8+dnqXzdlg0MRsRzPoDCs1wVjOGSyMRRaMzLDX4bgHw6xDYKccsWafXPPxQpkQfEjgW6+17pwcg60bw== + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +hsl-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" + integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= + +hsla-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" + integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= + +html-element-map@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.2.0.tgz#dfbb09efe882806af63d990cf6db37993f099f22" + integrity sha512-0uXq8HsuG1v2TmQ8QkIhzbrqeskE4kn52Q18QJ9iAA/SnHoEKXWiUxHQtclRsCFWEUD2So34X+0+pZZu862nnw== + dependencies: + array-filter "^1.0.0" + +html-encoding-sniffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" + integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== + dependencies: + whatwg-encoding "^1.0.1" + +html-encoding-sniffer@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" + integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== + dependencies: + whatwg-encoding "^1.0.5" + +html-entities@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" + integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= + +html-escaper@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.0.tgz#71e87f931de3fe09e56661ab9a29aadec707b491" + integrity sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig== + +html-minifier@^3.2.3: + version "3.5.21" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c" + integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA== + dependencies: + camel-case "3.0.x" + clean-css "4.2.x" + commander "2.17.x" + he "1.2.x" + param-case "2.1.x" + relateurl "0.2.x" + uglify-js "3.4.x" + +html-tokenize@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-tokenize/-/html-tokenize-2.0.0.tgz#8b3a9a5deb475cae6a6f9671600d2c20ab298251" + integrity sha1-izqaXetHXK5qb5ZxYA0sIKspglE= + dependencies: + buffer-from "~0.1.1" + inherits "~2.0.1" + minimist "~0.0.8" + readable-stream "~1.0.27-1" + through2 "~0.4.1" + +html-webpack-plugin@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b" + integrity sha1-sBq71yOsqqeze2r0SS69oD2d03s= + dependencies: + html-minifier "^3.2.3" + loader-utils "^0.2.16" + lodash "^4.17.3" + pretty-error "^2.0.2" + tapable "^1.0.0" + toposort "^1.0.0" + util.promisify "1.0.0" + +htmlparser2@^3.3.0, htmlparser2@^3.9.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +"http-parser-js@>=0.4.0 <0.4.11": + version "0.4.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" + integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +http-proxy-middleware@0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" + integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== + dependencies: + http-proxy "^1.17.0" + is-glob "^4.0.0" + lodash "^4.17.11" + micromatch "^3.1.10" + +http-proxy@^1.17.0: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +iconv-lite@0.4.24, iconv-lite@~0.4.13: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-utils@^4.0.0, icss-utils@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== + dependencies: + postcss "^7.0.14" + +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= + +ignore@^5.1.1: + version "5.1.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" + integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== + +imagemin@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/imagemin/-/imagemin-7.0.1.tgz#f6441ca647197632e23db7d971fffbd530c87dbf" + integrity sha512-33AmZ+xjZhg2JMCe+vDf6a9mzWukE7l+wAtesjE7KyteqqKjzxv7aVQeWnul1Ve26mWvEQqyPwl0OctNBfSR9w== + dependencies: + file-type "^12.0.0" + globby "^10.0.0" + graceful-fs "^4.2.2" + junk "^3.1.0" + make-dir "^3.0.0" + p-pipe "^3.0.0" + replace-ext "^1.0.0" + +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + +import-fresh@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@2.0.0, import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= + +infer-owner@^1.0.3, infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@^1.3.4, ini@^1.3.5: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +internal-ip@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" + integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== + dependencies: + default-gateway "^4.2.0" + ipaddr.js "^1.9.0" + +interpret@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" + integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== + +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +invariant@^2.2.2, invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +invert-kv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" + integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== + +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= + +ip@^1.1.0, ip@^1.1.5: + version "1.1.9" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396" + integrity sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ== + +ipaddr.js@1.9.1, ipaddr.js@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= + +is-absolute-url@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" + integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arguments@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" + integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= + dependencies: + binary-extensions "^1.0.0" + +is-boolean-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" + integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.4, is-callable@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" + integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-color-stop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" + integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= + dependencies: + css-color-names "^0.0.4" + hex-color-regex "^1.1.0" + hsl-regex "^1.0.0" + hsla-regex "^1.0.0" + rgb-regex "^1.0.1" + rgba-regex "^1.0.0" + +is-core-module@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw== + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-number-object@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" + integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-cwd@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-in-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" + integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== + dependencies: + is-path-inside "^2.1.0" + +is-path-inside@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" + integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== + dependencies: + path-is-inside "^1.0.2" + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + +is-regex@^1.0.4, is-regex@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" + integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== + dependencies: + has "^1.0.3" + +is-resolvable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== + +is-stream@^1.0.1, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-string@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" + integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== + +is-subset@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" + integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= + +is-symbol@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + dependencies: + has-symbols "^1.0.1" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-windows@^1.0.1, is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + integrity sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA== + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== + +istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" + integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== + dependencies: + "@babel/generator" "^7.4.0" + "@babel/parser" "^7.4.3" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.3" + "@babel/types" "^7.4.0" + istanbul-lib-coverage "^2.0.5" + semver "^6.0.0" + +istanbul-lib-report@^2.0.4: + version "2.0.8" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" + integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== + dependencies: + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + supports-color "^6.1.0" + +istanbul-lib-source-maps@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + rimraf "^2.6.3" + source-map "^0.6.1" + +istanbul-reports@^2.2.6: + version "2.2.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.7.tgz#5d939f6237d7b48393cc0959eab40cd4fd056931" + integrity sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg== + dependencies: + html-escaper "^2.0.0" + +jake@^10.8.5: + version "10.8.5" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" + integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.1" + minimatch "^3.0.4" + +jest-changed-files@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" + integrity sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg== + dependencies: + "@jest/types" "^24.9.0" + execa "^1.0.0" + throat "^4.0.0" + +jest-cli@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af" + integrity sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg== + dependencies: + "@jest/core" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + exit "^0.1.2" + import-local "^2.0.0" + is-ci "^2.0.0" + jest-config "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + prompts "^2.0.1" + realpath-native "^1.1.0" + yargs "^13.3.0" + +jest-config@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.9.0.tgz#fb1bbc60c73a46af03590719efa4825e6e4dd1b5" + integrity sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ== + dependencies: + "@babel/core" "^7.1.0" + "@jest/test-sequencer" "^24.9.0" + "@jest/types" "^24.9.0" + babel-jest "^24.9.0" + chalk "^2.0.1" + glob "^7.1.1" + jest-environment-jsdom "^24.9.0" + jest-environment-node "^24.9.0" + jest-get-type "^24.9.0" + jest-jasmine2 "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + micromatch "^3.1.10" + pretty-format "^24.9.0" + realpath-native "^1.1.0" + +jest-diff@^24.3.0, jest-diff@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" + integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== + dependencies: + chalk "^2.0.1" + diff-sequences "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-docblock@^24.3.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" + integrity sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA== + dependencies: + detect-newline "^2.1.0" + +jest-each@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.9.0.tgz#eb2da602e2a610898dbc5f1f6df3ba86b55f8b05" + integrity sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog== + dependencies: + "@jest/types" "^24.9.0" + chalk "^2.0.1" + jest-get-type "^24.9.0" + jest-util "^24.9.0" + pretty-format "^24.9.0" + +jest-environment-jsdom@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b" + integrity sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + jest-util "^24.9.0" + jsdom "^11.5.1" + +jest-environment-node@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3" + integrity sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + jest-util "^24.9.0" + +jest-get-type@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" + integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== + +jest-haste-map@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" + integrity sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ== + dependencies: + "@jest/types" "^24.9.0" + anymatch "^2.0.0" + fb-watchman "^2.0.0" + graceful-fs "^4.1.15" + invariant "^2.2.4" + jest-serializer "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.9.0" + micromatch "^3.1.10" + sane "^4.0.3" + walker "^1.0.7" + optionalDependencies: + fsevents "^1.2.7" + +jest-jasmine2@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz#1f7b1bd3242c1774e62acabb3646d96afc3be6a0" + integrity sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw== + dependencies: + "@babel/traverse" "^7.1.0" + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + co "^4.6.0" + expect "^24.9.0" + is-generator-fn "^2.0.0" + jest-each "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-runtime "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + pretty-format "^24.9.0" + throat "^4.0.0" + +jest-leak-detector@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz#b665dea7c77100c5c4f7dfcb153b65cf07dcf96a" + integrity sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA== + dependencies: + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-matcher-utils@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" + integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA== + dependencies: + chalk "^2.0.1" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-message-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3" + integrity sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/stack-utils" "^1.0.1" + chalk "^2.0.1" + micromatch "^3.1.10" + slash "^2.0.0" + stack-utils "^1.0.1" + +jest-mock@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6" + integrity sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w== + dependencies: + "@jest/types" "^24.9.0" + +jest-pnp-resolver@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" + integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== + +jest-regex-util@^24.3.0, jest-regex-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636" + integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA== + +jest-resolve-dependencies@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz#ad055198959c4cfba8a4f066c673a3f0786507ab" + integrity sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g== + dependencies: + "@jest/types" "^24.9.0" + jest-regex-util "^24.3.0" + jest-snapshot "^24.9.0" + +jest-resolve@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" + integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ== + dependencies: + "@jest/types" "^24.9.0" + browser-resolve "^1.11.3" + chalk "^2.0.1" + jest-pnp-resolver "^1.2.1" + realpath-native "^1.1.0" + +jest-runner@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.9.0.tgz#574fafdbd54455c2b34b4bdf4365a23857fcdf42" + integrity sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.4.2" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-config "^24.9.0" + jest-docblock "^24.3.0" + jest-haste-map "^24.9.0" + jest-jasmine2 "^24.9.0" + jest-leak-detector "^24.9.0" + jest-message-util "^24.9.0" + jest-resolve "^24.9.0" + jest-runtime "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.6.0" + source-map-support "^0.5.6" + throat "^4.0.0" + +jest-runtime@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.9.0.tgz#9f14583af6a4f7314a6a9d9f0226e1a781c8e4ac" + integrity sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.9.0" + "@jest/source-map" "^24.3.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/yargs" "^13.0.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.1.15" + jest-config "^24.9.0" + jest-haste-map "^24.9.0" + jest-message-util "^24.9.0" + jest-mock "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + realpath-native "^1.1.0" + slash "^2.0.0" + strip-bom "^3.0.0" + yargs "^13.3.0" + +jest-serializer@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.9.0.tgz#e6d7d7ef96d31e8b9079a714754c5d5c58288e73" + integrity sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ== + +jest-snapshot@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba" + integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew== + dependencies: + "@babel/types" "^7.0.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + expect "^24.9.0" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-resolve "^24.9.0" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + pretty-format "^24.9.0" + semver "^6.2.0" + +jest-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.9.0.tgz#7396814e48536d2e85a37de3e4c431d7cb140162" + integrity sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg== + dependencies: + "@jest/console" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/source-map" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + callsites "^3.0.0" + chalk "^2.0.1" + graceful-fs "^4.1.15" + is-ci "^2.0.0" + mkdirp "^0.5.1" + slash "^2.0.0" + source-map "^0.6.0" + +jest-validate@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab" + integrity sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ== + dependencies: + "@jest/types" "^24.9.0" + camelcase "^5.3.1" + chalk "^2.0.1" + jest-get-type "^24.9.0" + leven "^3.1.0" + pretty-format "^24.9.0" + +jest-watcher@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.9.0.tgz#4b56e5d1ceff005f5b88e528dc9afc8dd4ed2b3b" + integrity sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw== + dependencies: + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/yargs" "^13.0.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + jest-util "^24.9.0" + string-length "^2.0.0" + +jest-worker@^24.6.0, jest-worker@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" + integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== + dependencies: + merge-stream "^2.0.0" + supports-color "^6.1.0" + +jest@24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171" + integrity sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw== + dependencies: + import-local "^2.0.0" + jest-cli "^24.9.0" + +jquery@^3.4.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" + integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= + +js-yaml@^3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jsdom@^11.5.1: + version "11.12.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" + integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== + dependencies: + abab "^2.0.0" + acorn "^5.5.3" + acorn-globals "^4.1.0" + array-equal "^1.0.0" + cssom ">= 0.3.2 < 0.4.0" + cssstyle "^1.0.0" + data-urls "^1.0.0" + domexception "^1.0.1" + escodegen "^1.9.1" + html-encoding-sniffer "^1.0.2" + left-pad "^1.3.0" + nwsapi "^2.0.7" + parse5 "4.0.0" + pn "^1.1.0" + request "^2.87.0" + request-promise-native "^1.0.5" + sax "^1.2.4" + symbol-tree "^3.2.2" + tough-cookie "^2.3.4" + w3c-hr-time "^1.0.1" + webidl-conversions "^4.0.2" + whatwg-encoding "^1.0.3" + whatwg-mimetype "^2.1.0" + whatwg-url "^6.4.1" + ws "^5.2.0" + xml-name-validator "^3.0.0" + +jsdom@^15.1.0: + version "15.2.1" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-15.2.1.tgz#d2feb1aef7183f86be521b8c6833ff5296d07ec5" + integrity sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g== + dependencies: + abab "^2.0.0" + acorn "^7.1.0" + acorn-globals "^4.3.2" + array-equal "^1.0.0" + cssom "^0.4.1" + cssstyle "^2.0.0" + data-urls "^1.1.0" + domexception "^1.0.1" + escodegen "^1.11.1" + html-encoding-sniffer "^1.0.2" + nwsapi "^2.2.0" + parse5 "5.1.0" + pn "^1.1.0" + request "^2.88.0" + request-promise-native "^1.0.7" + saxes "^3.1.9" + symbol-tree "^3.2.2" + tough-cookie "^3.0.1" + w3c-hr-time "^1.0.1" + w3c-xmlserializer "^1.1.2" + webidl-conversions "^4.0.2" + whatwg-encoding "^1.0.5" + whatwg-mimetype "^2.3.0" + whatwg-url "^7.0.0" + ws "^7.0.0" + xml-name-validator "^3.0.0" + +jsdom@^16.5.0: + version "16.7.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" + integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== + dependencies: + abab "^2.0.5" + acorn "^8.2.4" + acorn-globals "^6.0.0" + cssom "^0.4.4" + cssstyle "^2.3.0" + data-urls "^2.0.0" + decimal.js "^10.2.1" + domexception "^2.0.1" + escodegen "^2.0.0" + form-data "^3.0.0" + html-encoding-sniffer "^2.0.1" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.0" + parse5 "6.0.1" + saxes "^5.0.1" + symbol-tree "^3.2.4" + tough-cookie "^4.0.0" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^2.0.0" + webidl-conversions "^6.1.0" + whatwg-encoding "^1.0.5" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.5.0" + ws "^7.4.6" + xml-name-validator "^3.0.0" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha512-a3xHnILGMtk+hDOqNwHzF6e2fNbiMrXZvxKQiEv2MlgQP+pjIOzqAmKYD2mDpXYE/44M7g+n9p2bKkYWDUcXCQ== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json3@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" + integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== + +json5@2.x, json5@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6" + integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ== + dependencies: + minimist "^1.2.0" + +json5@^0.5.0, json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +junk@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1" + integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== + +killable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" + integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== + +kind-of@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-1.1.0.tgz#140a3d2d41a36d2efcfa9377b62c24f8495a5c44" + integrity sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g== + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ== + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw== + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +konva@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/konva/-/konva-4.1.2.tgz#b0718ae02539866ff9d4897dec42d2fb58dc3812" + integrity sha512-1r2H85gzCY25Rcr0csfrtABWWaV2HjeFkO4zvkvy6sJrWLit2GJyX3rdQYEw7B8q3jKqccEGdYjxr7oa2st4Qg== + +last-call-webpack-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555" + integrity sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w== + dependencies: + lodash "^4.17.5" + webpack-sources "^1.1.0" + +lcid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== + dependencies: + invert-kv "^2.0.0" + +left-pad@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" + integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +loader-runner@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" + integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== + +loader-utils@1.2.3, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" + integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== + dependencies: + big.js "^5.2.2" + emojis-list "^2.0.0" + json5 "^1.0.1" + +loader-utils@^0.2.16: + version "0.2.17" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" + integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g= + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + object-assign "^4.0.1" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + +lodash.escape@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" + integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= + +lodash.flattendeep@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" + integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + +lodash.memoize@4.x, lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.4: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +loglevel@^1.6.6: + version "1.6.6" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.6.tgz#0ee6300cc058db6b3551fa1c4bf73b83bb771312" + integrity sha512-Sgr5lbboAUBo3eXCSPL4/KoVz3ROKquOjcctxmHIt+vol2DrqTQe3SwkKKuYhEiWB5kYa13YyopJ69deJ1irzQ== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lower-case@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= + +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lunr@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.8.tgz#a8b89c31f30b5a044b97d2d28e2da191b6ba2072" + integrity sha512-oxMeX/Y35PNFuZoHp+jUj5OSEmLCaIH4KTFJh7a93cHBoFmpw2IoPs22VIz7vyO2YUnx2Tn9dzIwO2P/4quIRg== + +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + +make-dir@^2.0.0, make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.0.tgz#1b5f39f6b9270ed33f9f054c5c0f84304989f801" + integrity sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw== + dependencies: + semver "^6.0.0" + +make-error@1.x: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= + dependencies: + tmpl "1.0.x" + +mamacro@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" + integrity sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA== + +map-age-cleaner@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +marked@^0.8.0: + version "0.8.2" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.8.2.tgz#4faad28d26ede351a7a1aaa5fec67915c869e355" + integrity sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw== + +math-expression-evaluator@^1.2.14: + version "1.2.17" + resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" + integrity sha1-3oGf282E3M2PrlnGrreWFbnSZqw= + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" + integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +mem@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" + integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== + dependencies: + map-age-cleaner "^0.1.1" + mimic-fn "^2.0.0" + p-is-promise "^2.0.0" + +memory-fs@^0.4.0, memory-fs@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.2.3, merge2@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" + integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== + +merge@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" + integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +micromatch@^4.0.0, micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@1.43.0, "mime-db@>= 1.43.0 < 2": + version "1.43.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" + integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19: + version "2.1.26" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" + integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + dependencies: + mime-db "1.43.0" + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.4.4: + version "2.4.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" + integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== + +mimic-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mini-create-react-context@^0.3.0: + version "0.3.2" + resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189" + integrity sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw== + dependencies: + "@babel/runtime" "^7.4.0" + gud "^1.0.0" + tiny-warning "^1.0.2" + +mini-css-extract-plugin@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e" + integrity sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A== + dependencies: + loader-utils "^1.1.0" + normalize-url "1.9.1" + schema-utils "^1.0.0" + webpack-sources "^1.1.0" + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= + +minimatch@^3.0.0, minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" + integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== + dependencies: + brace-expansion "^2.0.1" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q== + +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +minimist@~0.0.8: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz#3dcb6bb4a546e32969c7ad710f2c79a86abba93a" + integrity sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.1.tgz#7607ce778472a185ad6d89082aa2070f79cedcd5" + integrity sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w== + dependencies: + yallist "^4.0.0" + +mississippi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" + integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^3.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@0.x, mkdirp@^0.5.1, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +moo@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" + integrity sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw== + +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + +multipipe@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-1.0.2.tgz#cc13efd833c9cda99f224f868461b8e1a3fd939d" + integrity sha1-zBPv2DPJzamfIk+GhGG44aP9k50= + dependencies: + duplexer2 "^0.1.2" + object-assign "^4.1.0" + +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +nearley@^2.7.10: + version "2.19.0" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.0.tgz#37717781d0fd0f2bfc95e233ebd75678ca4bda46" + integrity sha512-2v52FTw7RPqieZr3Gth1luAXZR7Je6q3KaDHY5bjl/paDUdMu35fZ8ICNgiYJRr3tf3NMvIQQR1r27AvEr9CRA== + dependencies: + commander "^2.19.0" + moo "^0.4.3" + railroad-diagrams "^1.0.0" + randexp "0.4.6" + semver "^5.4.1" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +no-case@^2.2.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" + integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== + dependencies: + lower-case "^1.1.1" + +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + +node-fetch@^1.0.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + +node-fetch@^3.2.10: + version "3.2.10" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8" + integrity sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + +node-forge@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" + integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + +node-libs-browser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" + integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^3.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.1" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.11.0" + vm-browserify "^1.0.1" + +node-modules-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" + integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= + +node-notifier@^5.4.2: + version "5.4.5" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.5.tgz#0cbc1a2b0f658493b4025775a13ad938e96091ef" + integrity sha512-tVbHs7DyTLtzOiN78izLA85zRqB9NvEXkAf014Vx3jtSvn/xBl6bR8ZYifj+dFcFrKI21huSQgJZ6ZtL3B4HfQ== + dependencies: + growly "^1.3.0" + is-wsl "^1.1.0" + semver "^5.5.0" + shellwords "^0.1.1" + which "^1.3.0" + +node-object-hash@^1.2.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-1.4.2.tgz#385833d85b229902b75826224f6077be969a9e94" + integrity sha512-UdS4swXs85fCGWWf6t6DMGgpN/vnlKeSGEQ7hJcrs7PBFoxoKLmibc3QRb7fwiYsjdL7PX8iI/TMSlZ90dgHhQ== + +node-releases@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" + integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + +normalize-package-data@^2.3.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-url@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw= + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + +normalize-url@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" + integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +nth-check@^1.0.2, nth-check@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +nwsapi@^2.0.7, nwsapi@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" + integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-inspect@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" + integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== + +object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-is@^1.0.1, object-is@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4" + integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ== + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-keys@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" + integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY= + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.entries@^1.1.0, object.entries@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.1.tgz#ee1cf04153de02bb093fec33683900f57ce5399b" + integrity sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + has "^1.0.3" + +object.fromentries@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9" + integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + has "^1.0.3" + +object.getownpropertydescriptors@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" + integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +object.values@^1.1.0, object.values@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" + integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + has "^1.0.3" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +opener@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed" + integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA== + +opn@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== + dependencies: + is-wsl "^1.1.0" + +optimize-css-assets-webpack-plugin@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.1.tgz#9eb500711d35165b45e7fd60ba2df40cb3eb9159" + integrity sha512-Rqm6sSjWtx9FchdP0uzTQDc7GXDKnwVEGoSxjezPkzMewx7gEWE9IMUYKmigTRC4U3RaNSwYVnUDLuIdtTpm0A== + dependencies: + cssnano "^4.1.0" + last-call-webpack-plugin "^3.0.0" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +original@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" + integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== + dependencies: + url-parse "^1.4.3" + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-locale@^3.0.0, os-locale@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" + integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== + dependencies: + execa "^1.0.0" + lcid "^2.0.0" + mem "^4.0.0" + +os-tmpdir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + +p-each-series@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" + integrity sha1-kw89Et0fUOdDRFeiLNbwSsatf3E= + dependencies: + p-reduce "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-is-promise@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" + integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" + integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +p-map@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d" + integrity sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ== + dependencies: + aggregate-error "^3.0.0" + +p-pipe@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-3.0.0.tgz#ab1fb87c0b8dd79b3bb03a8a23680fc9d054e132" + integrity sha512-gwwdRFmaxsT3IU+Tl3vYKVRdjfhg8Bbdjw7B+E0y6F7Yz6l+eaQLn0BRmGMXIhcPDONPtOkMoNwx1etZh4zPJA== + +p-reduce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" + integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= + +p-retry@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" + integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== + dependencies: + retry "^0.12.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +pako@~1.0.5: + version "1.0.10" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" + integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw== + +parallel-transform@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" + integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== + dependencies: + cyclist "^1.0.1" + inherits "^2.0.3" + readable-stream "^2.1.5" + +param-case@2.1.x: + version "2.1.1" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" + integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= + dependencies: + no-case "^2.2.0" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-asn1@^5.0.0: + version "5.1.5" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" + integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ== + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" + +parse-asn1@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" + integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== + dependencies: + asn1.js "^5.2.0" + browserify-aes "^1.0.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-json@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f" + integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + lines-and-columns "^1.1.6" + +parse-node-version@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" + integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= + +parse5@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" + integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== + +parse5@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" + integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== + +parse5@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + +parse5@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" + integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA== + dependencies: + "@types/node" "*" + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-posix@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f" + integrity sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8= + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pbkdf2@^3.0.3: + version "3.0.17" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" + integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.5: + version "2.2.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" + integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +pirates@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" + integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== + dependencies: + node-modules-regexp "^1.0.0" + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +plugin-error@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace" + integrity sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4= + dependencies: + ansi-cyan "^0.1.1" + ansi-red "^0.1.1" + arr-diff "^1.0.1" + arr-union "^2.0.1" + extend-shallow "^1.1.2" + +pn@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" + integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== + +popper.js@^1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3" + integrity sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw== + +portfinder@^1.0.25: + version "1.0.25" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca" + integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.1" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +postcss-calc@^7.0.1: + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e" + integrity sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg== + dependencies: + postcss "^7.0.27" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.0.2" + +postcss-colormin@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381" + integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw== + dependencies: + browserslist "^4.0.0" + color "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-convert-values@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" + integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-discard-comments@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" + integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== + dependencies: + postcss "^7.0.0" + +postcss-discard-duplicates@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" + integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ== + dependencies: + postcss "^7.0.0" + +postcss-discard-empty@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" + integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w== + dependencies: + postcss "^7.0.0" + +postcss-discard-overridden@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" + integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg== + dependencies: + postcss "^7.0.0" + +postcss-merge-longhand@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" + integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw== + dependencies: + css-color-names "0.0.4" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + stylehacks "^4.0.0" + +postcss-merge-rules@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650" + integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + cssnano-util-same-parent "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + vendors "^1.0.0" + +postcss-minify-font-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" + integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-gradients@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471" + integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q== + dependencies: + cssnano-util-get-arguments "^4.0.0" + is-color-stop "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-params@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874" + integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg== + dependencies: + alphanum-sort "^1.0.0" + browserslist "^4.0.0" + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + uniqs "^2.0.0" + +postcss-minify-selectors@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8" + integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g== + dependencies: + alphanum-sort "^1.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +postcss-modules-extract-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== + dependencies: + postcss "^7.0.5" + +postcss-modules-local-by-default@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" + integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== + dependencies: + icss-utils "^4.1.1" + postcss "^7.0.32" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" + integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + +postcss-modules-values@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" + integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== + dependencies: + icss-utils "^4.0.0" + postcss "^7.0.6" + +postcss-normalize-charset@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" + integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g== + dependencies: + postcss "^7.0.0" + +postcss-normalize-display-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a" + integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-positions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f" + integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA== + dependencies: + cssnano-util-get-arguments "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-repeat-style@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c" + integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q== + dependencies: + cssnano-util-get-arguments "^4.0.0" + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-string@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c" + integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA== + dependencies: + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-timing-functions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9" + integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-unicode@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" + integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-url@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" + integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA== + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^3.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-whitespace@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82" + integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-ordered-values@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee" + integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw== + dependencies: + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-reduce-initial@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" + integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + +postcss-reduce-transforms@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29" + integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg== + dependencies: + cssnano-util-get-match "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-selector-parser@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz#b310f5c4c0fdaf76f94902bbaa30db6aa84f5270" + integrity sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA== + dependencies: + dot-prop "^5.2.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: + version "6.0.10" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" + integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-svgo@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.3.tgz#343a2cdbac9505d416243d496f724f38894c941e" + integrity sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + svgo "^1.0.0" + +postcss-unique-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" + integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg== + dependencies: + alphanum-sort "^1.0.0" + postcss "^7.0.0" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + +postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.23, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" + integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== + dependencies: + picocolors "^0.2.1" + source-map "^0.6.1" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= + +prettier@3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== + +pretty-error@^2.0.2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" + integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM= + dependencies: + renderkid "^2.0.1" + utila "~0.4" + +pretty-format@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" + integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== + dependencies: + "@jest/types" "^24.9.0" + ansi-regex "^4.0.0" + ansi-styles "^3.2.0" + react-is "^16.8.4" + +private@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + +prompts@^2.0.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.0.tgz#a444e968fa4cc7e86689a74050685ac8006c4cc4" + integrity sha512-NfbbPPg/74fT7wk2XYQ7hAIp9zJyZp5Fu19iRbORqqy1BhtrkZ0fPafBU+7bmn8ie69DpT0R6QpJIN2oisYjJg== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.3" + +prop-types-exact@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" + integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== + dependencies: + has "^1.0.3" + object.assign "^4.1.0" + reflect.ownkeys "^0.2.0" + +prop-types@15.6.1: + version "15.6.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" + integrity sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ== + dependencies: + fbjs "^0.8.16" + loose-envify "^1.3.1" + object-assign "^4.1.1" + +prop-types@^15.5.0: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + +psl@^1.1.24, psl@^1.1.28: + version "1.7.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" + integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== + +psl@^1.1.33: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + +query-string@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s= + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +raf@^3.4.0, raf@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + +railroad-diagrams@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" + integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= + +randexp@0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" + integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== + dependencies: + discontinuous-range "1.0.0" + ret "~0.1.10" + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-loader@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.0.tgz#d639c40fb9d72b5c7f8abc1fb2ddb25b29d3d540" + integrity sha512-iINUOYvl1cGEmfoaLjnZXt4bKfT2LJnZZib5N/LLyAphC+Dd11vNP9CNVb38j+SAJpFI1uo8j9frmih53ASy7Q== + dependencies: + loader-utils "^1.2.3" + schema-utils "^2.5.0" + +react-async-script@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.2.0.tgz#ab9412a26f0b83f5e2e00de1d2befc9400834b21" + integrity sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q== + dependencies: + hoist-non-react-statics "^3.3.0" + prop-types "^15.5.0" + +react-axe@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/react-axe/-/react-axe-3.3.0.tgz#b87bff644ed3ed6f1ca12bcc64c00000e359c25b" + integrity sha512-JoxU2jcTla37U6MtqIoYnGaRQcAHkNm9JxTjx2wcEgFm8Zd2A2vo9eboxcmpLjklXDKJwJNbyDo2Jcbbme6xwA== + dependencies: + axe-core "^3.3.2" + requestidlecallback "^0.3.0" + +react-dom@16.12.0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.12.0.tgz#0da4b714b8d13c2038c9396b54a92baea633fe11" + integrity sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.18.0" + +react-fast-compare@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + +react-google-recaptcha@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz#9f6f4954ce49c1dedabc2c532347321d892d0a16" + integrity sha512-K9jr7e0CWFigi8KxC3WPvNqZZ47df2RrMAta6KmRoE4RUi7Ys6NmNjytpXpg4HI/svmQJLKR+PncEPaNJ98DqQ== + dependencies: + prop-types "^15.5.0" + react-async-script "^1.1.1" + +react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" + integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== + +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-konva@16.12.0-0: + version "16.12.0-0" + resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-16.12.0-0.tgz#a39a383bb0c9e67a04cabde97c8015d74a2f14c5" + integrity sha512-gDwvNh0bKoS2hzYFiUHUipfJnEzqfOc0zB5PWy3yV6RFCAhE/LcN079wn/d/cY4S+3rX3OozKXtlMyJGVy2WJA== + dependencies: + react-reconciler "^0.24.0" + scheduler "^0.18.0" + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-measure@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-2.3.0.tgz#75835d39abec9ae13517f35a819c160997a7a44e" + integrity sha512-dwAvmiOeblj5Dvpnk8Jm7Q8B4THF/f1l1HtKVi0XDecsG6LXwGvzV5R1H32kq3TW6RW64OAf5aoQxpIgLa4z8A== + dependencies: + "@babel/runtime" "^7.2.0" + get-node-dimensions "^1.2.1" + prop-types "^15.6.2" + resize-observer-polyfill "^1.5.0" + +react-reconciler@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.24.0.tgz#5a396b2c2f5efe8554134a5935f49f546723f2dd" + integrity sha512-gAGnwWkf+NOTig9oOowqid9O0HjTDC+XVGBCAmJYYJ2A2cN/O4gDdIuuUQjv8A4v6GDwVfJkagpBBLW5OW9HSw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.18.0" + +react-resize-detector@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-2.3.0.tgz#57bad1ae26a28a62a2ddb678ba6ffdf8fa2b599c" + integrity sha512-oCAddEWWeFWYH5FAcHdBYcZjAw9fMzRUK9sWSx6WvSSOPVRxcHd5zTIGy/mOus+AhN/u6T4TMiWxvq79PywnJQ== + dependencies: + lodash.debounce "^4.0.8" + lodash.throttle "^4.1.1" + prop-types "^15.6.0" + resize-observer-polyfill "^1.5.0" + +react-router-dom@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18" + integrity sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew== + dependencies: + "@babel/runtime" "^7.1.2" + history "^4.9.0" + loose-envify "^1.3.1" + prop-types "^15.6.2" + react-router "5.1.2" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-router@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418" + integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A== + dependencies: + "@babel/runtime" "^7.1.2" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" + loose-envify "^1.3.1" + mini-create-react-context "^0.3.0" + path-to-regexp "^1.7.0" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-smooth@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-1.0.5.tgz#94ae161d7951cdd893ccb7099d031d342cb762ad" + integrity sha512-eW057HT0lFgCKh8ilr0y2JaH2YbNcuEdFpxyg7Gf/qDKk9hqGMyXryZJ8iMGJEuKH0+wxS0ccSsBBB3W8yCn8w== + dependencies: + lodash "~4.17.4" + prop-types "^15.6.0" + raf "^3.4.0" + react-transition-group "^2.5.0" + +react-test-renderer@^16.0.0-0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.12.0.tgz#11417ffda579306d4e841a794d32140f3da1b43f" + integrity sha512-Vj/teSqt2oayaWxkbhQ6gKis+t5JrknXfPVo+aIJ8QwYAqMPH77uptOdrlphyxl8eQI/rtkOYg86i/UWkpFu0w== + dependencies: + object-assign "^4.1.1" + prop-types "^15.6.2" + react-is "^16.8.6" + scheduler "^0.18.0" + +react-transition-group@^2.5.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" + integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== + dependencies: + dom-helpers "^3.4.0" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" + +react@16.12.0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83" + integrity sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + +read-pkg-up@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" + integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA== + dependencies: + find-up "^3.0.0" + read-pkg "^3.0.0" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6, readable-stream@^3.1.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" + integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@~1.0.17, readable-stream@~1.0.27-1: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +realpath-native@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" + integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== + dependencies: + util.promisify "^1.0.0" + +recharts-scale@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.3.tgz#040b4f638ed687a530357292ecac880578384b59" + integrity sha512-t8p5sccG9Blm7c1JQK/ak9O8o95WGhNXD7TXg/BW5bYbVlr6eCeRBNpgyigD4p6pSSMehC5nSvBUPj6F68rbFA== + dependencies: + decimal.js-light "^2.4.1" + +recharts@1.8.5: + version "1.8.5" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-1.8.5.tgz#ca94a3395550946334a802e35004ceb2583fdb12" + integrity sha512-tM9mprJbXVEBxjM7zHsIy6Cc41oO/pVYqyAsOHLxlJrbNBuLs0PHB3iys2M+RqCF0//k8nJtZF6X6swSkWY3tg== + dependencies: + classnames "^2.2.5" + core-js "^2.6.10" + d3-interpolate "^1.3.0" + d3-scale "^2.1.0" + d3-shape "^1.2.0" + lodash "^4.17.5" + prop-types "^15.6.0" + react-resize-detector "^2.3.0" + react-smooth "^1.0.5" + recharts-scale "^0.4.2" + reduce-css-calc "^1.3.0" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= + dependencies: + resolve "^1.1.6" + +reduce-css-calc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" + integrity sha1-dHyRTgSWFKTJz7umKYca0dKSdxY= + dependencies: + balanced-match "^0.4.2" + math-expression-evaluator "^1.2.14" + reduce-function-call "^1.0.1" + +reduce-function-call@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.3.tgz#60350f7fb252c0a67eb10fd4694d16909971300f" + integrity sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ== + dependencies: + balanced-match "^1.0.0" + +reflect.ownkeys@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" + integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= + +regenerator-runtime@0.13.3, regenerator-runtime@^0.13.2: + version "0.13.3" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" + integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw== + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexp.prototype.flags@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" + integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + +relateurl@0.2.x: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= + +relative@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/relative/-/relative-3.0.2.tgz#0dcd8ec54a5d35a3c15e104503d65375b5a5367f" + integrity sha1-Dc2OxUpdNaPBXhBFA9ZTdbWlNn8= + dependencies: + isobject "^2.0.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +renderkid@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149" + integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA== + dependencies: + css-select "^1.1.0" + dom-converter "^0.2" + htmlparser2 "^3.3.0" + strip-ansi "^3.0.0" + utila "^0.4.0" + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= + dependencies: + is-finite "^1.0.0" + +replace-ext@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" + integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= + +request-promise-core@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" + integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== + dependencies: + lodash "^4.17.15" + +request-promise-native@^1.0.5, request-promise-native@^1.0.7: + version "1.0.8" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" + integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== + dependencies: + request-promise-core "1.1.3" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@^2.87.0, request@^2.88.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +requestidlecallback@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/requestidlecallback/-/requestidlecallback-0.3.0.tgz#6fb74e0733f90df3faa4838f9f6a2a5f9b742ac5" + integrity sha1-b7dOBzP5DfP6pIOPn2oqX5t0KsU= + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resize-observer-polyfill@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= + +resolve@1.x, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== + dependencies: + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + +reusify@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rgb-regex@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" + integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= + +rgba-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" + integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= + +rimraf@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" + integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== + dependencies: + glob "^7.1.3" + +rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +rst-selector-parser@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" + integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE= + dependencies: + lodash.flattendeep "^4.4.0" + nearley "^2.7.10" + +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== + +run-parallel@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" + integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= + dependencies: + aproba "^1.1.1" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sane@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== + dependencies: + "@cnakazawa/watch" "^1.0.3" + anymatch "^2.0.0" + capture-exit "^2.0.0" + exec-sh "^0.3.2" + execa "^1.0.0" + fb-watchman "^2.0.0" + micromatch "^3.1.4" + minimist "^1.1.1" + walker "~1.0.5" + +sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +saxes@^3.1.9: + version "3.1.11" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b" + integrity sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g== + dependencies: + xmlchars "^2.1.1" + +saxes@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" + integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== + dependencies: + xmlchars "^2.2.0" + +scheduler@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4" + integrity sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + +schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.0, schema-utils@^2.6.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +scriptjs@^2.5.7: + version "2.5.9" + resolved "https://registry.yarnpkg.com/scriptjs/-/scriptjs-2.5.9.tgz#343915cd2ec2ed9bfdde2b9875cd28f59394b35f" + integrity sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg== + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= + +selfsigned@^1.10.7: + version "1.10.7" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b" + integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA== + dependencies: + node-forge "0.9.0" + +"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" + integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.4, setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shelljs@^0.8.3: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +sisteransi@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.4.tgz#386713f1ef688c7c0304dc4c0632898941cad2e3" + integrity sha512-/ekMoM4NJ59ivGSfKapeG+FWtrmWvA1p6FBZwXrqojw90vJu8lBmrTxCMuBCydKtkaUe2zt4PlxeTKpjwMbyig== + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= + +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +sockjs-client@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5" + integrity sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g== + dependencies: + debug "^3.2.5" + eventsource "^1.0.7" + faye-websocket "~0.11.1" + inherits "^2.0.3" + json3 "^3.3.2" + url-parse "^1.4.3" + +sockjs@0.3.19: + version "0.3.19" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" + integrity sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw== + dependencies: + faye-websocket "^0.10.0" + uuid "^3.0.1" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0= + dependencies: + is-plain-obj "^1.0.0" + +sort-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= + dependencies: + is-plain-obj "^1.0.0" + +source-list-map@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + +source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.4.15: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== + dependencies: + source-map "^0.5.6" + +source-map-support@^0.5.6: + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@~0.5.12: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.2: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.1.tgz#6f12ed1c5db7ea4f24ebb8b89ba58c87c08257f2" + integrity sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +ssri@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" + integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q== + dependencies: + figgy-pudding "^3.5.1" + +ssri@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-7.1.1.tgz#33e44f896a967158e3c63468e47ec46613b95b5f" + integrity sha512-w+daCzXN89PseTL99MkA+fxJEcU3wfaE/ah0i0lnOlpG1CYLJ2ZjzEry68YBKfLs4JfoTShrTEsJkAZuNZ/stw== + dependencies: + figgy-pudding "^3.5.1" + minipass "^3.1.1" + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +stack-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" + integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +stream-browserify@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-each@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" + integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-http@^2.7.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" + integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= + +string-length@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" + integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0= + dependencies: + astral-regex "^1.0.0" + strip-ansi "^4.0.0" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string.prototype.trim@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz#141233dff32c82bfad80684d7e5f0869ee0fb782" + integrity sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + +string.prototype.trimleft@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74" + integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string.prototype.trimright@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9" + integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string_decoder@^1.0.0, string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +style-loader@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.1.2.tgz#1b519c19faf548df6182b93e72ea1a4156022c2f" + integrity sha512-0Mpq1ZHFDCNq1F+6avNBgv+7q8V+mWRuzehxyJT+aKgzyN/yfKTwjYqaYwBgx+11UpQxL21zNQfzzlz+JcGURw== + dependencies: + loader-utils "^1.2.3" + schema-utils "^2.0.1" + +stylehacks@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" + integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +stylis-rule-sheet@^0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430" + integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw== + +stylis@^3.5.0: + version "3.5.4" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" + integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q== + +supports-color@6.1.0, supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svg-url-loader@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/svg-url-loader/-/svg-url-loader-3.0.3.tgz#95274eae80f4a46454a5b44e9582beb2f533465e" + integrity sha512-MKGiRNDs8fnHcZcPkhGcw9+130IXyFM9H8m6T7u3ScUuZYEeVzX0vNMru30D4MCF6vMYas5iw/Ru9lwFKBjaGw== + dependencies: + file-loader "~4.3.0" + loader-utils "~1.2.3" + +svgo@^1.0.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" + integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== + dependencies: + chalk "^2.4.1" + coa "^2.0.2" + css-select "^2.0.0" + css-select-base-adapter "^0.1.1" + css-tree "1.0.0-alpha.37" + csso "^4.0.2" + js-yaml "^3.13.1" + mkdirp "~0.5.1" + object.values "^1.1.0" + sax "~1.2.4" + stable "^0.1.8" + unquote "~1.1.1" + util.promisify "~1.0.0" + +symbol-tree@^3.2.2, symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +tabbable@^3.1.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-3.1.2.tgz#f2d16cccd01f400e38635c7181adfe0ad965a4a2" + integrity sha512-wjB6puVXTYO0BSFtCmWQubA/KIn7Xvajw0x0l6eJUudMG/EAiJvIUnyNX6xO4NpGrJ16lbD0eUseB9WxW0vlpQ== + +tapable@^1.0.0, tapable@^1.0.0-beta.5, tapable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +terser-webpack-plugin@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.2.tgz#6d3d1b0590c8f729bfbaeb7fb2528b8b62db4c74" + integrity sha512-SmvB/6gtEPv+CJ88MH5zDOsZdKXPS/Uzv2//e90+wM1IHFUhsguPKEILgzqrM1nQ4acRXN/SV4Obr55SXC+0oA== + dependencies: + cacache "^13.0.1" + find-cache-dir "^3.2.0" + jest-worker "^24.9.0" + schema-utils "^2.6.1" + serialize-javascript "^2.1.2" + source-map "^0.6.1" + terser "^4.4.3" + webpack-sources "^1.4.3" + +terser-webpack-plugin@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c" + integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA== + dependencies: + cacache "^12.0.2" + find-cache-dir "^2.1.0" + is-wsl "^1.1.0" + schema-utils "^1.0.0" + serialize-javascript "^2.1.2" + source-map "^0.6.1" + terser "^4.1.2" + webpack-sources "^1.4.0" + worker-farm "^1.7.0" + +terser@^4.1.2, terser@^4.4.3, terser@^4.8.1: + version "4.8.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" + integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + +test-exclude@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" + integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + read-pkg-up "^4.0.0" + require-main-filename "^2.0.0" + +throat@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" + integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= + +through2@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through2@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.4.2.tgz#dbf5866031151ec8352bb6c4db64a2292a840b9b" + integrity sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s= + dependencies: + readable-stream "~1.0.17" + xtend "~2.1.1" + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +time-stamp@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" + integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= + +timers-browserify@^2.0.4: + version "2.0.11" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" + integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ== + dependencies: + setimmediate "^1.0.4" + +timsort@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= + +tiny-invariant@^1.0.2: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73" + integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA== + +tiny-warning@^1.0.0, tiny-warning@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +tippy.js@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-5.1.2.tgz#5ac91233c59ab482ef5988cffe6e08bd26528e66" + integrity sha512-Qtrv2wqbRbaKMUb6bWWBQWPayvcDKNrGlvihxtsyowhT7RLGEh1STWuy6EMXC6QLkfKPB2MLnf8W2mzql9VDAw== + dependencies: + popper.js "^1.16.0" + +tmpl@1.0.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +toposort@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" + integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk= + +touch@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/touch/-/touch-2.0.2.tgz#ca0b2a3ae3211246a61b16ba9e6cbf1596287164" + integrity sha512-qjNtvsFXTRq7IuMLweVgFxmEuQ6gLbRs2jQxL80TtZ31dEKWYIxRXquij6w6VimyDek5hD3PytljHmEtAs2u0A== + dependencies: + nopt "~1.0.10" + +tough-cookie@^2.3.3, tough-cookie@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tough-cookie@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" + integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== + dependencies: + ip-regex "^2.1.0" + psl "^1.1.28" + punycode "^2.1.1" + +tough-cookie@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" + integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.1.2" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= + dependencies: + punycode "^2.1.0" + +tr46@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" + integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== + dependencies: + punycode "^2.1.1" + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= + +tryer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" + integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== + +ts-jest@24.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.3.0.tgz#b97814e3eab359ea840a1ac112deae68aa440869" + integrity sha512-Hb94C/+QRIgjVZlJyiWwouYUF+siNJHJHknyspaOcZ+OQAIdFG/UrdQVXw/0B8Z3No34xkUXZJpOTy9alOWdVQ== + dependencies: + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + mkdirp "0.x" + resolve "1.x" + semver "^5.5" + yargs-parser "10.x" + +ts-loader@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-6.2.1.tgz#67939d5772e8a8c6bdaf6277ca023a4812da02ef" + integrity sha512-Dd9FekWuABGgjE1g0TlQJ+4dFUfYGbYcs52/HQObE0ZmUNjQlmLAS7xXsSzy23AMaMwipsx5sNHvoEpT2CZq1g== + dependencies: + chalk "^2.3.0" + enhanced-resolve "^4.0.0" + loader-utils "^1.0.2" + micromatch "^4.0.0" + semver "^6.0.0" + +tsconfig-paths-webpack-plugin@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.2.0.tgz#6e70bd42915ad0efb64d3385163f0c1270f3e04d" + integrity sha512-S/gOOPOkV8rIL4LurZ1vUdYCVgo15iX9ZMJ6wx6w2OgcpT/G4wMyHB6WM+xheSqGMrWKuxFul+aXpCju3wmj/g== + dependencies: + chalk "^2.3.0" + enhanced-resolve "^4.0.0" + tsconfig-paths "^3.4.0" + +tsconfig-paths@^3.4.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + +tslib@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" + integrity sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ== + +tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +tslint-config-prettier@1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37" + integrity sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg== + +tslint-eslint-rules@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/tslint-eslint-rules/-/tslint-eslint-rules-5.4.0.tgz#e488cc9181bf193fe5cd7bfca213a7695f1737b5" + integrity sha512-WlSXE+J2vY/VPgIcqQuijMQiel+UtmXS+4nvK4ZzlDiqBfXse8FAvkNnTcYhnQyOTW5KFM+uRRGXxYhFpuBc6w== + dependencies: + doctrine "0.7.2" + tslib "1.9.0" + tsutils "^3.0.0" + +tslint-react@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/tslint-react/-/tslint-react-4.1.0.tgz#7153b724a8cfbea52423d0ffa469e8eba3bcc834" + integrity sha512-Y7CbFn09X7Mpg6rc7t/WPbmjx9xPI8p1RsQyiGCLWgDR6sh3+IBSlT+bEkc0PSZcWwClOkqq2wPsID8Vep6szQ== + dependencies: + tsutils "^3.9.1" + +tslint@5.20.1: + version "5.20.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d" + integrity sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg== + dependencies: + "@babel/code-frame" "^7.0.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^4.0.1" + glob "^7.1.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + mkdirp "^0.5.1" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.8.0" + tsutils "^2.29.0" + +tsutils@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== + dependencies: + tslib "^1.8.1" + +tsutils@^3.0.0, tsutils@^3.9.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + dependencies: + tslib "^1.8.1" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +typedoc-default-themes@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.7.0.tgz#ab02068a006d06443c1dce8ba157f5f16dc78e27" + integrity sha512-yeD56oPXMKJ5nDiCZ27x/SIxx11646Gr5GscxtLSmrh3ucMX6Lklgo7cSABafQXlGPSN5Kb/oLxmfN33BeqMWw== + dependencies: + backbone "^1.4.0" + jquery "^3.4.1" + lunr "^2.3.8" + underscore "^1.9.1" + +typedoc@0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.16.2.tgz#e32d0f99edffd0d3c3b08f8087f31133e0a55805" + integrity sha512-zaRJqcVzZorIP4oq7Y3AYAzf6C4ladwUXpvvedPOCOhdELVQbvLy6A8LlrE+svDtGrL7+K04ruHsN3KQESoYUw== + dependencies: + "@types/minimatch" "3.0.3" + fs-extra "^8.1.0" + handlebars "^4.7.0" + highlight.js "^9.17.1" + lodash "^4.17.15" + marked "^0.8.0" + minimatch "^3.0.0" + progress "^2.0.3" + shelljs "^0.8.3" + typedoc-default-themes "^0.7.0" + typescript "3.7.x" + +typescript@3.4.5: + version "3.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99" + integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw== + +typescript@3.7.4, typescript@3.7.x: + version "3.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.4.tgz#1743a5ec5fef6a1fa9f3e4708e33c81c73876c19" + integrity sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw== + +ua-parser-js@^0.7.18: + version "0.7.33" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" + integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw== + +uglify-js@3.4.x: + version "3.4.10" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" + integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== + dependencies: + commander "~2.19.0" + source-map "~0.6.1" + +uglify-js@^3.1.4: + version "3.15.2" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.2.tgz#1ed2c976f448063b1f87adb68c741be79959f951" + integrity sha512-peeoTk3hSwYdoc9nrdiEJk+gx1ALCtTjdYuKSXMTDqq7n1W7dHPqWDdSi+BPL0ni2YMeHD7hKUSdbj3TZauY2A== + +underscore@>=1.8.3, underscore@^1.9.1: + version "1.13.4" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.4.tgz#7886b46bbdf07f768e0052f1828e1dcab40c0dee" + integrity sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + +universalify@^0.1.0, universalify@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +unquote@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + +update-browserslist-db@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz#be06a5eedd62f107b7c19eb5bcefb194411abf38" + integrity sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +upper-case@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" + integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url-join@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== + +url-loader@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-3.0.0.tgz#9f1f11b371acf6e51ed15a50db635e02eec18368" + integrity sha512-a84JJbIA5xTFTWyjjcPdnsu+41o/SNE8SpXMdUvXs6Q+LuhCD9E2+0VCiuDWqgo3GGXVlFHzArDmBpj9PgWn4A== + dependencies: + loader-utils "^1.2.3" + mime "^2.4.4" + schema-utils "^2.5.0" + +url-parse@^1.4.3, url-parse@^1.4.7: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util.promisify@1.0.0, util.promisify@^1.0.0, util.promisify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= + dependencies: + inherits "2.0.1" + +util@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" + integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== + dependencies: + inherits "2.0.3" + +utila@^0.4.0, utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^3.0.1, uuid@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" + integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== + +v8-compile-cache@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" + integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vendors@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.3.tgz#a6467781abd366217c050f8202e7e50cc9eef8c0" + integrity sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +victory-area@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-area/-/victory-area-33.1.7.tgz#caa779f6cd2b2985d5ec4d228eeafbd7f53a2a1f" + integrity sha512-pfb9D/W8JydqE0fBH+wKM2tqixcXqIAxFXXm0juILfdhXtZd/Gkq3bjxY/PHPgkISCk1YMwGVuEtWzgV+qhnTw== + dependencies: + d3-shape "^1.2.0" + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-axis@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-axis/-/victory-axis-33.1.7.tgz#276dd0fd63b73085b07e9539ae61849c9dc5df2e" + integrity sha512-czWZ3LMiOSv979gz3SamiMEQw6HfmZsJUHpZPmWgI9XyvFKAEbBzuzVDfqx/NRcVHOfMkFzYvRtsDNfhG5+S2Q== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-bar@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-bar/-/victory-bar-33.1.7.tgz#fac24b159f4e514dd93b35f1a4455659224796bf" + integrity sha512-Yhqk34mbZ3fpxPQaZ+njg7uRPv/9/LmbGOmLdc/uBFrYZloVAvu3SgIqihLT4T18JSWzYg8czf9HgIbbpeRpyA== + dependencies: + d3-shape "^1.2.0" + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-box-plot@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-box-plot/-/victory-box-plot-33.1.7.tgz#9093d325cff07d2e07862427ef4759b3d4f9781d" + integrity sha512-l8xMU2VtiFNKcPeB3B85uyMHLMD6N5Y3044RGz7PPOx58I1RG4dOYOBQIWbvXZmkHbakpZQSiJE8aFzOhC4kUw== + dependencies: + d3-array "^1.2.0" + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-brush-container@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-brush-container/-/victory-brush-container-33.1.7.tgz#35b7dca2f0e065fa2770c3a8e7152b31dd4e6cbc" + integrity sha512-X7bJ3J2utGSMq6O/XBu8zHWXICcra43LyU6oUq1T+S2hu1/QKdnEaSV8rkDXSCaha1VH5x0pl02ql5CJpxQcVQ== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-brush-line@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-brush-line/-/victory-brush-line-33.1.7.tgz#0feacb1195ef2e3487afaf8f1379360eb30f0942" + integrity sha512-RvnSGwlWgSVSwpGv3KbQkchFq0tyx4cjssco7FTXhSF1fjHjPBy3NA6JFq2BDsYjw3GYsrX6hldtTkwPhfgw0Q== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-candlestick@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-candlestick/-/victory-candlestick-33.1.7.tgz#01c7c37c98179f8131c23d1ffe87e2f963ab70c6" + integrity sha512-cqhe8rgzBRsATQrYEQVlLO60MzyJ/gyRJgZ3s3T2HrGSLpca/P18ba5BckKw3yLM7t94kVCnJQj2eHJjNejp8g== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-chart@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-chart/-/victory-chart-33.1.7.tgz#0f0142cdef5e12141e0ea92f09a4a52a62eb1d05" + integrity sha512-vmQZLjY/vghD2EmqDDdXn9Xhdx9WEvIr2Z4CFS4WvVNTzrucYo5SQcu25K2/lCojiIQFRKkR2BZHUFOT0369ag== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + react-fast-compare "^2.0.0" + victory-axis "^33.1.7" + victory-core "^33.1.7" + victory-polar-axis "^33.1.7" + victory-shared-events "^33.1.7" + +victory-core@^33.0.1, victory-core@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-core/-/victory-core-33.1.7.tgz#9594f3f767aabffdad64b2c4ce72ee14477bd3ab" + integrity sha512-PhFl8hi+osOEh2XCGLTUcFO3jXt/8uLoaWiuCoYlsC4zxeLY2j/mgIw4lz4VmCDZ9kW4oOErBUoeLL41YBQnGQ== + dependencies: + d3-ease "^1.0.0" + d3-interpolate "^1.1.1" + d3-scale "^1.0.0" + d3-shape "^1.2.0" + d3-timer "^1.0.0" + lodash "^4.17.15" + prop-types "^15.5.8" + react-fast-compare "^2.0.0" + +victory-create-container@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-create-container/-/victory-create-container-33.1.7.tgz#48e37161f22a6f4aec2218cc98f925bf50edd131" + integrity sha512-EF7OyaZLzzRyjl+xM+QwowikuASmJsh0/j/UJM4U99E8JZ+o1P2n/ZBd1CkcVpyROSIRzmjnLpH33gLc5B/K4g== + dependencies: + lodash "^4.17.15" + victory-brush-container "^33.1.7" + victory-core "^33.1.7" + victory-cursor-container "^33.1.7" + victory-selection-container "^33.1.7" + victory-voronoi-container "^33.1.7" + victory-zoom-container "^33.1.7" + +victory-cursor-container@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-cursor-container/-/victory-cursor-container-33.1.7.tgz#5e284d898afdbfe1112a9614d5b231a4a11d3c0a" + integrity sha512-yIXndlVJlI8FbbuoJFBlfUwZ36/j8jVlBaYSxUVZqo85BkBFOLudXzBKSDyQ93+isAJLzVqGNKLFY2v3MiCVbw== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-errorbar@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-errorbar/-/victory-errorbar-33.1.7.tgz#ab1148d2913bf38991c02630609b6b74f8f3c5ab" + integrity sha512-lDTOPX/krcBhPGhak+ENbQv9sHyco4Wp8uVXVVN67cppn3mg0P3HIGYGM5b+9hzt53UZqQZbzxb3efyFGtgflA== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-group@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-group/-/victory-group-33.1.7.tgz#a45a27e9eeb46d0e9a673ebebc0be8f7f215a847" + integrity sha512-jAlRl/v1oemOrOdhV2HZiCLv+U2CFRNfwxfP3f85FzP8NY0sRcsN+1ePDkbnKRRDNX0Wkbmij+kPedgIvNyPPA== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + react-fast-compare "^2.0.0" + victory-core "^33.1.7" + +victory-legend@^33.0.1, victory-legend@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-legend/-/victory-legend-33.1.7.tgz#8bfb2fc45d059e3c4942c5341588aa993aea651b" + integrity sha512-PPtFIDVlVtTet8PLtScktHYPVd4lWdrB10meeN+J2D/G0WHu7fCz9X+lETURE2ml3OXBmPRN9h4PrspvlwcX9A== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-line@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-line/-/victory-line-33.1.7.tgz#eb91aa1f7cdbd036aa012e3655dd0406f8c26efa" + integrity sha512-fAR/gcuqTX4B/gX+GWzhnt9emfqbPh++2EFKLWOK8Z0+2W+yLkHSBifjbZfcD8/X8KDXRSX9qAsbLdYc8rt8xg== + dependencies: + d3-shape "^1.2.0" + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-pie@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-pie/-/victory-pie-33.1.7.tgz#60010d4a6f8c65666aaba55f982c71cd8ef052ab" + integrity sha512-bxY3nmFWkHMfr84xV8R1gHpysg+oLN9zosadzLX6VP9Agmhx977P/G0MJe8Rs80zgt1RzUx0QRNUne/8MCBvzQ== + dependencies: + d3-shape "^1.0.0" + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-polar-axis@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-polar-axis/-/victory-polar-axis-33.1.7.tgz#0f7d5378eb164b61b749127a64a5f79b0e940162" + integrity sha512-AOYxJIS2FeGXu1+CykCGoaVvOL88UWQXBVdHmnDKeS8amb281OZkQPy8vFVSpjcd1Y2Q15QzSOVVRmpvWxPYdg== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-scatter@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-scatter/-/victory-scatter-33.1.7.tgz#d3dd40113e2417f4be05da5348a99dba56301079" + integrity sha512-OUrQYS/526V1+M9iDnsX3LnmJBAeGFavTuwYqJMiV4k6Ca+Kvj22KMt0SkA5FA+i60c2338KbA59g7IaCcIZSw== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-selection-container@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-selection-container/-/victory-selection-container-33.1.7.tgz#4d5fc80301113705aaffa0d6509d191902a93d02" + integrity sha512-gUxLWCF95PIhi9nAISsbICf31XKSickm7p1v3HRCOP0Ggd1ItsQqKT2NwIimYsAUeir6So3iZWE8nuQ+SJs+kg== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-shared-events@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-shared-events/-/victory-shared-events-33.1.7.tgz#7b129655475b0de7939007a55b6c390a8a01f1c6" + integrity sha512-Ynqe2DKmrPgre2VDvWXNouHzG+JVPf1HA8ocqtvX3zhF2CwqSGu3uWy7bOR+rJaz95iBxeDmh4312oezCvKuQQ== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + react-fast-compare "^2.0.0" + victory-core "^33.1.7" + +victory-stack@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-stack/-/victory-stack-33.1.7.tgz#65bc0067e77d7ec2d921401035f005c6f74770f7" + integrity sha512-+ymudHqhCUPHB3Uv3ciHKZW4inbL4vJQ2TGYq4q4jFhpAqS8mHZBq7Ppq92fAKpOr6XcKKQS9UhaewyEPqLkHw== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + react-fast-compare "^2.0.0" + victory-core "^33.1.7" + +victory-tooltip@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-tooltip/-/victory-tooltip-33.1.7.tgz#660240a6a6f2138d389fc7b0d1f47ed59070c65a" + integrity sha512-M3N/3sx5xwtAQJjTFNQw8KlRBHQOOgSxjk1WyBbLv4zls/zu2reF0czbiFxKsgkVuf+yoxp76quHUId0hS58pg== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-voronoi-container@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-voronoi-container/-/victory-voronoi-container-33.1.7.tgz#3a53b6d4946f66ab83c7fc47fad0e0bf69c4162b" + integrity sha512-rGV9U/jbVWRlU1E7OxYp/ywDjrLX9F9JeXd94rajzS3kJoNFZv6huGgMZN+48ErvMK5WK9kRxFOz8a7dHZ8Tug== + dependencies: + delaunay-find "0.0.5" + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + victory-tooltip "^33.1.7" + +victory-voronoi@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-voronoi/-/victory-voronoi-33.1.7.tgz#d08e24a8ae4726d9ee29dc20750cb1e979d5acab" + integrity sha512-4zadtdb30ok1qbPUc58+zu1AFN4CBtLXVGjxHYGCUtJEg++r+1cM1Q7olunukjTRpKXrypoW2HFQ++33WzTsPw== + dependencies: + d3-voronoi "^1.1.2" + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory-zoom-container@^33.1.7: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory-zoom-container/-/victory-zoom-container-33.1.7.tgz#2fc763c9da3a64b57645851e470b5bd090a5be40" + integrity sha512-Xnr2xBGX0pbghzlFdX4hFIRN2LGjeS0+jgdHiFTQN1fnJibGx/wz5ePd+iHKuu18cq25xezQrlhJLBqSbdFjzw== + dependencies: + lodash "^4.17.15" + prop-types "^15.5.8" + victory-core "^33.1.7" + +victory@^33.0.5: + version "33.1.7" + resolved "https://registry.yarnpkg.com/victory/-/victory-33.1.7.tgz#40b32d919bd894f55cb1386007d53b7db622e7a2" + integrity sha512-/ni6zq2wv+ISwWQYUMiBF8Sy7mkPJ799o1BvEAiyBATaASa93fM1I0+0T3kF63D0BPxBinTXPokbI5fw7O8Hxw== + dependencies: + victory-area "^33.1.7" + victory-axis "^33.1.7" + victory-bar "^33.1.7" + victory-box-plot "^33.1.7" + victory-brush-container "^33.1.7" + victory-brush-line "^33.1.7" + victory-candlestick "^33.1.7" + victory-chart "^33.1.7" + victory-core "^33.1.7" + victory-create-container "^33.1.7" + victory-cursor-container "^33.1.7" + victory-errorbar "^33.1.7" + victory-group "^33.1.7" + victory-legend "^33.1.7" + victory-line "^33.1.7" + victory-pie "^33.1.7" + victory-polar-axis "^33.1.7" + victory-scatter "^33.1.7" + victory-selection-container "^33.1.7" + victory-shared-events "^33.1.7" + victory-stack "^33.1.7" + victory-tooltip "^33.1.7" + victory-voronoi "^33.1.7" + victory-voronoi-container "^33.1.7" + victory-zoom-container "^33.1.7" + +vm-browserify@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== + +w3c-hr-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" + integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU= + dependencies: + browser-process-hrtime "^0.1.2" + +w3c-hr-time@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" + integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== + dependencies: + browser-process-hrtime "^1.0.0" + +w3c-xmlserializer@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794" + integrity sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg== + dependencies: + domexception "^1.0.1" + webidl-conversions "^4.0.2" + xml-name-validator "^3.0.0" + +w3c-xmlserializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" + integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== + dependencies: + xml-name-validator "^3.0.0" + +walker@^1.0.7, walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= + dependencies: + makeerror "1.0.x" + +watchpack@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" + integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== + dependencies: + chokidar "^2.0.2" + graceful-fs "^4.1.2" + neo-async "^2.5.0" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +web-streams-polyfill@^3.0.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + +webdav@2.10.1: + version "2.10.1" + resolved "https://registry.yarnpkg.com/webdav/-/webdav-2.10.1.tgz#ccf932ee76e9df90bbdba371cbd92b61ddf01f3f" + integrity sha512-3UfnjGTAqSM9MW3Rpt1KrY1KneYK0wPCFryHTncqw1OP1pyiniT3uYhVpgmH6za/TkWOfnTnKCDKhwrLJFdzow== + dependencies: + axios "^0.19.0" + base-64 "^0.1.0" + hot-patcher "^0.5.0" + merge "^1.2.1" + minimatch "^3.0.4" + path-posix "^1.0.0" + url-join "^4.0.1" + url-parse "^1.4.7" + xml2js "^0.4.19" + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + +webidl-conversions@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" + integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== + +webpack-bundle-analyzer@3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.6.0.tgz#39b3a8f829ca044682bc6f9e011c95deb554aefd" + integrity sha512-orUfvVYEfBMDXgEKAKVvab5iQ2wXneIEorGNsyuOyVYpjYrI7CUOhhXNDd3huMwQ3vNNWWlGP+hzflMFYNzi2g== + dependencies: + acorn "^6.0.7" + acorn-walk "^6.1.1" + bfj "^6.1.1" + chalk "^2.4.1" + commander "^2.18.0" + ejs "^2.6.1" + express "^4.16.3" + filesize "^3.6.1" + gzip-size "^5.0.0" + lodash "^4.17.15" + mkdirp "^0.5.1" + opener "^1.5.1" + ws "^6.0.0" + +webpack-cli@3.3.10: + version "3.3.10" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.10.tgz#17b279267e9b4fb549023fae170da8e6e766da13" + integrity sha512-u1dgND9+MXaEt74sJR4PR7qkPxXUSQ0RXYq8x1L6Jg1MYVEmGPrH6Ah6C4arD4r0J1P5HKjRqpab36k0eIzPqg== + dependencies: + chalk "2.4.2" + cross-spawn "6.0.5" + enhanced-resolve "4.1.0" + findup-sync "3.0.0" + global-modules "2.0.0" + import-local "2.0.0" + interpret "1.2.0" + loader-utils "1.2.3" + supports-color "6.1.0" + v8-compile-cache "2.0.3" + yargs "13.2.4" + +webpack-dev-middleware@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3" + integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw== + dependencies: + memory-fs "^0.4.1" + mime "^2.4.4" + mkdirp "^0.5.1" + range-parser "^1.2.1" + webpack-log "^2.0.0" + +webpack-dev-server@3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.10.1.tgz#1ff3e5cccf8e0897aa3f5909c654e623f69b1c0e" + integrity sha512-AGG4+XrrXn4rbZUueyNrQgO4KGnol+0wm3MPdqGLmmA+NofZl3blZQKxZ9BND6RDNuvAK9OMYClhjOSnxpWRoA== + dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^2.1.8" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + debug "^4.1.1" + del "^4.1.1" + express "^4.17.1" + html-entities "^1.2.1" + http-proxy-middleware "0.19.1" + import-local "^2.0.0" + internal-ip "^4.3.0" + ip "^1.1.5" + is-absolute-url "^3.0.3" + killable "^1.0.1" + loglevel "^1.6.6" + opn "^5.5.0" + p-retry "^3.0.1" + portfinder "^1.0.25" + schema-utils "^1.0.0" + selfsigned "^1.10.7" + semver "^6.3.0" + serve-index "^1.9.1" + sockjs "0.3.19" + sockjs-client "1.4.0" + spdy "^4.0.1" + strip-ansi "^3.0.1" + supports-color "^6.1.0" + url "^0.11.0" + webpack-dev-middleware "^3.7.2" + webpack-log "^2.0.0" + ws "^6.2.1" + yargs "12.0.5" + +webpack-log@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" + integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== + dependencies: + ansi-colors "^3.0.0" + uuid "^3.3.2" + +webpack-merge@4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-sources@^1.0.1, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack@4.41.5: + version "4.41.5" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.5.tgz#3210f1886bce5310e62bb97204d18c263341b77c" + integrity sha512-wp0Co4vpyumnp3KlkmpM5LWuzvZYayDwM2n17EHFr4qxBBbRokC7DJawPJC7TfSFZ9HZ6GsdH40EBj4UV0nmpw== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/wasm-edit" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + acorn "^6.2.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.1.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.1" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.3" + watchpack "^1.6.0" + webpack-sources "^1.4.1" + +websocket-driver@>=0.5.1: + version "0.7.3" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" + integrity sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg== + dependencies: + http-parser-js ">=0.4.0 <0.4.11" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-fetch@>=0.10.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" + integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== + +whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^6.4.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +whatwg-url@^8.0.0, whatwg-url@^8.5.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" + integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== + dependencies: + lodash "^4.7.0" + tr46 "^2.1.0" + webidl-conversions "^6.1.0" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +word-wrap@~1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +worker-farm@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" + integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== + dependencies: + errno "~0.1.7" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.1.tgz#d0b05463c188ae804396fd5ab2a370062af87529" + integrity sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write-file-atomic@^2.0.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write-json-file@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-2.3.0.tgz#2b64c8a33004d54b8698c76d585a77ceb61da32f" + integrity sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8= + dependencies: + detect-indent "^5.0.0" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + pify "^3.0.0" + sort-keys "^2.0.0" + write-file-atomic "^2.0.0" + +ws@^5.2.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d" + integrity sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA== + dependencies: + async-limiter "~1.0.0" + +ws@^6.0.0, ws@^6.2.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" + integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== + dependencies: + async-limiter "~1.0.0" + +ws@^7.0.0, ws@^7.4.6: + version "7.5.9" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" + integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +xml2js@^0.4.19: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + +xmlchars@^2.1.1, xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +xtend@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" + integrity sha1-bv7MKk2tjmlixJAbM3znuoe10os= + dependencies: + object-keys "~0.4.0" + +"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.7.2.tgz#f26aabf738590ab61efaca502358e48dc9f348b2" + integrity sha512-qXROVp90sb83XtAoqE8bP9RwAkTTZbugRUTm5YeFCBfNRPEp2YzTeqWiz7m5OORHzEvrA/qcGS8hp/E+MMROYw== + dependencies: + "@babel/runtime" "^7.6.3" + +yargs-parser@10.x: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== + dependencies: + camelcase "^4.1.0" + +yargs-parser@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" + integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^13.1.0, yargs-parser@^13.1.1: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@12.0.5: + version "12.0.5" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" + integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== + dependencies: + cliui "^4.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^1.0.1" + os-locale "^3.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^11.1.1" + +yargs@13.2.4: + version "13.2.4" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" + integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + os-locale "^3.1.0" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.0" + +yargs@^13.3.0: + version "13.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" + integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.1" diff --git a/tests/AFC-System-SUT-Compliance-Test-Vectors.txt b/tests/AFC-System-SUT-Compliance-Test-Vectors.txt new file mode 100644 index 0000000..fd27a73 --- /dev/null +++ b/tests/AFC-System-SUT-Compliance-Test-Vectors.txt @@ -0,0 +1,132 @@ +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SRS1","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SRS1"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180621, "longitude": -97.560614}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-SRS1"}],"version": "1.4"}, {"testCaseId": "AFCS.SRS.1"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "URS1","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": ""}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 34.051151, "longitude": -118.255078}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-URS1"}],"version": "1.4"}, {"testCaseId": "AFCS.URS.1"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-URS2"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.723655, "longitude": -87.683357}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-URS2"}],"version": "1.4"}, {"testCaseId": "AFCS.URS.2"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "URS3","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-URS3"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": { "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-URS3"}],"version": "1.4"}, {"testCaseId": "AFCS.URS.3"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "URS4","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-URS4"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 29.75077, "longitude": -95.36454}}},"requestId": "REQ-URS4"}],"version": "1.4"}, {"testCaseId": "AFCS.URS.4"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "URS5","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-URS5"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL"}, "ellipse": {"center": {"latitude": 39.949079, "longitude": -75.161307}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-URS5"}],"version": "1.4"}, {"testCaseId": "AFCS.URS.5"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "URS6","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-URS6"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.449081, "longitude": -112.081081}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-URS6"}],"version": "1.4"}, {"testCaseId": "AFCS.URS.6"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "URS7","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-URS7"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": -51.692741, "longitude": -57.85685}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-URS7"}],"version": "1.4"}, {"testCaseId": "AFCS.URS.7"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP1","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP1"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180621, "longitude": -97.560614}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP1"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.1"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP2","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP2"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.177062, "longitude": -97.546817}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP2"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.2"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP3","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP3"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180553, "longitude": -97.560701}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP3"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.3"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP4","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP4"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.879231, "longitude": -87.636215}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP4"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.4"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP5","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP5"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.878912, "longitude": -87.635929}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP5"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.5"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP6","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP6"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.878912, "longitude": -87.635929}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP6"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.6"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP7","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP7"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": -3}, "ellipse": {"center": {"latitude": 33.180621, "longitude": -97.560614}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP7"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.7"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP8","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP8"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.177062, "longitude": -97.546817}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP8"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.8"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP9","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP9"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180553, "longitude": -97.560701}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP9"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.9"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP10","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP10"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.879231, "longitude": -87.636215}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP10"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.10"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP11","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP11"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.878912, "longitude": -87.635929}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP11"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.11"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP12","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP12"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.878912, "longitude": -87.635929}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP12"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.12"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP13","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP13"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.769641, "longitude": -118.376295}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}},"requestId": "REQ-FSP13"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.13"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP14","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP14"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.770381, "longitude": -118.376872}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}},"requestId": "REQ-FSP14"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.14"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP15","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP15"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.770381, "longitude": -118.376872}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}},"requestId": "REQ-FSP15"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.15"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP16","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP16"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 33.769641, "longitude": -118.376295}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}},"requestId": "REQ-FSP16"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.16"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP17","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP17"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 33.772642, "longitude": -118.375067}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}},"requestId": "REQ-FSP17"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.17"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP18","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP18"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 33.772642, "longitude": -118.375067}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}},"requestId": "REQ-FSP18"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.18"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP19","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP19"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.769641, "longitude": -118.376295}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}},"requestId": "REQ-FSP19"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.19"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP20","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP20"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.770381, "longitude": -118.376872}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}},"requestId": "REQ-FSP20"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.20"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP21","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP21"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.770381, "longitude": -118.376872}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}},"requestId": "REQ-FSP21"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.21"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP22","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP22"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 33.769641, "longitude": -118.376295}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}},"requestId": "REQ-FSP22"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.22"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP23","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP23"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 33.772642, "longitude": -118.375067}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}},"requestId": "REQ-FSP23"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.23"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP24","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP24"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 33.772642, "longitude": -118.375067}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}},"requestId": "REQ-FSP24"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.24"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP25","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP25"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 30.571694, "longitude": -102.230361}, "orientation": 0, "minorAxis": 100, "majorAxis": 100}},"requestId": "REQ-FSP25"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.25"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP26","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP26"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 30.573949, "longitude": -102.234875}, "orientation": 0, "minorAxis": 300, "majorAxis": 300}},"requestId": "REQ-FSP26"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.26"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP27","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP27"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 30.086965, "longitude": -101.103761}, "orientation": 70, "minorAxis": 250, "majorAxis": 250}},"requestId": "REQ-FSP27"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.27"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP28","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP28"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 30.571694, "longitude": -102.230361}, "orientation": 0, "minorAxis": 100, "majorAxis": 100}},"requestId": "REQ-FSP28"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.28"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP29","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP29"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 30.573949, "longitude": -102.234875}, "orientation": 0, "minorAxis": 300, "majorAxis": 300}},"requestId": "REQ-FSP29"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.29"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP30","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP30"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 30.086965, "longitude": -101.103761}, "orientation": 70, "minorAxis": 250, "majorAxis": 250}},"requestId": "REQ-FSP30"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.30"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP31","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP31"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "linearPolygon": {"outerBoundary": [{"latitude": 30.5725933, "longitude": -102.231406}, {"latitude": 30.5725933, "longitude": -102.229316}, {"latitude": 30.570795, "longitude": -102.229316}, {"latitude": 30.570795, "longitude": -102.231406}]}},"requestId": "REQ-FSP31"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.31"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP32","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP32"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "radialPolygon": {"outerBoundary": [{"length": 300, "angle": 0}, {"length": 300, "angle": 36}, {"length": 300, "angle": 72}, {"length": 300, "angle": 108}, {"length": 300, "angle": 144}, {"length": 300, "angle": 180}, {"length": 300, "angle": 216}, {"length": 300, "angle": 252}, {"length": 300, "angle": 288}, {"length": 300, "angle": 324}], "center": {"latitude": 30.573949, "longitude": -102.234875}}},"requestId": "REQ-FSP32"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.32"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP33","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP33"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 30.086965, "longitude": -101.103761}, "orientation": 70, "minorAxis": 250, "majorAxis": 250}},"requestId": "REQ-FSP33"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.33"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP34","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP34"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 30.571694, "longitude": -102.230361}, "orientation": 0, "minorAxis": 100, "majorAxis": 100}},"requestId": "REQ-FSP34"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.34"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP35","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP35"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 30.573949, "longitude": -102.234875}, "orientation": 0, "minorAxis": 300, "majorAxis": 300}},"requestId": "REQ-FSP35"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.35"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP36","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP36"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 30.086965, "longitude": -101.103761}, "orientation": 70, "minorAxis": 250, "majorAxis": 250}},"requestId": "REQ-FSP36"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.36"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP37","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP37"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 9}, "ellipse": {"center": {"latitude": 34.0517490391756, "longitude": -118.174086769162}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP37"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.37"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP38","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP38"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 55}, "ellipse": {"center": {"latitude": 33.44493, "longitude": -112.067148}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP38"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.38"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP39","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP39"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 7}, "ellipse": {"center": {"latitude": 33.867634, "longitude": -118.037267}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP39"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.39"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP40","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP40"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 89}, "ellipse": {"center": {"latitude": 33.4657921944995, "longitude": -111.969947953029}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP40"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.40"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP41","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP41"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 9}, "ellipse": {"center": {"latitude": 32.780716, "longitude": -117.134037}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}},"requestId": "REQ-FSP41"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.41"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP42","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP42"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 8}, "ellipse": {"center": {"latitude": 32.773875, "longitude": -117.139232}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}},"requestId": "REQ-FSP42"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.42"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP43","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP43"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 83}, "ellipse": {"center": {"latitude": 39.792935, "longitude": -105.018517}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}},"requestId": "REQ-FSP43"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.43"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP44","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP44"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 9}, "ellipse": {"center": {"latitude": 34.0517490391756, "longitude": -118.174086769162}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP44"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.44"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP45","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP45"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 55}, "ellipse": {"center": {"latitude": 33.44493, "longitude": -112.067148}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP45"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.45"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP46","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP46"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 7}, "ellipse": {"center": {"latitude": 33.867634, "longitude": -118.037267}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP46"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.46"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP47","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP47"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 89}, "ellipse": {"center": {"latitude": 33.4657921944995, "longitude": -111.969947953029}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP47"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.47"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP48","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP48"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 9}, "ellipse": {"center": {"latitude": 32.780716, "longitude": -117.134037}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}},"requestId": "REQ-FSP48"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.48"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP49","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP49"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 8}, "ellipse": {"center": {"latitude": 32.773875, "longitude": -117.139232}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}},"requestId": "REQ-FSP49"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.49"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP50","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP50"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 83}, "ellipse": {"center": {"latitude": 39.792935, "longitude": -105.018517}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}},"requestId": "REQ-FSP50"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.50"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131, "channelCfi": [21, 25, 29, 33]}, {"globalOperatingClass": 132, "channelCfi": [19, 27, 35]}, {"globalOperatingClass": 133, "channelCfi": [23, 39]}, {"globalOperatingClass": 134, "channelCfi": [15, 47]}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP51","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP51"}]},"inquiredFrequencyRange": [{"lowFrequency": 6048, "highFrequency": 6109}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.892312, "longitude": -87.609841}, "orientation": 0, "minorAxis": 5, "majorAxis": 10}},"requestId": "REQ-FSP51"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.51"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131, "channelCfi": [21, 25, 29, 33]}, {"globalOperatingClass": 132, "channelCfi": [19, 27, 35]}, {"globalOperatingClass": 133, "channelCfi": [23, 39]}, {"globalOperatingClass": 134, "channelCfi": [15, 47]}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP52","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP52"}]},"inquiredFrequencyRange": [{"lowFrequency": 6048, "highFrequency": 6109}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.892312, "longitude": -87.609841}, "orientation": 0, "minorAxis": 5, "majorAxis": 10}},"requestId": "REQ-FSP52"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.52"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131, "channelCfi": [81, 85, 89]}, {"globalOperatingClass": 132, "channelCfi": [83, 91]}, {"globalOperatingClass": 133, "channelCfi": [87]}, {"globalOperatingClass": 134, "channelCfi": [79]}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP53","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP53"}]},"inquiredFrequencyRange": [{"lowFrequency": 6360, "highFrequency": 6391}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 42.333582, "longitude": -83.053009}, "orientation": 10, "minorAxis": 5, "majorAxis": 5}},"requestId": "REQ-FSP53"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.53"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131, "channelCfi": [81, 85, 89]}, {"globalOperatingClass": 132, "channelCfi": [83, 91]}, {"globalOperatingClass": 133, "channelCfi": [87]}, {"globalOperatingClass": 134, "channelCfi": [79]}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP54","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP54"}]},"inquiredFrequencyRange": [{"lowFrequency": 6360, "highFrequency": 6391}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 42.333582, "longitude": -83.053009}, "orientation": 10, "minorAxis": 5, "majorAxis": 5}},"requestId": "REQ-FSP54"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.54"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131, "channelCfi": [13, 17, 21, 25]}, {"globalOperatingClass": 132, "channelCfi": [11, 19,27]}, {"globalOperatingClass": 133, "channelCfi": [7, 23]}, {"globalOperatingClass": 134, "channelCfi": [15]}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP55","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP55"}]},"inquiredFrequencyRange": [{"lowFrequency": 6019, "highFrequency": 6079}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 39.286173, "longitude": -76.606187}, "orientation": 10, "minorAxis": 5, "majorAxis": 10}},"requestId": "REQ-FSP55"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.55"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131, "channelCfi": [13, 17, 21, 25]}, {"globalOperatingClass": 132, "channelCfi": [11, 19,27]}, {"globalOperatingClass": 133, "channelCfi": [7, 23]}, {"globalOperatingClass": 134, "channelCfi": [15]}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP56","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP56"}]},"inquiredFrequencyRange": [{"lowFrequency": 6019, "highFrequency": 6079}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 39.286173, "longitude": -76.606187}, "orientation": 10, "minorAxis": 5, "majorAxis": 10}},"requestId": "REQ-FSP56"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.56"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP57","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP57"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 61.068, "longitude": -152.0359}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}},"requestId": "REQ-FSP57"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.57"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP58","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP58"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 64.203, "longitude": -149.3355}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}},"requestId": "REQ-FSP58"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.58"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP59","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP59"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 69.154632, "longitude": -148.899575}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}},"requestId": "REQ-FSP59"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.59"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP60","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP60"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 70.328691, "longitude": -149.63919}, "orientation": 0, "minorAxis": 25, "majorAxis": 25}},"requestId": "REQ-FSP60"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.60"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP61","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP61"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 70.328372, "longitude": -149.637287}, "orientation": 0, "minorAxis": 25, "majorAxis": 25}},"requestId": "REQ-FSP61"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.61"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP62","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP62"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 38.823357, "longitude": -120.68475}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP62"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.62"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP63","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP63"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 38.820129, "longitude": -120.68426}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP63"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.63"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP64","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP64"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 38.816705, "longitude": -120.695567}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP64"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.64"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP65","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP65"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 38.823357, "longitude": -120.68475}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP65"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.65"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP66","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP66"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 38.820129, "longitude": -120.68426}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP66"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.66"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP67","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP67"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 38.816705, "longitude": -120.695567}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP67"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.67"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP68","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP68"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 38.823357, "longitude": -120.68475}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP68"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.68"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP69","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP69"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 38.820129, "longitude": -120.68426}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP69"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.69"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP70","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP70"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 38.816705, "longitude": -120.695567}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP70"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.70"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP71","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP71"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.095169, "longitude": -116.42293}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP71"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.71"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP72","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP72"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.073958, "longitude": -116.421737}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP72"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.72"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP73","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP73"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.095169, "longitude": -116.42293}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP73"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.73"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP74","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP74"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.073958, "longitude": -116.421737}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP74"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.74"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP75","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP75"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.095169, "longitude": -116.42293}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP75"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.75"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP76","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP76"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.073958, "longitude": -116.421737}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP76"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.76"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP77","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP77"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 39.523761, "longitude": -121.300259}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP77"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.77"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP78","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP78"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 39.519614, "longitude": -121.275352}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP78"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.78"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP79","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP79"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 39.523761, "longitude": -121.300259}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP79"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.79"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP80","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP80"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 39.51883, "longitude": -121.301513}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP80"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.80"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP81","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP81"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 39.519614, "longitude": -121.275352}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP81"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.81"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP82","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP82"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 39.523761, "longitude": -121.300259}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP82"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.82"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP83","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP83"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 39.51883, "longitude": -121.301513}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP83"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.83"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP84","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP84"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 39.519614, "longitude": -121.275352}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP84"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.84"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP85","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP85"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.684652, "longitude": -76.483668}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP85"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.85"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP86","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP86"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.711802, "longitude": -76.473996}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP86"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.86"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP87","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP87"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.681608, "longitude": -76.482183}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP87"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.87"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP88","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP88"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.684652, "longitude": -76.483668}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP88"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.88"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP89","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP89"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.711802, "longitude": -76.473996}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP89"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.89"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP90","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP90"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.681608, "longitude": -76.482183}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP90"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.90"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP91","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP91"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.684652, "longitude": -76.483668}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP91"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.91"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP92","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP92"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.711802, "longitude": -76.473996}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP92"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.92"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP93","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP93"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 47.608377, "longitude": -122.327159}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP93"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.93"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP94","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP94"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 47.608377, "longitude": -122.327159}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP94"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.94"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP95","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP95"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 47.608377, "longitude": -122.327159}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP95"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.95"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP96","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP96"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 47.608377, "longitude": -122.327159}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP96"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.96"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP97","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP97"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 47.608377, "longitude": -122.327159}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP97"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.97"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP98","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP98"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 18}, "ellipse": {"center": {"latitude": 47.747233, "longitude": -121.088367}, "orientation": 45, "minorAxis": 30, "majorAxis": 30}},"requestId": "REQ-FSP98"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.98"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP99","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP99"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 47.741269, "longitude": -121.077035}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP99"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.99"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP1","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP1"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180621, "longitude": -97.560614}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP1"},{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP2","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP2"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.177062, "longitude": -97.546817}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP2"},{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP3","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP3"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180553, "longitude": -97.560701}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP3"},{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP4","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP4"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.879231, "longitude": -87.636215}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP4"},{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP5","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP5"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.878912, "longitude": -87.635929}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP5"},{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP6","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP6"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.878912, "longitude": -87.635929}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}},"requestId": "REQ-FSP6"}],"version": "1.4"}, {"testCaseId": "AFCS.FSP.100"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP1","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP1"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 46.4968, "longitude": -84.331771}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}},"requestId": "REQ-IBP1"}],"version": "1.4"}, {"testCaseId": "AFCS.IBP.1"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP2","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP2"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 48.359797, "longitude": -92.155281}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}},"requestId": "REQ-IBP2"}],"version": "1.4"}, {"testCaseId": "AFCS.IBP.2"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP3","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP3"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 54.90827, "longitude": -130.838902}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}},"requestId": "REQ-IBP3"}],"version": "1.4"}, {"testCaseId": "AFCS.IBP.3"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP4","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP4"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 42.32452, "longitude": -83.054659}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}},"requestId": "REQ-IBP4"}],"version": "1.4"}, {"testCaseId": "AFCS.IBP.4"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP5","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP5"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": { "orientation": 0, "minorAxis": 30, "majorAxis": 30}},"requestId": "REQ-IBP5"}],"version": "1.4"}, {"testCaseId": "AFCS.IBP.5"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP6","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP6"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": { "orientation": 0, "minorAxis": 30, "majorAxis": 30}},"requestId": "REQ-IBP6"}],"version": "1.4"}, {"testCaseId": "AFCS.IBP.6"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP7","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP7"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": { "orientation": 0, "minorAxis": 30, "majorAxis": 30}},"requestId": "REQ-IBP7"}],"version": "1.4"}, {"testCaseId": "AFCS.IBP.7"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP8","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP8"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 3, "heightType": "AGL", "height": 3}, "ellipse": { "orientation": 0, "minorAxis": 30, "majorAxis": 30}},"requestId": "REQ-IBP8"}],"version": "1.4"}, {"testCaseId": "AFCS.IBP.8"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP1","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP1"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 10}, "ellipse": {"center": {"latitude": 18.16277, "longitude": -66.722083}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP1"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.1"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP2","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP2"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 30, "heightType": "AGL", "height": 300}, "ellipse": {"center": {"latitude": 38.377266, "longitude": -78.468021}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP2"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.2"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP3","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP3"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 12}, "ellipse": {"center": {"latitude": 33.8291, "longitude": -107.388916}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP3"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.3"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP4","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP4"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 18}, "ellipse": {"center": {"latitude": 48.361254, "longitude": -119.585033}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP4"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.4"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP5","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP5"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 8, "heightType": "AGL", "height": 78}, "ellipse": {"center": {"latitude": 30.351048, "longitude": -103.669369}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP5"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.5"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP6","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP6"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 5, "heightType": "AGL", "height": 45}, "ellipse": {"center": {"latitude": 43.256237, "longitude": -71.675651}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP6"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.6"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP7","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP7"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 6}, "ellipse": {"center": {"latitude": 31.914955, "longitude": -111.304475}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP7"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.7"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP8","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP8"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 8}, "ellipse": {"center": {"latitude": 35.507881, "longitude": -106.377759}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP8"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.8"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP9","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP9"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 15}, "ellipse": {"center": {"latitude": 19.705507, "longitude": -155.122522}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP9"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.9"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP10","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP10"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 8}, "ellipse": {"center": {"latitude": 41.486251, "longitude": -91.432634}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP10"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.10"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP11","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP11"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 11}, "ellipse": {"center": {"latitude": 37.006164, "longitude": -118.014402}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP11"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.11"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP12","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP12"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 12}, "ellipse": {"center": {"latitude": 34.282091, "longitude": -108.385163}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP12"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.12"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP13","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP13"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 6}, "ellipse": {"center": {"latitude": 17.69417, "longitude": -64.864822}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP13"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.13"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP14","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP14"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3.95}, "ellipse": {"center": {"latitude": 37.402836, "longitude": -117.959636}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP14"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.14"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP15","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP15"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 15}, "ellipse": {"center": {"latitude": 40.641732, "longitude": -121.268964}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}},"requestId": "REQ-SIP15"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.15"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP16","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP16"}]},"inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}],"location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 1, "heightType": "AGL", "height": 14.2}, "ellipse": {"center": {"latitude": 48.996, "longitude": -119.62}, "orientation": 0, "minorAxis": 10, "majorAxis": 10}},"requestId": "REQ-SIP16"}],"version": "1.4"}, {"testCaseId": "AFCS.SIP.16"} diff --git a/tests/AFC-System-SUT-Compliance-Test-Vectors.xlsx b/tests/AFC-System-SUT-Compliance-Test-Vectors.xlsx new file mode 100644 index 0000000..36119b3 Binary files /dev/null and b/tests/AFC-System-SUT-Compliance-Test-Vectors.xlsx differ diff --git a/tests/Dockerfile b/tests/Dockerfile new file mode 100644 index 0000000..e104932 --- /dev/null +++ b/tests/Dockerfile @@ -0,0 +1,35 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +FROM python:3.11-slim + +WORKDIR /usr/app +COPY tests/requirements.txt /usr/app/ + +RUN apt-get update \ + && apt-get install net-tools \ + && pip3 install --no-cache-dir -r requirements.txt \ + && rm -rf /var/lib/apt/lists/* + +COPY \ +tests/afc_tests.py \ +tests/_version.py \ +tests/_afc_errors.py \ +tests/_afc_types.py \ +tests/_wfa_types.py \ +tests/afc_input.sqlite3 \ +tools/certs.sh \ +dispatcher/certs/servers/server.bundle.pem \ +dispatcher/certs/clients/test_ca_crt.pem \ +dispatcher/certs/clients/test_ca_key.pem \ +/usr/app/ + +ENV AFC_CA_CERT_PATH ${AFC_CA_CERT_PATH:-/usr/app} +ENV PYTHONPATH=/usr/app + +ENTRYPOINT ["/usr/app/afc_tests.py"] +CMD ["-h"] diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..8b12de4 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,339 @@ +This work is licensed under the OpenAFC Project License, a copy of which is included with this software program. + +
    +
    + +## Table of Contents +- [**Introduction**](#introduction) +- [**Description**](#description) +- [**Basic functionality**](#basic-functionality) + - [Testing sequence](#testing-sequence) + - [Set AFC Server configuration](#set-afc-server-configuration) + - [Test database](#test-database) +- [**Commands and procedures**](#commands-and-procedures) + - [Tests execution options](#tests-execution-options) + - [Add new test vectors](#add-new-test-vectors) + - [Dry-run test vectors](#dry-run-test-vectors) + - [Dump test database](#dump-test-database) + - [Export AFC Server admin configuration](#export-afc-server-admin-configuration) + - [Add AFC Server admin configuration](#add-afc-server-admin-configuration) + - [Remove AFC Server admin configuration](#remove-afc-server-admin-configuration) + - [List AFC Server admin configuration](#list-afc-server-admin-configuration) + - [Export tests from WFA excel file to test DB](#export-tests-from-wfa-excel-file-to-test-db) + - [Compare AFC config records](#compare-afc-config-records) + - [Reacquisition response records for exist requests](#reacquisition-response-records-for-exist-requests) + - [How to run HTTPs access test](#how-to-run-https-access-test) +- [**Testing setup**](#testing-setup) +- [**Change testing database manually**](#change-testing-database-manually) + +# **Introduction** + +This document describes the setup for testing environment to the openAFC github project. +In addition, describes the execution procedure for the tests. +Please contact support@telecominfraproject.com in case you need access to the openAFC project. + +

    + + +# **Description** + +The system consists of a testing utility that communicates with the AFC server. +The utility sends WFA standard AFC requests and gets AFC responses. It also provides +an analysis of the results. The utility also can be used to keep and export the AFC Server +administration configuration used during validation. It consists of users, APs, +and server configurations. + +

    + + +# **Basic functionality** +## Testing sequence + +The sequence executes all required tests and returns fail if even one test fails. +The testing procedure begins by preparing a test database with test vectors and responses. +Follows run of test utility with server and test options (for details see #tests-execution-options). +``` +afc_tests.py --addr --cmd run [--testcase_indexes ] [--testcase_ids ] +``` +Current implementation provides an already prepared database file (afc_input.sqlite3). + +## Set AFC Server configuration + +There is a need to configure AFC Server prior testing. The configuration parameters include a list of APs, +their users and AFC Server configurations per predefined user. +Those parameters are stored in the test database and may be exported to an external JSON file to be used on the AFC server. +The procedure of configuring the AFC server has 2 steps. To export parameters from the testing database into JSON file +(#export-afc-server-admin-configuration) and to provide it to the local configuration utility (#add-afc-server-admin-configuration). + +## Test database + +The testing database keeps all required AFC Server configuration data (admin configuration data) +in SQL tables: user_config, ap_config, afc_config. All tables keep information as string without +splitting JSON into fields which can be redesigned by request. + +

    + + +# **Commands and procedures** +## Tests execution options + +Start sequential run of tests according to the data in the database. +``` +afc_tests.py --addr 1.2.3.4 --cmd run +``` +Run a test or number of tests according to test case row index. +``` +afc_tests.py --addr 1.2.3.4 --cmd run --testcase_indexes 3,5,6,7 +``` +Run a test or number of tests according to test case ids. +``` +afc_tests.py --addr 1.2.3.4 --cmd run --testcase_ids AFCS.IBP.5, AFCS.FSP.18 +``` +Run a test and save test result to csv format file. +``` +afc_tests.py --addr 1.2.3.4 --outfile=filename.csv --cmd run --testcase_indexes 22 +``` +Start run all tests and save results as ‘golden reference’ in the database. +``` +afc_tests.py --addr 1.2.3.4 --cmd add_reqs +``` +Run the 1st test from test DB explicitly use HTTP. +``` +afc_tests.py --addr 1.2.3.4 --prot http --port 80 --testcase_indexes 1 --cmd run +``` +Run the 100 tests from test DB. If less exist reuse from the beggining of the DB. +For example, if there are 11 testcases in DB and required to run 33, so +the test app runs exist testcases 3 times. +``` +afc_tests.py --addr 1.2.3.4 --cmd run --tests2run 1234 +``` + +## Add new test vectors + +Add new test vectors to the database provide a file to the following command line. +``` +afc_tests.py --addr=1.2.3.4 --cmd add_reqs --infile add_test_vector.txt +``` +Provided file consists of any number of test vectors in the following format of AFC request. + +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}], "deviceDescriptor": {"rulesetIds": ["US_47_CFR_PART_15_SUBPART_E"], "serialNumber": "Alpha001", "certificationId": [{"nra": "AlphaNRA01", "id": "AlphaID01"}]}, "vendorExtensions": [], "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "minDesiredPower": 18, "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 5, "heightType": "AGL", "height": 129}, "ellipse": {"orientation": 45, "minorAxis": 50, "center": {"latitude": 40.75924, "longitude": -73.97434}, "majorAxis": 100}}, "requestId": "1"}], "version": "1.1"}, {"testCase": "AFC.FSP.1”, "optonal":”optional”} + +By default, a new test vector is sent to the AFC server in order to acquire it’s response and keep it in the database. + +## Dry-run test vectors + +This is an option to run a test vector from a file without further saving it or it's response to the database file. +``` +afc_tests.py --addr 1.2.3.4 --cmd dry_run --infile add_test_vector.txt +``` + +## Dump test database + +Release testing database binary together with SQLITE dump file. +Steps to produce such dump file (dump.sql). +``` +sqlite3 afc_input.sqlite3 +> +> .output dump.sql +> .dump +> .quit +``` + +Show all entries from the database (requests and responses) +``` +afc_tests.py --cmd dump_db +``` + +## Export AFC Server admin configuration + +Export from the testing database to text file in JSON format. +``` +afc_tests.py --cmd exp_adm_cfg --outfile export_admin_cfg.json +``` + +## Add AFC Server admin configuration + +Read configurations from provided file and set AFC server +``` +rat-manage-api cfg add src=./add_admin_cfg.json +``` + +## Remove AFC Server admin configuration + +Read configurations from provided file and remove them from AFC server +``` +rat-manage-api cfg del src=./del_admin_cfg.json +``` + +## List AFC Server admin configuration + +List internal data from provided admin configuration file +``` +rat-manage-api cfg list src=./add_admin_cfg.json +``` + +## Export tests from WFA excel file to test DB + +First to export test vectors from WFA excel file into JSON format file. +``` +afc_tests.py --cmd parse_tests --infile --outfile +``` +For example, export all WFA test vectors + +``` +afc_tests.py --cmd parse_tests --infile "AFC System (SUT) Test Vectors r6.xlsx" --outfile abc.txt --test_id all +``` +Next step, to import those test vectors into test DB. +``` +afc_tests.py --cmd ins_reqs --infile abc.txt +``` + +Add extended (enhanced tests) that are not wfa. For example: +``` +afc_tests.py --cmd ext_reqs --infile brcm_ext.txt + +``` +Next step, to get new AFC responses for test vectors. It requires to make reacquisition. +The commands sends every test vector, gets relevant response and inserts into test DB. +``` +afc_tests.py --cmd reacq --addr --port +``` +Following example exports test vectors and corresponded responses to WFA format files. +All files created in local directory "wfa_test". +``` +afc_tests.py --cmd dump_db --table wfa --outfile +``` +An example how to use it with docker container, named "test". +Following sequence creates files at path "`pwd`/data/wfa_test". + +``` +docker run --rm -v `pwd`/data:/data test --cmd dump_db --table wfa --outpath /data +``` +dump all test requests and responses, including extended ones : +``` +docker run --rm -v `pwd`/data:/data test --cmd dump_db --table all --outpath /data +``` +There is a way to export device descriptors from WFA excel file into JSON format file. +``` +afc_tests.py --cmd parse_tests --dev_desc --infile --outfile +``` +Next step, to import those device descriptors into test DB. +``` +afc_tests.py --cmd ins_devs --infile abc.txt +``` + +## Compare AFC config records + +Compare provided AFC configuration with all/any records in the database. +``` +afc_tests.py --cmd cmp_cfg --infile ../../afc_config.json +``` +``` +afc_tests.py --cmd cmp_cfg --infile ../../afc_config.json --idx 1 +``` + +## Reacquisition response records for exist requests + +Re-run tests from the database and update corresponded responses. +``` +afc_tests.py --cmd reacq --addr 1.2.3.4 +``` + +## How to run HTTPs access test + +Follows an example on how to run an HTTPS access test. The test utility only does +protocol handshake using mutual TLS. +``` +cd /tests +./afc_tests.py --cmd run --addr --port --ca_cert tmp.bundle.pem +--cli_key tmp_cli.key.pem --cli_cert tmp_cli.bundle.pem +``` +How to run specific tests with existing AFC servers using a docker image. +It uses afc_tests.py utility internally in addition to script 'certs.sh' which +creates client certificates at runtime. +``` +docker run --rm --addr --port --cmd run --testcase_ids AFCS.FSP.1 +--prefix_cmd /usr/app/certs.sh cert_client --cli_cert /usr/app/test_cli/test_cli_crt.pem +--cli_key /usr/app/test_cli/test_cli_key.pem +``` +How to build a docker image for testing from sources. As a result there is a docker image 'afc_test'. +``` +cd +docker build -f tests/Dockerfile -t afc_test . +``` + +## How to send an email with test results + +Current implementation provides ability to send an mail with tests results in attached file. By default it uses Google mail server "smtp.gmail.com", port 465. +It is required to provide output file (option --outfile ) to collect tests results and to attach to the email. +``` +cd /tests +./afc_tests.py --cmd run --addr --port --ca_cert tmp.bundle.pem +--cli_key tmp_cli.key.pem --cli_cert tmp_cli.bundle.pem ----email_from --email_to --email_pwd --outfile res.csv +``` +Use of Google mail server required to make certain configurations to email account. +One alternative to permit less secure application access by the following link. +https://www.google.com/settings/security/lesssecureapps +Another alternative to enable 2-step verification on a Google account and configure it App passwords. + +

    + + +# **Testing setup** + +The testing setup consists of utility and database. +The utility provides options to send AFC request, to receive AFC response, +analyze the response, acquire response data, export AFC server admin data +to server admin configuration file. The admin server configuration file has +data required for creation of server test users and server test APs. +Each server user is provided with corresponding AFC server configuration. +The AFC server admin configuration file is in JSON format by following structure: +``` +{“afcAdminConfig”: + {“afcConfig: {...}}, + “userConfig: { “username”: ”alpha”, “password”: ”123456”, “rolename”: [“AP”, “Admin”]}, + “apConfig”: [ + { “serialNumber”: ”Alpha01”, “certificationId”: [{ “”nra”:”AlphaNRA02”, ”id”:”AlphaID01”}]}, + { “serialNumber”: ”Alpha02”, “certificationId”: [{ “”nra”:”AlphaNRA02”, ”id”:”AlphaID02”}]} + ] +} +``` +The AFC server admin configuration file provided to the AFC server utility (rat-manage-api) in order to configure the server. + +There is also the ability to delete AFC server administration configuration by providing a JSON file of the following format. +``` +{“afcAdminConfig”: + “userConfig: { “username”: ”alpha”}, + “apConfig”: [ + { “serialNumber”: ”Alpha01”}, + { “serialNumber”: ”Alpha02”} + ] +} +``` + +# **Change testing database manually** + +The procedure requires to drop the DB into a SQL file, edit if needed and create a new DB based on changed SQL file. + +Open the DB and dump to a SQL file. +``` +sqlite3 ./afc_input.sqlite3 .dump > dump.sql +``` +Open and edit dump.sql with any editor as it is a text file with not complicated SQL code. + +Make a backup from original DB. +``` +mv ./afc_input.sqlite3 ./afc_input.sqlite3_backup +``` +Open a new file and create tables in it. +``` +sqlite3 ./afc_input.sqlite3 +.read dump.sql +.quit + +``` +At this stage there is a fixed DB that can be used. + +

    + +Happy usage! diff --git a/tests/_afc_errors.py b/tests/_afc_errors.py new file mode 100644 index 0000000..e9382ba --- /dev/null +++ b/tests/_afc_errors.py @@ -0,0 +1,35 @@ +# +# Copyright © 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +class AfcError(Exception): + pass + + +class IncompleteRange(AfcError): + def __init__(self, left, right, msg='Incomplete range'): + self.msg = msg + self.left = left + self.right = right + super().__init__(self.msg, self.left, self.right) + + +class IncompleteFreqRange(IncompleteRange): + def __init__(self, left, right, msg='Incomplete freq range'): + IncompleteRange.__init__(self, left, right, msg) + + +class IncompleteGeoCoordinates(IncompleteRange): + def __init__(self, left, right, msg='Incomplete Geo coordinates'): + IncompleteRange.__init__(self, left, right, msg) + +# Local Variables: +# mode: Python +# indent-tabs-mode: nil +# python-indent: 4 +# End: +# +# vim: sw=4:et:tw=80:cc=+1 diff --git a/tests/_afc_types.py b/tests/_afc_types.py new file mode 100644 index 0000000..9c5f7d3 --- /dev/null +++ b/tests/_afc_types.py @@ -0,0 +1,18 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +AFC_TEST_STATUS = {'Ok': 0, 'Error': 1} +AFC_OK = AFC_TEST_STATUS['Ok'] +AFC_ERR = AFC_TEST_STATUS['Error'] + +# Local Variables: +# mode: Python +# indent-tabs-mode: nil +# python-indent: 4 +# End: +# +# vim: sw=4:et:tw=80:cc=+1 diff --git a/tests/_version.py b/tests/_version.py new file mode 100644 index 0000000..ba7728a --- /dev/null +++ b/tests/_version.py @@ -0,0 +1,16 @@ +# +# Copyright © 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +__version__ = "0.4.0" + +# Local Variables: +# mode: Python +# indent-tabs-mode: nil +# python-indent: 4 +# End: +# +# vim: sw=4:et:tw=80:cc=+1 diff --git a/tests/_wfa_types.py b/tests/_wfa_types.py new file mode 100644 index 0000000..ab98b50 --- /dev/null +++ b/tests/_wfa_types.py @@ -0,0 +1,363 @@ +# +# Copyright (C) 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +from _afc_types import * +from _afc_errors import * + +UNIT_NAME_CLM = 1 +PURPOSE_CLM = 2 +TEST_VEC_CLM = 3 +TEST_CAT_CLM = 4 +INDOOR_DEPL_CLM = 6 +VERSION_CLM = 9 +REQ_ID_CLM = 10 + +SER_NBR_CLM = 11 +RULESET_CLM = 12 +ID_CLM = 13 + +ELLIPSE_LONGITUDE_CLM = 14 +ELLIPSE_LATITUDE_CLM = 15 +ELLIPSE_MAJORAXIS_CLM = 16 +ELLIPSE_MINORAXIS_CLM = 17 +ELLIPSE_ORIENTATION_CLM = 18 + +L_POLYGON_LONGITUDE_CLM = 19 +L_POLYGON_LATITUDE_CLM = 20 + +R_POLYGON_CENTER_LONG_CLM = 49 +R_POLYGON_CENTER_LAT_CLM = 50 +R_POLYGON_LENGTH_CLM = 51 +R_POLYGON_ANGLE_CLM = 52 + +ELE_HEIGHT = 81 +ELE_HEIGHTTYPE = 82 +ELE_VERTICALUNCERTAINTY = 83 + +INDOORDEPLOYMENT = 84 + +INQ_FREQ_RNG_LOWFREQ_A = 85 +INQ_FREQ_RNG_HIGHFREQ_A = 86 +INQ_FREQ_RNG_LOWFREQ_B = 87 +INQ_FREQ_RNG_HIGHFREQ_B = 88 + +GLOBALOPERATINGCLASS_131 = 89 +CHANNEL_CFI_131 = 90 +GLOBALOPERATINGCLASS_132 = 91 +CHANNEL_CFI_132 = 92 +GLOBALOPERATINGCLASS_133 = 93 +CHANNEL_CFI_133 = 94 +GLOBALOPERATINGCLASS_134 = 95 +CHANNEL_CFI_134 = 96 +GLOBALOPERATINGCLASS_136 = 97 +CHANNEL_CFI_136 = 98 +GLOBALOPERATINGCLASS_137 = 99 +CHANNEL_CFI_137 = 100 + +MINDESIREDPOWER = 101 +VENDOREXTS = 102 +STANDALONE_VENDOREXTS = 103 +COMBINED_CLM = 104 + +REQ_INQUIRY_HEADER = '{"availableSpectrumInquiryRequests": [' +REQ_INQUIRY_FOOTER = '],' +REQ_FOOTER = '}' +REQ_INQ_CHA_HEADER = '"inquiredChannels": [' +REQ_INQ_CHA_FOOTER = '],' +REQ_INQ_CHA_GL_OPER_CLS = '"globalOperatingClass": ' +REQ_INQ_CHA_CHANCFI = '"channelCfi": ' + +REQ_DEV_DESC_HEADER = '"deviceDescriptor": ' +REQ_DEV_DESC_FOOTER = '}' +REQ_CERT_LOC = '"location": ' +REQ_SER_NBR = '"serialNumber": ' +REQ_CERT_ID_HEADER = '"certificationId": [{' +REQ_CERT_ID_FOOTER = '}]' +REQ_RULESET = '"rulesetId": ' +REQ_CERT_ID = '"id": ' + +REQ_REQUEST_ID = '"requestId": ' +REQ_VENDOR_EXT = '"vendorExtensions": [],' +REQ_VERSION = '"version": ' + +REQ_INQ_FREQ_RANG_HEADER = '"inquiredFrequencyRange": [' +REQ_INQ_FREQ_RANG_FOOTER = '],' +REQ_LOWFREQUENCY = '"lowFrequency": ' +REQ_HIGHFREQUENCY = '"highFrequency": ' +REQ_MIN_DESIRD_PWR = '"minDesiredPower": ' + +REQ_LOC_HEADER = '"location": {' +REQ_LOC_FOOTER = '},' +REQ_LOC_INDOORDEPL = '"indoorDeployment": ' +REQ_LOC_ELEV_HEADER = '"elevation": {' +REQ_LOC_VERT_UNCERT = '"verticalUncertainty": ' +REQ_LOC_HEIGHT_TYPE = '"heightType": ' +REQ_LOC_HEIGHT = '"height": ' +REQ_LOC_ELLIP_HEADER = '"ellipse": {' +REQ_LOC_ELLIP_FOOTER = '}' +REQ_LOC_ORIENT = '"orientation": ' +REQ_LOC_MINOR_AXIS = '"minorAxis": ' +REQ_LOC_CENTER = '"center": ' +REQ_LOC_L_POLYGON_HEADER = '"linearPolygon": {' +REQ_LOC_L_POLYGON_FOOTER = '}' +REQ_LOC_L_POLYGON_OUTER_HEADER = '"outerBoundary": [' +REQ_LOC_L_POLYGON_OUTER_FOOTER = ']' +REQ_LOC_R_POLYGON_HEADER = '"radialPolygon": {' +REQ_LOC_R_POLYGON_FOOTER = '}' +REQ_LOC_R_POLYGON_OUTER_HEADER = '"outerBoundary": [' +REQ_LOC_R_POLYGON_OUTER_FOOTER = ']' +REQ_LOC_LATITUDE = '"latitude": ' +REQ_LOC_LONGITUDE = '"longitude": ' +REQ_LOC_MAJOR_AXIS = '"majorAxis": ' +REQ_LOC_ANGLE = '"angle": ' +REQ_LOC_LENGTH = '"length": ' + +META_HEADER = '{' +META_TESTCASE_ID = '"testCaseId": ' +META_FOOTER = '}' + +LOC_TYPE_ELLIPSE = 'ellipse' +LOC_ELLIPSE_NBR_POS = 1 +LOC_TYPE_LINEAR_POL = 'linear polygon' +LOC_LINEAR_POL_NBR_POS = 15 +LOC_TYPE_RADIAL_POL = 'radial polygon' +LOC_RADIAL_POL_NBR_POS = 20 + +NEW_AFC_TEST_SUFX = '_afc_test_reqs.txt' +AFC_TEST_IDENT = {'all': 0, 'srs': 1, 'urs': 2, + 'sri': 3, 'fsp': 4, 'ibp': 5, 'sip': 6} + +WFA_TEST_DIR = 'wfa_test' + + +class AfcFreqRange: + """Afc Frequency range""" + + def __init__(self): + self.low = 0 + self.high = 0 + + def set_range_limit(self, cell, type): + if isinstance(cell.value, int): + setattr(self, type, cell.value) + + def append_range(self): + if (not self.low or not self.high): + raise IncompleteFreqRange(self.low, self.high) + if (self.low + self.high): + return '{' + REQ_LOWFREQUENCY + str(self.low) + ', ' +\ + REQ_HIGHFREQUENCY + str(self.high) + '}' + + +class AfcCell: + """Keep current cell from excel document.""" + + def __init__(self, sheet, row): + self.sh = sheet + self.row = row + + +class AfcCoordinates: + """Create Afc position - pair of latitude and longitude.""" + + def __init__(self): + self.coordinates = {'latitude': '', 'longitude': ''} + + def _set_coordinates(self, excell_obj, lat_col, long_col): + cell = excell_obj.sh.cell(row=excell_obj.row, column=lat_col) + if isinstance(cell.value, float): + self.coordinates['latitude'] = '{' + REQ_LOC_LATITUDE +\ + str(cell.value) + ', ' + cell = excell_obj.sh.cell(row=excell_obj.row, column=long_col) + if isinstance(cell.value, float): + self.coordinates['longitude'] = REQ_LOC_LONGITUDE +\ + str(cell.value) + '}' + return self.coordinates['latitude'] + self.coordinates['longitude'] + + +class AfcCoordRadPol: + """Create Afc position - pair of angle and length.""" + + def __init__(self): + self.coordinates = {'angle': '', 'length': ''} + + def _set_coordinates(self, excell_obj, ang_col, len_col): + cell = excell_obj.sh.cell(row=excell_obj.row, column=ang_col) + if isinstance(cell.value, int): + self.coordinates['angle'] = REQ_LOC_ANGLE +\ + str(cell.value) + '}' + cell = excell_obj.sh.cell(row=excell_obj.row, column=len_col) + if isinstance(cell.value, int): + self.coordinates['length'] = '{' + REQ_LOC_LENGTH +\ + str(cell.value) + ', ' + return self.coordinates['length'] + self.coordinates['angle'] + + +class AfcGeoCoordinates: + """Afc Geo coordinates""" + + def __init__(self, sheet, row): + self.positions = [] + self.region_type = 'unknown' + self.exc = AfcCell(sheet, row) + self.find_region_type() + + def _valid_cell(self, clm): + cell = self.exc.sh.cell(row=self.exc.row, column=clm) + if (str(cell.value) == 'NULL'): + return AFC_ERR + return AFC_OK + + def find_region_type(self): + if self._valid_cell(ELLIPSE_LONGITUDE_CLM) == AFC_OK: + self.region_type = LOC_TYPE_ELLIPSE + self.nbr_loc_pos = LOC_ELLIPSE_NBR_POS + self.start_col = ELLIPSE_LONGITUDE_CLM + elif self._valid_cell(L_POLYGON_LONGITUDE_CLM) == AFC_OK: + self.region_type = LOC_TYPE_LINEAR_POL + self.nbr_loc_pos = LOC_LINEAR_POL_NBR_POS + self.start_col = L_POLYGON_LONGITUDE_CLM + elif self._valid_cell(R_POLYGON_CENTER_LONG_CLM) == AFC_OK: + self.region_type = LOC_TYPE_RADIAL_POL + self.nbr_loc_pos = LOC_RADIAL_POL_NBR_POS + self.start_col = R_POLYGON_LENGTH_CLM + + def _add_positions(self): + """ + Build list of strings in following format + {"latitude": 30.5725933, "longitude": -102.229316}, + """ + for i in range(0, self.nbr_loc_pos, 2): + if self.region_type == LOC_TYPE_RADIAL_POL: + coor = AfcCoordinates()._set_coordinates( + self.exc, self.start_col - 1 + i, self.start_col - 2 + i) + if len(coor): + self.positions.append(coor) + coor = AfcCoordRadPol()._set_coordinates( + self.exc, self.start_col + 1 + i, self.start_col + i) + else: + coor = AfcCoordinates()._set_coordinates( + self.exc, self.start_col + 1 + i, self.start_col + i) + if len(coor): + self.positions.append(coor) + + def _set_orientation(self): + cell = self.exc.sh.cell(row=self.exc.row, + column=ELLIPSE_ORIENTATION_CLM) + if isinstance(cell.value, int): + setattr(self, 'orientation', REQ_LOC_ORIENT + str(cell.value)) + + def _set_minoraxis(self): + cell = self.exc.sh.cell(row=self.exc.row, + column=ELLIPSE_MINORAXIS_CLM) + if isinstance(cell.value, int): + setattr(self, 'minoraxis', REQ_LOC_MINOR_AXIS + str(cell.value)) + + def _set_majoraxis(self): + cell = self.exc.sh.cell(row=self.exc.row, + column=ELLIPSE_MAJORAXIS_CLM) + if isinstance(cell.value, int): + setattr(self, 'majoraxis', REQ_LOC_MAJOR_AXIS + str(cell.value)) + + def _collect_ellipse(self): + self._add_positions() + self._set_orientation() + self._set_minoraxis() + self._set_majoraxis() + + def _collect_linear_polygon(self): + self._add_positions() + + def _collect_radial_polygon(self): + self._add_positions() + + def _append_ellipse_coordinates(self): + res = '' + if self.positions: + res += REQ_LOC_CENTER + ''.join(map(str, self.positions)) + ',' + if hasattr(self, 'orientation'): + res += ' ' + self.orientation + ',' + if hasattr(self, 'minoraxis'): + res += ' ' + self.minoraxis + ',' + if hasattr(self, 'majoraxis'): + res += ' ' + self.majoraxis + if res and res[-1] == ',': + res = res[:-1] + return res + REQ_LOC_ELLIP_FOOTER + + def _append_l_polygon_coordinates(self): + res = '' + if (len(self.positions)): + res += REQ_LOC_L_POLYGON_OUTER_HEADER +\ + ', '.join(map(str, self.positions)) + return res + REQ_LOC_L_POLYGON_OUTER_FOOTER + + def _append_r_polygon_coordinates(self): + res = '' + if (len(self.positions)): + center_lat_long = self.positions.pop(0) + if (len(self.positions)): + res += REQ_LOC_R_POLYGON_OUTER_HEADER +\ + ', '.join(map(str, self.positions)) +\ + REQ_LOC_R_POLYGON_OUTER_FOOTER + if (len(center_lat_long)): + res += ', ' + REQ_LOC_CENTER +\ + center_lat_long + return res + + def collect_coordinates(self): + res = '' + if self.region_type == LOC_TYPE_ELLIPSE: + self._collect_ellipse() + res = REQ_LOC_ELLIP_HEADER + self._append_ellipse_coordinates() + elif self.region_type == LOC_TYPE_LINEAR_POL: + self._collect_linear_polygon() + res = REQ_LOC_L_POLYGON_HEADER +\ + self._append_l_polygon_coordinates() +\ + REQ_LOC_L_POLYGON_FOOTER + elif self.region_type == LOC_TYPE_RADIAL_POL: + self._collect_radial_polygon() + res = REQ_LOC_R_POLYGON_HEADER +\ + self._append_r_polygon_coordinates() +\ + REQ_LOC_R_POLYGON_FOOTER + return res + + +def build_device_desc(indoor_deploy, ser_nbr, + ruleset_id, cert_id, ins_location): + _ser_nbr = "" + _cert_id = "" + if isinstance(ser_nbr, str): + _ser_nbr = str(ser_nbr) + if isinstance(cert_id, str): + _cert_id = str(cert_id) + + ostr = "{" + if ins_location: + if not indoor_deploy: + location = "2" + elif "indoor" in indoor_deploy.lower(): + location = "3" + else: + location = "2" + + ostr += REQ_CERT_LOC + '"' + location + '",' + + ostr += REQ_SER_NBR + '"' + _ser_nbr + '",' +\ + REQ_CERT_ID_HEADER + REQ_RULESET + '"' + str(ruleset_id) + '",' +\ + REQ_CERT_ID + '"' + _cert_id + '"' + REQ_CERT_ID_FOOTER +\ + REQ_DEV_DESC_FOOTER + + return ostr + +# Local Variables: +# mode: Python +# indent-tabs-mode: nil +# python-indent: 4 +# End: +# +# vim: sw=4:et:tw=80:cc=+1 diff --git a/tests/afc_config.json b/tests/afc_config.json new file mode 100644 index 0000000..887b3c1 --- /dev/null +++ b/tests/afc_config.json @@ -0,0 +1 @@ +{"afcConfig":{"antennaPattern":{"kind":"F.1245"},"polarizationMismatchLoss":{"kind":"Fixed Value","value":3},"bodyLoss":{"kind":"Fixed Value","valueIndoor":0,"valueOutdoor":0},"buildingPenetrationLoss":{"kind":"Fixed Value","value":20.5},"receiverFeederLoss":{"UNII5":3,"UNII7":2.5,"other":2},"fsReceiverNoise":{"UNII5":-110,"UNII7":-109.5,"other":-109},"threshold":-6,"maxLinkDistance":50,"maxEIRP":36,"minEIRP":18,"propagationModel":{"kind":"FCC 6GHz Report & Order","win2Confidence":50,"itmConfidence":50,"p2108Confidence":50,"buildingSource":"None","terrainSource":"3DEP (30m)"},"propagationEnv":"NLCD Point","ulsDatabase":"CONUS_filtered_ULS_21Jan2020_6GHz_1.1.0_fixbps_sort_1record_unii5_7.sqlite3","regionStr":"CONUS","rasDatabase":"RASdatabase.csv","APUncertainty":{"horizontal":30,"height":5},"ITMParameters":{"polarization":"Vertical","ground":"Good Ground","dielectricConst":25,"conductivity":0.02,"minSpacing":3,"maxPoints":2000},"clutterAtFS":false,"version":"3.3.10-22787"}} diff --git a/tests/afc_input.sqlite3 b/tests/afc_input.sqlite3 new file mode 100644 index 0000000..1fbb103 Binary files /dev/null and b/tests/afc_input.sqlite3 differ diff --git a/tests/afc_input.sqlite3_backup b/tests/afc_input.sqlite3_backup new file mode 100644 index 0000000..54b2b5e Binary files /dev/null and b/tests/afc_input.sqlite3_backup differ diff --git a/tests/afc_tests.py b/tests/afc_tests.py new file mode 100755 index 0000000..b48f802 --- /dev/null +++ b/tests/afc_tests.py @@ -0,0 +1,2190 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +""" +Description + +The execution always done in specific order: configuration, database, testing. +In case there are not available valid responses to compare with +need to make acquisition and create new database as following. + ./afc_tests.py --addr
    --log debug --cmd run +""" + +import argparse +import certifi +import csv +import datetime +import hashlib +import inspect +import io +import json +import logging +import openpyxl as oxl +import os +import re +import requests +import shutil +import sqlite3 +import subprocess +import sys +import time + +import smtplib +import ssl +from email import encoders +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +from bs4 import BeautifulSoup +from deepdiff import DeepDiff +from multiprocessing.pool import Pool +from string import Template +from _afc_types import * +from _afc_errors import * +from _version import __version__ +from _wfa_types import * + +AFC_URL_SUFFIX = '/fbrat/ap-afc/' +AFC_REQ_NAME = 'availableSpectrumInquiry' +AFC_WEBUI_URL_SUFFIX = '/fbrat/ratapi/v1/' +AFC_WEBUI_REQ_NAME = 'availableSpectrumInquirySec' +AFC_WEBUI_TOKEN = 'about_csrf' +headers = {'content-type': 'application/json'} + +AFC_TEST_DB_FILENAME = 'afc_input.sqlite3' +TBL_REQS_NAME = 'test_vectors' +TBL_RESPS_NAME = 'test_data' +TBL_USERS_NAME = 'user_config' +TBL_AFC_CFG_NAME = 'afc_config' +TBL_AP_CFG_NAME = 'ap_config' + +AFC_PROT_NAME = 'https' + +# metadata variables +TESTCASE_ID = "testCaseId" +# mandatory keys that need to be read from input text file +MANDATORY_METADATA_KEYS = {TESTCASE_ID} + +app_log = logging.getLogger(__name__) + + +class TestCfg(dict): + """Keep test configuration""" + + def __init__(self): + dict.__init__(self) + self.update({ + 'cmd': '', + 'port': 443, + 'url_path': AFC_PROT_NAME + '://', + 'log_level': logging.INFO, + 'db_filename': AFC_TEST_DB_FILENAME, + 'tests': None, + 'is_test_by_index': True, + 'resp': '', + 'stress': 0, + 'precision': None}) + + def _send_recv(self, params): + """Run AFC test and wait for respons""" + data = params.split("'") + get_req = '' + for item in data: + try: + json.loads(item) + except ValueError as e: + continue + get_req = item + break + + new_req_json = json.loads(get_req.encode('utf-8')) + new_req = json.dumps(new_req_json, sort_keys=True) + if (self['webui'] is False): + params_data = { + 'conn_type': self['conn_type'], + 'debug': self['debug'], + 'edebug': cfg['elaborated_debug'], + 'gui': self['gui'] + } + else: + # emulating request calll from webui + params_data = { + 'debug': 'True', + 'edebug': cfg['elaborated_debug'], + 'gui': 'True' + } + if (self['cache'] == False): + params_data['nocache'] = 'True' + + ser_cert = not self['verif'] + cli_certs = () + if (self['prot'] == AFC_PROT_NAME and + self['verif'] == False): + # add mtls certificates if explicitly provided + if not isinstance(self['cli_cert'], type(None)): + cli_certs = ("".join(self['cli_cert']), + "".join(self['cli_key'])) + # add tls certificates if explicitly provided + if not isinstance(self['ca_cert'], type(None)): + ser_cert = "".join(self['ca_cert']) + app_log.debug(f"Client {cli_certs}, Server {ser_cert}") + + before_ts = time.monotonic() + rawresp = requests.post( + self['url_path'], + params=params_data, + data=new_req, + headers=headers, + timeout=600, # 10 min + verify=self['verif']) + resp = rawresp.json() + + tId = resp.get('taskId') + if ((self['conn_type'] == 'async') and + (not isinstance(tId, type(None)))): + tState = resp.get('taskState') + params_data['task_id'] = tId + while (tState == 'PENDING') or (tState == 'PROGRESS'): + app_log.debug('_run_test() state %s, tid %s, status %d', + tState, tId, rawresp.status_code) + time.sleep(2) + rawresp = requests.get(self['url_path'], + params=params_data) + if rawresp.status_code == 200: + resp = rawresp.json() + break + + tm_secs = time.monotonic() - before_ts + app_log.info('Test done at %.1f secs', tm_secs) + return new_req, resp + + +class TestResultComparator: + """ AFC Response comparator + + Private instance attributes: + _precision -- Precision for results' comparison in dB. 0 means exact match + """ + + def __init__(self, precision): + """ Constructor + + Arguments: + precision -- Precision for results' comparison in dB. 0 means exact + match + """ + assert precision >= 0 + self._precision = precision + + def compare_results(self, ref_str, result_str): + """ Compares reference and actual AFC responses + + Arguments: + ref_str -- Reference response JSON in string representation + result_str -- Actual response json in string representation + Returns list of difference description strings. Empty list means match + """ + # List of difference description strings + diffs = [] + # Reference and actual JSON dictionaries + jsons = [] + for s, kind in [(ref_str, "reference"), (result_str, "result")]: + try: + jsons.append(json.loads(s)) + except json.JSONDecodeError as ex: + diffs.append(f"Failed to decode {kind} JSON data: {ex}") + return diffs + self._recursive_compare(jsons[0], jsons[1], [], diffs) + return diffs + + def _recursive_compare(self, ref_json, result_json, path, diffs): + """ Recursive comparator of JSON nodes + + Arguments: + ref_json -- Reference response JSON dictionary + result_json -- Actual response JSON dictionary + path -- Path (sequence of indices) to node in question + diffs -- List of difference description strings to update + """ + # Items in questions in JSON dictionaries + ref_item = self._get_item(ref_json, path) + result_item = self._get_item(result_json, path) + # Human readable path representation for difference messages + path_repr = f"[{']['.join(str(idx) for idx in path)}]" + if ref_item == result_item: + return # Items are equal - nothing to do + # So, items are different. What's the difference? + if isinstance(ref_item, dict): + # One item is dictionary. Other should also should be dictionary... + if not isinstance(result_item, dict): + diffs.append(f"Different item types at {path_repr}") + return + # ... with same set of keys + ref_keys = set(ref_item.keys()) + result_keys = set(result_item.keys()) + for unique_key in (ref_keys ^ result_keys): + if self._compare_channel_lists(ref_json, result_json, + path + [unique_key], diffs): + ref_keys -= {unique_key} + result_keys -= {unique_key} + if ref_keys != result_keys: + msg = f"Different set of keys at {path_repr}" + for kind, elems in [("reference", ref_keys - result_keys), + ("result", result_keys - ref_keys)]: + if elems: + msg += \ + f" Unique {kind} keys: {', '.join(sorted(elems))}." + diffs.append(msg) + return + # Comparing values for individual keys + for key in sorted(ref_keys): + self._recursive_compare(ref_json, result_json, path + [key], + diffs) + elif isinstance(ref_item, list): + # One item is list. Other should also be list... + if not isinstance(result_item, list): + diffs.append(f"Different item types at {path_repr}") + return + # If this is a channel list (or part thereof - handle it) + if self._compare_channel_lists(ref_json, result_json, path, diffs): + return + # Proceeding with comparison of other list kinds + if len(ref_item) != len(result_item): + diffs.append( + (f"Different list lengths at at {path_repr}: " + f"{len(ref_item)} elements in reference vs " + f"{len(result_item)} elements in result")) + return + # Comparing individual elements + for i in range(len(ref_item)): + self._recursive_compare(ref_json, result_json, path + [i], + diffs) + else: + # Items should be scalars + for item, kind in [(ref_item, "Reference"), + (result_item, "Result")]: + if not isinstance(item, (int, float, str)): + diffs.append((f"{kind} data contains unrecognized item " + f"type at {path_repr}")) + return + diffs.append((f"Difference at {path}: reference content is " + f"{ref_item}, result content is {result_item}")) + + def _compare_channel_lists(self, ref_json, result_json, path, diffs): + """ Trying to compare channel lists + + Arguments: + ref_json -- Reference response JSON dictionary + result_json -- Actual response JSON dictionary + path -- Path (sequence of indices) to node in question + diffs -- List of difference description strings to update + Returns true if channel list comparison was done, no further action + required, False if node is not a channel list, should be compared as + usual + """ + if path[-1] == "channelCfi": + # Comparison will be made at "maxEirp" + return True + # Human readable path representation for difference messages + path_repr = f"[{']['.join(str(idx) for idx in path)}]" + # EIRP dictionaries, indexed by channel identification (number or + # frequency range) + ref_channels = {} + result_channels = {} + if path[-1] == "maxEirp": + # Channel numbers + for kind, src, chan_dict in \ + [("reference", ref_json, ref_channels), + ("result", result_json, result_channels)]: + try: + numbers = self._get_item(src, path[:-1] + ["channelCfi"], + default_last=[]) + chan_dict.update( + dict(zip([str(n) for n in numbers], + self._get_item(src, path, default_last=[])))) + except (TypeError, ValueError, KeyError): + diffs.append((f"Unrecognized channel list structure at " + f"{path_repr} in {kind}")) + return True + elif path[-1] == "availableFrequencyInfo": + # Channel frequencies + for kind, src, chan_dict in \ + [("reference", ref_json, ref_channels), + ("result", result_json, result_channels)]: + try: + for freq_info in self._get_item(src, path, + default_last=[]): + fr = freq_info["frequencyRange"] + low = fr['lowFrequency'] + high = fr['highFrequency'] + for freq in range(low, high): + chan_dict[f"[{freq} - {freq+1}"] = \ + float(freq_info.get("maxPSD") or + freq_info.get("maxPsd")) + except (TypeError, ValueError, KeyError): + diffs.append((f"Unrecognized frequency list structure at " + f"{path_repr} in {kind}")) + return True + else: + return False + # Now will compare two channel dictionaries + # First looking for unique channels + for this_kind, this_channels, other_kind, other_channels in \ + [("reference", ref_channels, "result", result_channels), + ("result", result_channels, "reference", ref_channels)]: + for channel in sorted(set(this_channels.keys()) - + set(other_channels.keys())): + diffs.append( + (f"Channel {channel} present in {path_repr} of " + f"{this_kind} with EIRP limit of " + f"{this_channels[channel]}dBm, but absent in " + f"{other_kind}")) + # Then looking for different EIRPs on common channels + for channel in sorted(set(ref_channels.keys()) & + set(result_channels.keys())): + diff = abs(ref_channels[channel] - result_channels[channel]) + if diff <= self._precision: + continue + diffs.append( + (f"Different values in {path_repr} for channel {channel}: " + f"reference has EIRP of {ref_channels[channel]}dBm, " + f"result has EIRP of {result_channels[channel]}dBm, " + f"difference is: {diff:g}dB")) + return True + + def _get_item(self, j, path, default_last=None): + """ Retrieves item by sequence of indices + + Arguments: + j -- JSON dictionary + path -- Sequence of indices + default_last -- What to return if item at last index is absent. None + means throw exception (if nonlast item is absent - + exception is also thrown) + Returns retrieved item + """ + for path_idx, elem_idx in enumerate(path): + try: + j = j[elem_idx] + except (KeyError, IndexError): + if (default_last is not None) and \ + (path_idx == (len(path) - 1)): + return default_last + raise + return j + + +def json_lookup(key, json_obj, val): + """Lookup for key in json and change it value if required""" + keepit = [] + + def lookup(key, json_obj, val, keepit): + if isinstance(json_obj, dict): + for k, v in json_obj.items(): + if isinstance(v, (dict, list)): + lookup(key, v, val, keepit) + elif k == key: + keepit.append(v) + if val: + json_obj[k] = val + elif isinstance(json_obj, list): + for node in json_obj: + lookup(key, node, val, keepit) + return keepit + + found = lookup(key, json_obj, val, keepit) + return found + + +def get_md5(fname): + hash_md5 = hashlib.md5() + with open(fname, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + + +def create_email_attachment(filename): + part = None + with open(filename, "rb") as attachment: + # Add file as application/octet-stream + # Email client can usually download this automatically as attachment + part = MIMEBase("application", "octet-stream") + part.set_payload(attachment.read()) + # Add header as key/value pair to attachment part + encoders.encode_base64(part) + part.add_header("Content-Disposition", + f"attachment; filename= {filename}",) + return part + + +def send_email(cfg): + """Send an email to predefined adress using gmail smtp server""" + sender = cfg['email_from'] + recipient = cfg['email_to'] + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()" + f" from: {sender}, to: {recipient}") + context = ssl.create_default_context() + with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server: + server.login(sender, cfg['email_pwd']) + + body = f"Please find attached responses." + + message = MIMEMultipart("alternative") + message['Subject'] = f"AFC test results" + message['From'] = sender + message['To'] = recipient + if not isinstance(cfg['email_cc'], type(None)): + message['Cc'] = cfg['email_cc'] + + # Turn these into plain/html MIMEText objects + message.attach(MIMEText(body, "plain")) + message.attach(create_email_attachment(cfg['outfile'][0])) + + server.sendmail(sender, recipient, message.as_string()) + + +def _send_recv(cfg, req_data, ssn=None): + """Send AFC request and receiver it's response""" + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + + new_req_json = json.loads(req_data.encode('utf-8')) + new_req = json.dumps(new_req_json, sort_keys=True) + if (cfg['webui'] is False): + params_data = { + 'conn_type': cfg['conn_type'], + 'debug': cfg['debug'], + 'edebug': cfg['elaborated_debug'], + 'gui': cfg['gui'] + } + if (cfg['cache'] == False): + params_data['nocache'] = 'True' + post_func = requests.post + else: + # emulating request call from webui + params_data = { + 'debug': 'True', + 'gui': 'True' + } + headers['Accept-Encoding'] = 'gzip, defalte' + headers['Referer'] = cfg['base_url'] + 'fbrat/www/index.html' + headers['X-Csrf-Token'] = cfg['token'] + app_log.debug( + f"({os.getpid()}) {inspect.stack()[0][3]}()\n" + f"Cookies: {requests.utils.dict_from_cookiejar(ssn.cookies)}") + post_func = ssn.post + + ser_cert = () + cli_certs = None + if ((cfg['prot'] == AFC_PROT_NAME and + cfg['verif']) or (cfg['ca_cert'])): + + # add mtls certificates if explicitly provided + if not isinstance(cfg['cli_cert'], type(None)): + cli_certs = ("".join(cfg['cli_cert']), "".join(cfg['cli_key'])) + + # add tls certificates if explicitly provided + if not isinstance(cfg['ca_cert'], type(None)): + ser_cert = "".join(cfg['ca_cert']) + cfg['verif'] = True + else: + os.environ['REQUESTS_CA_BUNDLE'] = certifi.where() + app_log.debug(f"REQUESTS_CA_BUNDLE " + f"{os.environ.get('REQUESTS_CA_BUNDLE')}") + if "REQUESTS_CA_BUNDLE" in os.environ: + ser_cert = "".join(os.environ.get('REQUESTS_CA_BUNDLE')) + cfg['verif'] = True + else: + app_log.error(f"Missing CA certificate while forced.") + return + + app_log.debug(f"Client {cli_certs}, Server {ser_cert}") + + try: + rawresp = post_func( + cfg['url_path'], + params=params_data, + data=new_req, + headers=headers, + timeout=600, # 10 min + cert=cli_certs, + verify=ser_cert if cfg['verif'] else False) + rawresp.raise_for_status() + except (requests.exceptions.HTTPError, + requests.exceptions.ConnectionError) as err: + app_log.error(f"{err}") + return + + resp = rawresp.json() + + tId = resp.get('taskId') + if ((cfg['conn_type'] == 'async') and + (not isinstance(tId, type(None)))): + tState = resp.get('taskState') + params_data['task_id'] = tId + while (tState == 'PENDING') or (tState == 'PROGRESS'): + app_log.debug('_run_test() state %s, tid %s, status %d', + tState, tId, rawresp.status_code) + time.sleep(2) + rawresp = requests.get(cfg['url_path'], + params=params_data) + if rawresp.status_code == 200: + resp = rawresp.json() + break + + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()" + f" Resp status: {rawresp.status_code}") + return resp + + +def _send_recv_token(cfg, ssn): + """Making login, open session and getting CSRF token""" + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + + token = '' + ser_cert = () + cli_certs = None + app_log.debug(f"=== ser {type(ser_cert)}") + if ((cfg['prot'] == AFC_PROT_NAME and + cfg['verif']) or (cfg['ca_cert'])): + + # add mtls certificates if explicitly provided + if not isinstance(cfg['cli_cert'], type(None)): + cli_certs = ("".join(cfg['cli_cert']), "".join(cfg['cli_key'])) + + # add tls certificates if explicitly provided + if not isinstance(cfg['ca_cert'], type(None)): + ser_cert = "".join(cfg['ca_cert']) + cfg['verif'] = True + else: + os.environ['REQUESTS_CA_BUNDLE'] = certifi.where() + app_log.debug(f"REQUESTS_CA_BUNDLE " + f"{os.environ.get('REQUESTS_CA_BUNDLE')}") + if "REQUESTS_CA_BUNDLE" in os.environ: + ser_cert = "".join(os.environ.get('REQUESTS_CA_BUNDLE')) + cfg['verif'] = True + else: + app_log.error(f"Missing CA certificate while forced.") + return token + + app_log.debug(f"Client {cli_certs}, Server {ser_cert}") + + # get login + ssn.headers.update({ + 'Accept-Encoding': 'gzip, defalte' + }) + url_login = cfg['base_url'] + 'fbrat/user/sign-in' + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()\n" + f"===> URL {url_login}\n" + f"===> Status {ssn.headers}\n" + f"===> Cookies: {ssn.cookies}\n") + + try: + rawresp = ssn.get(url_login, + stream=False, + cert=cli_certs, + verify=ser_cert if cfg['verif'] else False) + except (requests.exceptions.HTTPError, + requests.exceptions.ConnectionError) as err: + app_log.error(f"{err}") + return token + soup = BeautifulSoup(rawresp.text, 'html.parser') + inp_tkn = soup.find('input', id='csrf_token') + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()\n" + f"<--- Status {rawresp.status_code}\n" + f"<--- Headers {rawresp.headers}\n" + f"<--- Cookies: {ssn.cookies}\n" + f"<--- Input: {inp_tkn}\n") + token = inp_tkn.get('value') + + # fetch username and password from test db + con = sqlite3.connect(cfg['db_filename']) + cur = con.cursor() + cur.execute('SELECT * FROM %s\n' % TBL_USERS_NAME) + found_user = cur.fetchall() + con.close() + found_json = json.loads(found_user[0][1]) + app_log.debug(f"Found Users: {found_json['username']}") + + form_data = { + 'next': '/', + 'reg_next': '/', + 'csrf_token': token, + 'username': found_json['username'], + 'password': found_json['password'] + } + ssn.headers.update({ + 'Content-Type': 'application/x-www-form-urlencoded', + 'Referer': url_login + }) + try: + rawresp = ssn.post(url_login, + data=form_data, + stream=False, + cert=cli_certs, + verify=ser_cert if cfg['verif'] else False) + except (requests.exceptions.HTTPError, + requests.exceptions.ConnectionError) as err: + app_log.error(f"{err}") + return token + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()\n" + f"<--- Status {rawresp.status_code}\n" + f"<--- Headers {rawresp.headers}\n" + f"<--- Cookies: {ssn.cookies}\n") + + return token + + +def make_db(filename): + """Create DB file only with schema""" + app_log.debug('%s()', inspect.stack()[0][3]) + if os.path.isfile(filename): + app_log.debug('%s() The db file is exists, no need to create new one.', + inspect.stack()[0][3]) + return True + + app_log.info('Create DB tables (%s, %s) from source files', + TBL_REQS_NAME, TBL_RESPS_NAME) + con = sqlite3.connect(filename) + cur = con.cursor() + cur.execute('CREATE TABLE IF NOT EXISTS ' + TBL_REQS_NAME + + ' (test_id varchar(50), data json)') + cur.execute('CREATE TABLE IF NOT EXISTS ' + TBL_RESPS_NAME + + ' (test_id varchar(50), data json, hash varchar(255))') + con.close() + return True + + +def compare_afc_config(cfg): + """ + Compare AFC configuration from the DB with provided one. + """ + app_log.debug('%s()', inspect.stack()[0][3]) + + if not os.path.isfile(cfg['db_filename']): + app_log.error('Missing DB file %s', cfg['db_filename']) + return AFC_ERR + + con = sqlite3.connect(cfg['db_filename']) + cur = con.cursor() + cur.execute('SELECT * FROM %s' % TBL_AFC_CFG_NAME) + found_cfgs = cur.fetchall() + con.close() + + # get record from the input file + if isinstance(cfg['infile'], type(None)): + app_log.debug('Missing input file to compare with.') + return AFC_OK + + filename = cfg['infile'][0] + with open(filename, 'r') as fp_test: + while True: + rec = fp_test.read() + if not rec: + break + try: + get_rec = json.loads(rec) + except (ValueError, TypeError) as e: + continue + break + app_log.debug(json.dumps(get_rec, sort_keys=True, indent=4)) + + get_cfg = '' + app_log.debug('Found %d config records', len(found_cfgs)) + idx = 0 + max_idx = len(found_cfgs) + if not isinstance(cfg['idx'], type(None)): + idx = cfg['idx'] + if idx >= max_idx: + app_log.error("The index (%d) is out of range (0 - %d).", + idx, max_idx - 1) + return AFC_ERR + max_idx = idx + 1 + while idx < max_idx: + for item in list(found_cfgs[idx]): + try: + get_cfg = json.loads(item) + except (ValueError, TypeError) as e: + continue + break + app_log.debug("Record %d:\n%s", + idx, + json.dumps(get_cfg['afcConfig'], + sort_keys=True, + indent=4)) + + get_diff = DeepDiff(get_cfg['afcConfig'], + get_rec, + report_repetition=True) + app_log.info("rec %d:\n%s", idx, get_diff) + idx += 1 + + return AFC_OK + + +def start_acquisition(cfg): + """ + Fetch test vectors from the DB, drop previous response table, + run tests and fill responses in the DB with hash values + """ + app_log.debug(f"{inspect.stack()[0][3]}()") + + found_reqs, found_resp, ids, test_ids = _convert_reqs_n_resps_to_dict(cfg) + + # check if to make acquisition of all tests + con = sqlite3.connect(cfg['db_filename']) + cur = con.cursor() + # drop response table and create new one if all testcases required + # to reacquisition + all_resps = False + if (len(test_ids) == len(found_reqs)): + all_resps = True + if all_resps: + try: + app_log.debug(f"{inspect.stack()[0][3]}() " + f"drop table {TBL_RESPS_NAME}") + cur.execute('DROP TABLE ' + TBL_RESPS_NAME) + except Exception as OperationalError: + # awkward but bearable + app_log.debug('Missing table %s', TBL_RESPS_NAME) + cur.execute('CREATE TABLE ' + TBL_RESPS_NAME + + ' (test_id varchar(50), data json, hash varchar(255))') + + app_log.info(f"Number of tests to make acquisition - {len(test_ids)}") + for test_id in test_ids: + req_id = ids[test_id][0] + app_log.debug(f"Request: {req_id}") + resp = _send_recv(cfg, json.dumps(found_reqs[test_id][0])) + if isinstance(resp, type(None)): + app_log.error(f"Test {test_ids} ({req_id}) is Failed.") + continue + + json_lookup('availabilityExpireTime', resp, '0') + upd_data = json.dumps(resp, sort_keys=True) + hash_obj = hashlib.sha256(upd_data.encode('utf-8')) + app_log.debug(f"{inspect.stack()[0][3]}() new: " + f"{hash_obj.hexdigest()}") + if all_resps: + cur.execute('INSERT INTO ' + TBL_RESPS_NAME + ' values ( ?, ?, ?)', + [req_id, + upd_data, + hash_obj.hexdigest()]) + con.commit() + elif (test_id in found_resp.keys() and + found_resp[test_id][1] == hash_obj.hexdigest()): + app_log.debug(f"Skip to update hash for {req_id}. " + f"Found the same value.") + continue + else: + hash = hash_obj.hexdigest() + cur.execute('UPDATE ' + TBL_RESPS_NAME + ' SET ' + + 'data = ?, hash = ? WHERE test_id =?', + (upd_data, hash, ids[test_id][0])) + con.commit() + con.close() + return AFC_OK + + +def process_jsonline(line): + """ + Function to process the input line from .txt file containing comma + separated json strings + """ + + # convert the input line to list of dictioanry/dictionaries + line_list = json.loads("[" + line + "]") + request_dict = line_list[0] + metadata_dict = line_list[1] if len(line_list) > 1 else {} + + return request_dict, metadata_dict + + +def get_db_req_resp(cfg): + """ + Function to retrieve request and response records from the database + """ + con = sqlite3.connect(cfg['db_filename']) + cur = con.cursor() + cur.execute('SELECT * FROM %s' % TBL_REQS_NAME) + found_reqs = cur.fetchall() + db_reqs_list = [row[0] for row in found_reqs] + + cur.execute('SELECT * FROM %s' % TBL_RESPS_NAME) + found_resps = cur.fetchall() + db_resp_list = [row[0] for row in found_resps] + con.close() + + return db_reqs_list, db_resp_list + + +def insert_reqs_int(filename, con, cur): + """ + Insert requests from input file to a table in test db. + + """ + with open(filename, 'r') as fp_test: + while True: + dataline = fp_test.readline() + if not dataline: + break + + # process dataline arguments + app_log.debug(f"= {dataline}") + request_json, metadata_json = process_jsonline(dataline) + + # reject the request if mandatory metadata arguments are not + # present + if not MANDATORY_METADATA_KEYS.issubset( + set(metadata_json.keys())): + # missing mandatory keys in test case input + app_log.error("Test case input does not contain required" + " mandatory arguments: %s", + ", ".join(list( + MANDATORY_METADATA_KEYS - set(metadata_json.keys())))) + return AFC_ERR + + app_log.info(f"Insert new request in DB " + f"({metadata_json[TESTCASE_ID]})") + app_log.debug(f"+ {metadata_json[TESTCASE_ID]}") + cur.execute('INSERT INTO ' + TBL_REQS_NAME + ' VALUES ( ?, ?)', + (metadata_json[TESTCASE_ID], + json.dumps(request_json),)) + con.commit() + con.close() + return AFC_OK + + +def insert_reqs(cfg): + """ + Insert requests from input file to a table in test db. + Drop previous table of requests. + + """ + app_log.debug(f"{inspect.stack()[0][3]}()") + + if isinstance(cfg['infile'], type(None)): + app_log.error(f"Missing input file") + return AFC_ERR + + filename = cfg['infile'][0] + app_log.debug(f"{inspect.stack()[0][3]}() {filename}") + + if not os.path.isfile(filename): + app_log.error(f"Missing raw test data file {filename}") + return AFC_ERR + + if not os.path.isfile(cfg['db_filename']): + app_log.error(f"Unable to find test db file.") + return AFC_ERR + + con = sqlite3.connect(cfg['db_filename']) + # drop existing table of requests and create new one + app_log.info(f"Drop table of requests ({TBL_REQS_NAME})") + cur = con.cursor() + try: + cur.execute('DROP TABLE ' + TBL_REQS_NAME) + except Exception as OperationalError: + app_log.debug(f"Fail to drop, missing table {TBL_REQS_NAME}") + cur.execute('CREATE TABLE ' + TBL_REQS_NAME + + ' (test_id varchar(50), data json)') + con.commit() + return insert_reqs_int(filename, con, cur) + + +def extend_reqs(cfg): + """ + Insert requests from input file to a table in test db. + Drop previous table of requests. + + """ + app_log.debug(f"{inspect.stack()[0][3]}()") + + if isinstance(cfg['infile'], type(None)): + app_log.error(f"Missing input file") + return AFC_ERR + + filename = cfg['infile'][0] + app_log.debug(f"{inspect.stack()[0][3]}() {filename}") + + if not os.path.isfile(filename): + app_log.error(f"Missing raw test data file {filename}") + return AFC_ERR + + if not os.path.isfile(cfg['db_filename']): + app_log.error(f"Unable to find test db file.") + return AFC_ERR + + con = sqlite3.connect(cfg['db_filename']) + # add more rows to existing table of requests + app_log.info(f"Extending table of requests ({TBL_REQS_NAME})") + cur = con.cursor() + return insert_reqs_int(filename, con, cur) + + +def insert_devs(cfg): + """ + Insert device descriptors from input file to a table in test db. + Drop previous table of devices. + """ + app_log.debug(f"{inspect.stack()[0][3]}()") + + if isinstance(cfg['infile'], type(None)): + app_log.error(f"Missing input file") + return AFC_ERR + + filename = cfg['infile'][0] + app_log.debug(f"{inspect.stack()[0][3]}() {filename}") + + if not os.path.isfile(filename): + app_log.error(f"Missing raw test data file {filename}") + return AFC_ERR + + if not os.path.isfile(cfg['db_filename']): + app_log.error(f"Unable to find test db file.") + return AFC_ERR + + con = sqlite3.connect(cfg['db_filename']) + # drop existing table of requests and create new one + app_log.info(f"Drop table of devices ({TBL_AP_CFG_NAME})") + cur = con.cursor() + try: + cur.execute('DROP TABLE ' + TBL_AP_CFG_NAME) + except Exception as OperationalError: + app_log.debug(f"Fail to drop, missing table {TBL_AP_CFG_NAME}") + cur.execute('CREATE TABLE ' + TBL_AP_CFG_NAME + + ' (ap_config_id, data json, user_id)') + cnt = 1 + con.commit() + with open(filename, 'r') as fp_test: + while True: + dataline = fp_test.readline() + if not dataline or (len(dataline) < 72): + break + + # process dataline arguments + app_log.debug(f"= {dataline}") + + cur.execute('INSERT INTO ' + TBL_AP_CFG_NAME + + ' VALUES ( ?, ?, ?)', (cnt, dataline[:-1], 1)) + con.commit() + cnt += 1 + con.close() + return AFC_OK + + +def add_reqs(cfg): + """Prepare DB source files""" + app_log.debug(f"{inspect.stack()[0][3]}()") + + if isinstance(cfg['infile'], type(None)): + app_log.error('Missing input file') + return AFC_ERR + + filename = cfg['infile'][0] + app_log.debug('%s() %s', inspect.stack()[0][3], filename) + + if not os.path.isfile(filename): + app_log.error('Missing raw test data file %s', filename) + return AFC_ERR + + if not make_db(cfg['db_filename']): + return AFC_ERR + + # fetch available requests and responses + db_reqs_list, db_resp_list = get_db_req_resp(cfg) + + con = sqlite3.connect(cfg['db_filename']) + with open(filename, 'r') as fp_test: + while True: + dataline = fp_test.readline() + if not dataline: + break + + # process dataline arguments + request_json, metadata_json = process_jsonline(dataline) + + # reject the request if mandatory metadata arguments are not + # present + if not MANDATORY_METADATA_KEYS.issubset( + set(metadata_json.keys())): + # missing mandatory keys in test case input + app_log.error("Test case input does not contain required" + " mandatory arguments: %s", + ", ".join(list( + MANDATORY_METADATA_KEYS - set(metadata_json.keys())))) + return AFC_ERR + + # check if the test case already exists in the database test + # vectors + if metadata_json[TESTCASE_ID] in db_reqs_list: + app_log.error("Test case: %s already exists in database", + metadata_json[TESTCASE_ID]) + break + + app_log.info("Executing test case: %s", metadata_json[TESTCASE_ID]) + new_req, resp = cfg._send_recv(json.dumps(request_json)) + + # get request id from a request, response not always has it + # the request contains test category + new_req_json = json.loads(new_req.encode('utf-8')) + req_id = json_lookup('requestId', new_req_json, None) + + resp_res = json_lookup('shortDescription', resp, None) + if (resp_res[0] != 'Success') \ + and (req_id[0].lower().find('urs') == -1) \ + and (req_id[0].lower().find('ibp') == -1): + app_log.error('Failed in test response - %s', resp_res) + break + + app_log.info('Got response for the request') + json_lookup('availabilityExpireTime', resp, '0') + + app_log.info('Insert new request in DB') + cur = con.cursor() + cur.execute('INSERT INTO ' + TBL_REQS_NAME + ' VALUES ( ?, ?)', + (metadata_json[TESTCASE_ID], new_req,)) + con.commit() + + app_log.info('Insert new resp in DB') + upd_data = json.dumps(resp, sort_keys=True) + hash_obj = hashlib.sha256(upd_data.encode('utf-8')) + cur = con.cursor() + cur.execute('INSERT INTO ' + TBL_RESPS_NAME + ' values ( ?, ?, ?)', + [metadata_json[TESTCASE_ID], + upd_data, hash_obj.hexdigest()]) + con.commit() + con.close() + return AFC_OK + + +def dump_table(conn, tbl_name, out_file, pref): + app_log.debug(f"{inspect.stack()[0][3]}() {tbl_name}") + fp_new = '' + + if 'single' in out_file: + fp_new = open(out_file['single'], 'w') + + conn.execute(f"SELECT * FROM {tbl_name}") + found_data = conn.fetchall() + for val in enumerate(found_data): + if isinstance(fp_new, io.IOBase): + fp_new.write(f"{str(val)}\n") + elif 'split' in out_file: + tbl_fname = { + TBL_REQS_NAME: '_Request.txt', + TBL_RESPS_NAME: '_Response.txt' + } + new_json = json.loads(val[1][1].encode('utf-8')) + prefix, name, nbr = val[1][0].split('.') + app_log.debug(f"{inspect.stack()[0][3]}() {name} {nbr}") + # omit URS testcases + if (name.lower().find('urs') != -1) or (pref and not pref == prefix): + continue + + fp_test = open(f"{out_file['split']}/{prefix}_{name}_{nbr}" + + f"{tbl_fname[tbl_name]}", 'a') + fp_test.write(f"{val[1][1]}\n") + fp_test.close() + else: + # Just dump to the console + app_log.info(f"{val[1]}") + + if isinstance(fp_new, io.IOBase): + fp_new.close() + + +def dump_database(cfg): + """Dump data from test DB tables""" + app_log.debug(f"{inspect.stack()[0][3]}()") + find_key = '' + found_tables = [] + # keep configuration for output path and files + # 'single' - only single file for whole output + # 'split' - separate file for each response + out_file = {} + + if not os.path.isfile(cfg['db_filename']): + app_log.error(f"Missing DB file {cfg['db_filename']}") + return AFC_ERR + + set_dump_db_opts = { + 'wfa': [(TBL_REQS_NAME,), (TBL_RESPS_NAME,)], + 'all': [(TBL_REQS_NAME,), (TBL_RESPS_NAME,)], + 'req': [(TBL_REQS_NAME,)], + 'resp': [(TBL_RESPS_NAME,)], + 'ap': [('ap_config',)], + 'cfg': [('afc_config',)], + 'user': [('user_config',)] + } + + prefix = { + 'wfa': "AFCS", + 'all': None + } + + tbl = 'True' + if isinstance(cfg['table'], list): + tbl = cfg['table'][0] + con = sqlite3.connect(cfg['db_filename']) + cur = con.cursor() + + if tbl in set_dump_db_opts: + # Dump only tables with requests and responses + found_tables.extend(set_dump_db_opts[tbl]) + elif tbl == 'True': + # Dump all tables if no options provided + cur.execute(f"SELECT name FROM sqlite_master WHERE type='table';") + found_tables = cur.fetchall() + + pref = None + if tbl == 'wfa' or tbl == 'all': + if tbl in prefix: + pref = prefix[tbl] + + out_file['split'] = './' + if not isinstance(cfg['outpath'], type(None)): + out_file['split'] = cfg['outpath'][0] + '/' + + out_file['split'] += WFA_TEST_DIR + if os.path.exists(out_file['split']): + shutil.rmtree(out_file['split']) + os.mkdir(out_file['split']) + elif isinstance(cfg['outfile'], type(None)): + app_log.error(f"Missing output filename.\n") + return AFC_ERR + else: + out_file['single'] = cfg['outfile'][0] + + for tbl in enumerate(found_tables): + app_log.debug(f"Dump {tbl} to {out_file}") + dump_table(cur, tbl[1][0], out_file, pref) + con.close() + return AFC_OK + + +def export_admin_config(cfg): + """Export admin server configuration""" + app_log.debug('%s()', inspect.stack()[0][3]) + + con = sqlite3.connect(cfg['db_filename']) + cur = con.cursor() + cur.execute('SELECT COUNT(*) FROM ' + TBL_AP_CFG_NAME) + found_rcds = cur.fetchall() + with open(cfg['outfile'][0], 'w') as fp_exp: + cur.execute('SELECT * FROM %s' % TBL_AFC_CFG_NAME) + found_cfg = cur.fetchall() + app_log.debug('Found AfcCfg: %s', found_cfg) + + cur.execute('SELECT * FROM %s\n' % TBL_USERS_NAME) + found_user = cur.fetchall() + app_log.debug('Found Users: %s\n', found_user) + + cur.execute('SELECT * FROM %s\n' % TBL_AP_CFG_NAME) + found_aps = cur.fetchall() + con.close() + + aps = '' + idx = 0 + for count, val in enumerate(found_aps): + aps += str(val[1]) + ',' + app_log.debug('Found APs: %s\n', aps[:-1]) + + out_str = '{"afcAdminConfig":' + found_cfg[0][1] + ', '\ + '"userConfig":' + found_user[0][1] + ', '\ + '"apConfig":[' + aps[:-1] + ']}' + fp_exp.write(out_str) + app_log.info('Server admin config exported to %s', cfg['outfile'][0]) + return AFC_OK + + +def dry_run_test(cfg): + """Run one or more requests from provided file""" + if isinstance(cfg['infile'], type(None)): + app_log.error('Missing input file') + return AFC_ERR + + filename = cfg['infile'][0] + app_log.debug('%s() %s', inspect.stack()[0][3], filename) + + if not os.path.isfile(filename): + app_log.error('Missing raw test data file %s', filename) + return AFC_ERR + + with open(filename, 'r') as fp_test: + while True: + dataline = fp_test.readline() + if not dataline: + break + app_log.info('Request:') + app_log.info(dataline) + + # process dataline arguments + request_json, _ = process_jsonline(dataline) + + resp = _send_recv(cfg, json.dumps(request_json)) + + # get request id from a request, response not always has it + # the request contains test category + new_req_json = json.loads(json.dumps(request_json).encode('utf-8')) + req_id = json_lookup('requestId', new_req_json, None) + + resp_res = json_lookup('shortDescription', resp, None) + if (resp_res[0] != 'Success') \ + and (req_id[0].lower().find('urs') == -1): + app_log.error('Failed in test response - %s', resp_res) + app_log.debug(resp) + break + + app_log.info('Got response for the request') + app_log.info('Resp:') + app_log.info(resp) + app_log.info('\n\n') + + json_lookup('availabilityExpireTime', resp, '0') + upd_data = json.dumps(resp, sort_keys=True) + hash_obj = hashlib.sha256(upd_data.encode('utf-8')) + return AFC_OK + + +def get_nbr_testcases(cfg): + """ Find APs count on DB table """ + if not os.path.isfile(cfg['db_filename']): + print('INFO: Missing DB file %s', cfg['db_filename']) + return False + con = sqlite3.connect(cfg['db_filename']) + cur = con.cursor() + cur.execute('SELECT count("requestId") from ' + TBL_REQS_NAME) + found_data = cur.fetchall() + db_inquiry_count = found_data[0][0] + con.close() + app_log.debug("found %s ap lists from db table", db_inquiry_count) + return db_inquiry_count + + +def collect_tests2combine(sh, rows, t_ident, t2cmb, cmb_t): + """ + Lookup for combined test vectors, + build of required test vectors to combine + """ + app_log.debug('%s()\n', inspect.stack()[0][3]) + for i in range(1, rows + 1): + cell = sh.cell(row=i, column=PURPOSE_CLM) + if ((cell.value is None) or + (AFC_TEST_IDENT.get(cell.value.lower()) is None) or + (cell.value == 'SRI')): + continue + + if (t_ident != 'all') and (cell.value.lower() != t_ident): + continue + + cell = sh.cell(row=i, column=COMBINED_CLM) + if cell.value is not None and \ + cell.value.upper() != 'NO': + raw_list = str(cell.value) + + test_case_id = sh.cell(row=i, column=UNIT_NAME_CLM).value + test_case_id += "." + test_case_id += sh.cell(row=i, column=PURPOSE_CLM).value + test_case_id += "." + test_case_id += str(sh.cell(row=i, column=TEST_VEC_CLM).value) + + cmb_t[test_case_id] = [] + for t in raw_list.split(','): + if '-' in t: + # found range of test vectors + left, right = t.split('-') + t2cmb_ident = '' + for r in AFC_TEST_IDENT: + if r in left.lower(): + min = int(left.replace(r.upper(), '')) + max = int(right.replace(r.upper(), '')) + 1 + t2cmb_ident = r.upper() + for cnt in range(min, max): + tcase = t2cmb_ident + str(cnt) + t2cmb[tcase] = '' + cmb_t[test_case_id] += [tcase] + else: + # found single test vector + t2cmb[t] = '' + cmb_t[test_case_id] += [t] + + +def _parse_tests_dev_desc(sheet, fp_new, rows): + app_log.debug('%s()\n', inspect.stack()[0][3]) + for i in range(1, rows + 1): + res_str = "" + cell = sheet.cell(row=i, column=PURPOSE_CLM) + if ((cell.value is None) or + (AFC_TEST_IDENT.get(cell.value.lower()) is None) or + (cell.value == 'SRI')): + continue + + # skip combined test vectors because device descriptor is missing + cell = sheet.cell(row=i, column=COMBINED_CLM) + if cell.value is not None and \ + cell.value.upper() != 'NO': + continue + + res_str += build_device_desc( + sheet.cell(row=i, column=INDOOR_DEPL_CLM).value, + sheet.cell(row=i, column=SER_NBR_CLM).value, + sheet.cell(row=i, column=RULESET_CLM).value, + sheet.cell(row=i, column=ID_CLM).value, + True) + + fp_new.write(res_str + '\n') + return res_str + + +def _parse_tests_all(sheet, fp_new, rows, test_ident): + app_log.debug('%s()\n', inspect.stack()[0][3]) + # collect tests to combine in next loop + tests2combine = dict() + # gather combined tests + combined_tests = dict() + collect_tests2combine(sheet, rows, test_ident, + tests2combine, + combined_tests) + if len(combined_tests): + app_log.info('Found combined test vectors: %s', + ' '.join(combined_tests)) + app_log.info('Found test vectors to combine: %s', + ' '.join(tests2combine)) + for i in range(1, rows + 1): + cell = sheet.cell(row=i, column=PURPOSE_CLM) + if ((cell.value is None) or + (AFC_TEST_IDENT.get(cell.value.lower()) is None) or + (cell.value == 'SRI')): + continue + + if (test_ident != 'all') and (cell.value.lower() != test_ident): + continue + + uut = sheet.cell(row=i, column=UNIT_NAME_CLM).value + purpose = sheet.cell(row=i, column=PURPOSE_CLM).value + test_vec = sheet.cell(row=i, column=TEST_VEC_CLM).value + + test_case_id = uut + "." + purpose + "." + str(test_vec) + + # Prepare request header '{"availableSpectrumInquiryRequests": [{' + res_str = REQ_INQUIRY_HEADER + # check if the test case is combined + cell = sheet.cell(row=i, column=COMBINED_CLM) + if cell.value is not None and \ + cell.value.upper() != 'NO': + for item in combined_tests[test_case_id]: + res_str += tests2combine[item] + ',' + res_str = res_str[:-1] + else: + # + # Inquired Channels + # + res_str += '{' + REQ_INQ_CHA_HEADER + cell = sheet.cell(row=i, column=GLOBALOPERATINGCLASS_131) + res_str += '{' + REQ_INQ_CHA_GL_OPER_CLS + str(cell.value) + cell = sheet.cell(row=i, column=CHANNEL_CFI_131) + if cell.value is not None: + res_str += ', ' + REQ_INQ_CHA_CHANCFI + str(cell.value) + res_str += '}, ' + cell = sheet.cell(row=i, column=GLOBALOPERATINGCLASS_132) + res_str += '{' + REQ_INQ_CHA_GL_OPER_CLS + str(cell.value) + cell = sheet.cell(row=i, column=CHANNEL_CFI_132) + if cell.value is not None: + res_str += ', ' + REQ_INQ_CHA_CHANCFI + str(cell.value) + res_str += '}, ' + cell = sheet.cell(row=i, column=GLOBALOPERATINGCLASS_133) + res_str += '{' + REQ_INQ_CHA_GL_OPER_CLS + str(cell.value) + cell = sheet.cell(row=i, column=CHANNEL_CFI_133) + if cell.value is not None: + res_str += ', ' + REQ_INQ_CHA_CHANCFI + str(cell.value) + res_str += '}, ' + cell = sheet.cell(row=i, column=GLOBALOPERATINGCLASS_134) + res_str += '{' + REQ_INQ_CHA_GL_OPER_CLS + str(cell.value) + cell = sheet.cell(row=i, column=CHANNEL_CFI_134) + if cell.value is not None: + res_str += ', ' + REQ_INQ_CHA_CHANCFI + str(cell.value) + res_str += '}, ' + cell = sheet.cell(row=i, column=GLOBALOPERATINGCLASS_136) + res_str += '{' + REQ_INQ_CHA_GL_OPER_CLS + str(cell.value) + cell = sheet.cell(row=i, column=CHANNEL_CFI_136) + if cell.value is not None: + res_str += ', ' + REQ_INQ_CHA_CHANCFI + str(cell.value) + res_str += '}, ' + cell = sheet.cell(row=i, column=GLOBALOPERATINGCLASS_137) + res_str += '{' + REQ_INQ_CHA_GL_OPER_CLS + str(cell.value) + cell = sheet.cell(row=i, column=CHANNEL_CFI_137) + if cell.value is not None: + res_str += ', ' + REQ_INQ_CHA_CHANCFI + str(cell.value) + res_str += '}' + REQ_INQ_CHA_FOOTER + ' ' + # + # Device descriptor + # + res_str += REQ_DEV_DESC_HEADER + + res_str += build_device_desc( + sheet.cell(row=i, column=INDOOR_DEPL_CLM).value, + sheet.cell(row=i, column=SER_NBR_CLM).value, + sheet.cell(row=i, column=RULESET_CLM).value, + sheet.cell(row=i, column=ID_CLM).value, + False) + res_str += ',' + # + # Inquired Frequency Range + # + res_str += REQ_INQ_FREQ_RANG_HEADER + freq_range = AfcFreqRange() + freq_range.set_range_limit( + sheet.cell( + row=i, + column=INQ_FREQ_RNG_LOWFREQ_A), + 'low') + freq_range.set_range_limit( + sheet.cell( + row=i, + column=INQ_FREQ_RNG_HIGHFREQ_A), + 'high') + try: + res_str += freq_range.append_range() + except IncompleteFreqRange as e: + app_log.debug(f"{e} - row {i}") + freq_range = AfcFreqRange() + freq_range.set_range_limit( + sheet.cell( + row=i, + column=INQ_FREQ_RNG_LOWFREQ_B), + 'low') + freq_range.set_range_limit( + sheet.cell( + row=i, + column=INQ_FREQ_RNG_HIGHFREQ_B), + 'high') + try: + tmp_str = freq_range.append_range() + res_str += ', ' + tmp_str + except IncompleteFreqRange as e: + app_log.debug(f"{e} - row {i}") + res_str += REQ_INQ_FREQ_RANG_FOOTER + + cell = sheet.cell(row=i, column=MINDESIREDPOWER) + if (cell.value): + res_str += REQ_MIN_DESIRD_PWR + str(cell.value) + ', ' + # + # Location + # + res_str += REQ_LOC_HEADER + cell = sheet.cell(row=i, column=INDOORDEPLOYMENT) + res_str += REQ_LOC_INDOORDEPL + str(cell.value) + ', ' + + # Location - elevation + res_str += REQ_LOC_ELEV_HEADER + cell = sheet.cell(row=i, column=ELE_VERTICALUNCERTAINTY) + if isinstance(cell.value, int): + res_str += REQ_LOC_VERT_UNCERT + str(cell.value) + ', ' + cell = sheet.cell(row=i, column=ELE_HEIGHTTYPE) + res_str += REQ_LOC_HEIGHT_TYPE + '"' + str(cell.value) + '"' + cell = sheet.cell(row=i, column=ELE_HEIGHT) + if isinstance(cell.value, int) or isinstance(cell.value, float): + res_str += ', ' + REQ_LOC_HEIGHT + str(cell.value) + res_str += '}, ' + + # Location - uncertainty reqion + geo_coor = AfcGeoCoordinates(sheet, i) + try: + res_str += geo_coor.collect_coordinates() + except IncompleteGeoCoordinates as e: + app_log.debug(e) + + res_str += REQ_LOC_FOOTER + + cell = sheet.cell(row=i, column=REQ_ID_CLM) + if isinstance(cell.value, str): + req_id = cell.value + else: + req_id = "" + res_str += REQ_REQUEST_ID + '"' + req_id + '"' + res_str += '}' + + # collect test vectors required for combining + # build test case id in format + short_tcid = ''.join(test_case_id.split('.', 1)[1].split('.')) + if short_tcid in tests2combine.keys(): + tests2combine[short_tcid] = res_str.split( + REQ_INQUIRY_HEADER)[1] + + res_str += REQ_INQUIRY_FOOTER + cell = sheet.cell(row=i, column=VERSION_CLM) + res_str += REQ_VERSION + '"' + str(cell.value) + '"' + res_str += REQ_FOOTER + + # adding metadata parameters + res_str += ', ' + + # adding test case id + res_str += META_HEADER + res_str += META_TESTCASE_ID + '"' + test_case_id + '"' + res_str += META_FOOTER + fp_new.write(res_str + '\n') + return AFC_OK + + +def parse_tests(cfg): + app_log.debug('%s()\n', inspect.stack()[0][3]) + filename = '' + out_fname = '' + + if isinstance(cfg['infile'], type(None)): + app_log.error('Missing input file') + return AFC_ERR + + filename = cfg['infile'][0] + + if not os.path.isfile(filename): + app_log.error('Missing raw test data file %s', filename) + return AFC_ERR + + test_ident = cfg['test_id'] + + if isinstance(cfg['outfile'], type(None)): + out_fname = test_ident + NEW_AFC_TEST_SUFX + else: + out_fname = cfg['outfile'][0] + + wb = oxl.load_workbook(filename, data_only=True) + + for sh in wb: + app_log.debug('Sheet title: %s', sh.title) + app_log.debug('rows %d, cols %d\n', sh.max_row, sh.max_column) + + # Look for request sheet + for s in range(len(wb.sheetnames)): + if wb.sheetnames[s] == "Availability Requests": + break + + wb.active = s + sheet = wb.active + nbr_rows = sheet.max_row + app_log.debug('Rows range 1 - %d', nbr_rows + 1) + app_log.info('Export tests into file: %s', out_fname) + fp_new = open(out_fname, 'w') + + # partial parse if only certain parameters required. + # only for device descriptor for now. + if cfg['dev_desc']: + res_str = _parse_tests_dev_desc(sheet, fp_new, nbr_rows) + fp_new.write(res_str + '\n') + else: + _parse_tests_all(sheet, fp_new, nbr_rows, test_ident) + + fp_new.close() + return AFC_OK + + +def test_report(fname, runtimedata, testnumdata, testvectordata, + test_result, upd_data): + """Procedure to generate AFC test results report. + Args: + runtimedata(str) : Tested case running time. + testnumdata(str): Tested case id. + testvectordata(int): Tested vector id. + test_result(str: Test result Pass is 0 or Fail is 1) + upd_data(list): List for test response data + Return: + Create test results file. + """ + ts_time = datetime.datetime.fromtimestamp(time.time()).\ + strftime('%Y_%m_%d_%H%M%S') + # Test results output args + data_names = ['Date', 'Test Number', 'Test Vector', 'Running Time', + 'Test Result', 'Response data'] + data = {'Date': ts_time, 'Test Number': testnumdata, + 'Test Vector': testvectordata, 'Running Time': runtimedata, + 'Test Result': test_result, 'Response data': upd_data} + with open(fname, "a") as f: + file_writer = csv.DictWriter(f, fieldnames=data_names) + if os.stat(fname).st_size == 0: + file_writer.writeheader() + file_writer.writerow(data) + + +def _run_tests(cfg, reqs, resps, comparator, ids, test_cases): + """ + Run tests + reqs: {testcaseid: [request_json_str]} + resps: {testcaseid: [response_json_str, response_hash]} + comparator: reference to object + test_cases: [testcaseids] + """ + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}() {test_cases} " + f"{cfg['url_path']}") + + if not len(resps): + app_log.info(f"Unable to compare response data." + f"Suggest to make acquisition of responses.") + return AFC_ERR + + all_test_res = AFC_OK + accum_secs = 0 + + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}() " + f"{type(cfg['webui'])} ") + ssn = None + if cfg['webui'] is True: + ssn = requests.Session() + cfg['token'] = _send_recv_token(cfg, ssn) + + for test_case in test_cases: + # Default reset test_res value + test_res = AFC_OK + + req_id = ids[test_case][0] + app_log.info(f"Prepare to run test - {req_id}") + if test_case not in reqs: + app_log.warning(f"The requested test case {test_case} is " + f"invalid/not available in database") + continue + + request_data = reqs[test_case][0] + app_log.debug(f"{inspect.stack()[0][3]}() {request_data}") + + before_ts = time.monotonic() + resp = _send_recv(cfg, json.dumps(request_data), ssn) + tm_secs = time.monotonic() - before_ts + res = f"id {test_case} name {req_id} status $status time {tm_secs:.1f}" + res_template = Template(res) + + if isinstance(resp, type(None)): + test_res = AFC_ERR + all_test_res = AFC_ERR + elif cfg['webui'] is True: + pass + else: + json_lookup('availabilityExpireTime', resp, '0') + upd_data = json.dumps(resp, sort_keys=True) + + diffs = [] + hash_obj = hashlib.sha256(upd_data.encode('utf-8')) + diffs = comparator.compare_results(ref_str=resps[test_case][0], + result_str=upd_data) + if (resps[test_case][1] == hash_obj.hexdigest()) \ + if cfg['precision'] is None else (not diffs): + res = res_template.substitute(status="Ok") + else: + for line in diffs: + app_log.error(f" Difference: {line}") + app_log.error(hash_obj.hexdigest()) + test_res = AFC_ERR + + if test_res == AFC_ERR: + res = res_template.substitute(status="Fail") + app_log.error(res) + all_test_res = AFC_ERR + + accum_secs += tm_secs + app_log.info(res) + + # For saving test results option + if not isinstance(cfg['outfile'], type(None)): + test_report(cfg['outfile'][0], float(tm_secs), + test_case, req_id, + ("PASS" if test_res == AFC_OK else "FAIL"), + upd_data) + + app_log.info(f"Total testcases runtime : {round(accum_secs, 1)} secs") + + if not isinstance(cfg['outfile'], type(None)): + send_email(cfg) + + return all_test_res + + +def prep_and_run_tests(cfg, reqs, resps, ids, test_cases): + """ + Run tests + reqs: {testcaseid: [request_json_str]} + resps: {testcaseid: [response_json_str, response_hash]} + test_cases: [testcaseids] + """ + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + + test_res = AFC_OK + results_comparator = TestResultComparator(precision=cfg['precision'] or 0) + + # calculate max number of tests to run + max_nbr_tests = len(test_cases) + if not isinstance(cfg['tests2run'], type(None)): + max_nbr_tests = int("".join(cfg['tests2run'])) + + while (max_nbr_tests != 0): + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}() " + f"Number of tests to run: {max_nbr_tests}") + if max_nbr_tests < len(test_cases): + del test_cases[-(len(test_cases) - max_nbr_tests):] + + # if stress mode execute testcases in parallel and concurrent + if cfg['stress'] == 1: + app_log.debug(f"{inspect.stack()[0][3]}() max {max_nbr_tests}" + f" len {len(test_cases)}") + inputs = [(cfg, reqs, resps, results_comparator, ids, [test]) + for test in test_cases] + with Pool(max_nbr_tests) as my_pool: + results = my_pool.starmap(_run_tests, inputs) + if not any(r == 0 for r in results): + test_res = AFC_ERR + else: + res = _run_tests(cfg, reqs, resps, + results_comparator, ids, test_cases) + if res != AFC_OK: + test_res = res + # when required to run more tests than there are testcases + # start run run from the beginning + max_nbr_tests -= len(test_cases) + return test_res + + +def _convert_reqs_n_resps_to_dict(cfg): + """ + Fetch test vectors and responses from the DB + Convert to dictionaries indexed by id or original index from table + Prepare list of new indexes + """ + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + + if not os.path.isfile(cfg['db_filename']): + app_log.error('Missing DB file %s', cfg['db_filename']) + return AFC_ERR + + con = sqlite3.connect(cfg['db_filename']) + cur = con.cursor() + cur.execute('SELECT * FROM %s' % TBL_REQS_NAME) + found_reqs = cur.fetchall() + cur.execute('SELECT * FROM %s' % TBL_RESPS_NAME) + found_resps = cur.fetchall() + con.close() + + # reformat the reqs_dict and resp_dict accordingly + if not isinstance(cfg['testcase_ids'], type(None)): + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}() by id") + reqs_dict = { + row[0]: [json.loads(row[1])] + for row in found_reqs + } + resp_dict = { + row[0]: [row[1], row[2]] + for row in found_resps + } + ids_dict = { + row[0]: [row[0], req_index + 1] + for req_index, row in enumerate(found_reqs) + } + test_indx = list(map(str.strip, cfg.pop("testcase_ids").split(','))) + cfg.pop("testcase_indexes") + else: + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}() by index") + reqs_dict = { + str(req_index + 1): [json.loads(row[1])] + for req_index, row in enumerate(found_reqs) + } + resp_dict = { + str(resp_index + 1): [row[1], row[2]] + for resp_index, row in enumerate(found_resps) + } + ids_dict = { + str(req_index + 1): [row[0], req_index + 1] + for req_index, row in enumerate(found_reqs) + } + if not isinstance(cfg['testcase_indexes'], type(None)): + test_indx = list(map(str.strip, + cfg.pop("testcase_indexes").split(','))) + cfg.pop("testcase_ids") + else: + test_indx = [ + str(item) for item in list(range(1, len(reqs_dict) + 1)) + ] + + # build list of indexes, omitting non-existing elements + test_cases = list() + for i in range(0, len(test_indx)): + if reqs_dict.get(test_indx[i]) is None: + app_log.debug(f"Missing value for index {test_indx[i]}") + continue + test_cases.append(test_indx[i]) + app_log.debug(f"{inspect.stack()[0][3]}() Final list of indexes. " + f"{test_cases}") + + return reqs_dict, resp_dict, ids_dict, test_cases + + +def run_test(cfg): + """Fetch test vectors from the DB and run tests""" + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}() " + f"{cfg['tests']}, {cfg['url_path']}") + + reqs_dict, resp_dict, ids, test_cases = _convert_reqs_n_resps_to_dict(cfg) + return prep_and_run_tests(cfg, reqs_dict, resp_dict, ids, test_cases) + + +def stress_run(cfg): + """ + Get test vectors from the database and run tests + in parallel + """ + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}() " + f"{cfg['tests']}, {cfg['url_path']}") + cfg['stress'] = 1 + + return run_test(cfg) + + +def _run_cert_tests(cfg): + """ + Run tests + """ + app_log.debug( + f"({os.getpid()}) {inspect.stack()[0][3]}() {cfg['url_path']}") + + test_res = AFC_OK + + try: + if isinstance(cfg['cli_cert'], type(None)): + rawresp = requests.get(cfg['url_path'], + verify="".join(cfg['ca_cert'])) + else: + rawresp = requests.get(cfg['url_path'], + cert=("".join(cfg['cli_cert']), + "".join(cfg['cli_key'])), + verify="".join(cfg['ca_cert'])) + except Exception as e: + app_log.error(f"({os.getpid()}) {inspect.stack()[0][3]}() {e}") + test_res = AFC_ERR + except OSError as os_err: + proc = psutil.Process() + app_log.error(f"({os.getpid()}) {inspect.stack()[0][3]}() " + f"{os_err} - {proc.open_files()}") + test_res = AFC_ERR + else: + if rawresp.status_code != 200: + test_res = AFC_ERR + + return test_res + + +def run_cert_tests(cfg): + """ + Run certificate tests + """ + app_log.debug(f"({os.getpid()}) {inspect.stack()[0][3]}()") + + test_res = AFC_OK + + # calculate max number of tests to run + if isinstance(cfg['tests2run'], type(None)): + app_log.error(f"Missing number of tests to run.") + return AFC_ERR + + max_nbr_tests = int("".join(cfg['tests2run'])) + + before_ts = time.monotonic() + while (max_nbr_tests != 0): + # if stress mode execute testcases in parallel and concurrent + inputs = [(cfg)] + with Pool(max_nbr_tests) as my_pool: + results = my_pool.map(_run_cert_tests, inputs) + if not any(r == 0 for r in results): + test_res = AFC_ERR + max_nbr_tests -= 1 + tm_secs = time.monotonic() - before_ts + app_log.info(f"Tests {max_nbr_tests} done at {tm_secs:.3f} s") + + return test_res + + +log_level_map = { + 'debug': logging.DEBUG, # 10 + 'info': logging.INFO, # 20 + 'warn': logging.WARNING, # 30 + 'err': logging.ERROR, # 40 + 'crit': logging.CRITICAL, # 50 +} + + +def set_log_level(opt): + app_log.setLevel(log_level_map[opt]) + return log_level_map[opt] + + +def get_version(cfg): + """Get AFC test utility version""" + app_log.info('AFC Test utility version %s', __version__) + app_log.info('AFC Test db hash %s', get_md5(AFC_TEST_DB_FILENAME)) + + +def pre_hook(cfg): + """Execute provided functionality prior to main command""" + app_log.debug(f"{inspect.stack()[0][3]}()") + return subprocess.call(cfg['prefix_cmd']) + + +def parse_run_test_args(cfg): + """Parse arguments for command 'run_test'""" + app_log.debug(f"{inspect.stack()[0][3]}()") + if isinstance(cfg['addr'], list): + # check if provided required certification files + if (cfg['prot'] != AFC_PROT_NAME): + # update URL if not the default protocol + cfg['url_path'] = cfg['prot'] + '://' + + cfg['url_path'] += str(cfg['addr'][0]) + ':' + str(cfg['port']) + cfg['base_url'] = cfg['url_path'] + '/' + if cfg['webui'] is False: + cfg['url_path'] += AFC_URL_SUFFIX + AFC_REQ_NAME + else: + cfg['url_token'] = cfg['url_path'] + AFC_WEBUI_URL_SUFFIX +\ + AFC_WEBUI_TOKEN + cfg['url_path'] += AFC_URL_SUFFIX + AFC_WEBUI_REQ_NAME + return AFC_OK + + +def parse_run_cert_args(cfg): + """Parse arguments for command 'run_cert'""" + app_log.debug(f"{inspect.stack()[0][3]}()") + if ((not isinstance(cfg['addr'], list)) or + isinstance(cfg['ca_cert'], type(None))): + app_log.error(f"{inspect.stack()[0][3]}() Missing arguments") + return AFC_ERR + + cfg['url_path'] = cfg['prot'] + '://' + str(cfg['addr'][0]) +\ + ':' + str(cfg['port']) + '/' + return AFC_OK + + +# available commands to execute in alphabetical order +execution_map = { + 'add_reqs': [add_reqs, parse_run_test_args], + 'ins_reqs': [insert_reqs, parse_run_test_args], + 'ext_reqs': [extend_reqs, parse_run_test_args], + 'ins_devs': [insert_devs, parse_run_test_args], + 'cmp_cfg': [compare_afc_config, parse_run_test_args], + 'dry_run': [dry_run_test, parse_run_test_args], + 'dump_db': [dump_database, parse_run_test_args], + 'get_nbr_testcases': [get_nbr_testcases, parse_run_test_args], + 'exp_adm_cfg': [export_admin_config, parse_run_test_args], + 'parse_tests': [parse_tests, parse_run_test_args], + 'reacq': [start_acquisition, parse_run_test_args], + 'run': [run_test, parse_run_test_args], + 'run_cert': [run_cert_tests, parse_run_cert_args], + 'stress': [stress_run, parse_run_test_args], + 'ver': [get_version, parse_run_test_args], +} + + +def make_arg_parser(): + """Define command line options""" + args_parser = argparse.ArgumentParser( + epilog=__doc__.strip(), + formatter_class=argparse.RawTextHelpFormatter) + + args_parser.add_argument('--addr', nargs=1, type=str, + help="
    - set AFC Server address.\n") + args_parser.add_argument('--prot', nargs='?', choices=['https', 'http'], + default='https', + help=" - set connection protocol " + "(default=https).\n") + args_parser.add_argument('--port', nargs='?', default='443', + type=int, + help=" - set connection port " + "(default=443).\n") + args_parser.add_argument('--conn_type', nargs='?', + choices=['sync', 'async'], default='sync', + help=" - set connection to be " + "synchronous or asynchronous (default=sync).\n") + args_parser.add_argument('--conn_tm', nargs='?', default=None, type=int, + help=" - set timeout for asynchronous " + "connection (default=None). \n") + args_parser.add_argument('--verif', action='store_true', + help=" - skip SSL verification " + "on post request.\n") + args_parser.add_argument('--outfile', nargs=1, type=str, + help=" - set filename for output " + "of tests results.\n") + args_parser.add_argument( + '--outpath', + nargs=1, + type=str, + help=" - set path to output filename for " + "results output.\n") + args_parser.add_argument('--infile', nargs=1, type=str, + help=" - set filename as a source " + "for test requests.\n") + args_parser.add_argument('--debug', action='store_true', + help="during a request make files " + "with details for debugging.\n") + args_parser.add_argument('--elaborated_debug', action='store_true', + help="during a request make files " + "with even more details for debugging.\n") + args_parser.add_argument('--gui', action='store_true', + help="during a request make files " + "with details for debugging.\n") + args_parser.add_argument('--webui', action='store_true', + help="during a request make files\n") + args_parser.add_argument('--log', type=set_log_level, + default='info', dest='log_level', + help=" - set " + "logging level (default=info).\n") + args_parser.add_argument('--testcase_indexes', nargs='?', + help=" - set single or group of tests " + "to run.\n") + args_parser.add_argument( + '--testcase_ids', + nargs='?', + help=" - set single or group of test case ids " + "to run.\n") + args_parser.add_argument('--table', nargs=1, type=str, + help=" - set " + "database table name.\n") + args_parser.add_argument('--idx', nargs='?', + type=int, + help=" - set table record index.\n") + args_parser.add_argument('--test_id', + default='all', + help="WFA test identifier, for example " + "srs, urs, fsp, ibp, sip, etc (default=all).\n") + args_parser.add_argument( + "--precision", + metavar="PRECISION_DB", + type=float, + help="Maximum allowed deviation of power limits from " + "reference values in dB. 0 means exact match is " + "required. Default is to use hash-based exact match " + "comparison") + args_parser.add_argument('--cache', action='store_true', + help="enable cache during a request, otherwise " + "disabled.\n") + args_parser.add_argument( + '--tests2run', + nargs=1, + type=str, + help=" - the total number of tests to run.\n") + args_parser.add_argument( + '--ca_cert', + nargs=1, + type=str, + help=" - set CA certificate filename to " + "verify the remote server.\n") + args_parser.add_argument( + '--cli_cert', + nargs=1, + type=str, + help=" - set client certificate filename.\n") + args_parser.add_argument( + '--cli_key', + nargs=1, + type=str, + help=" - set client private key filename.\n") + args_parser.add_argument('--dev_desc', action='store_true', + help="parse only device descriptors values.\n") + args_parser.add_argument( + '--prefix_cmd', + nargs='*', + type=str, + help="hook to call currently provided command before " + "main command specified by --cmd option.\n") + args_parser.add_argument('--email_from', type=str, + help=" - set sender email.\n") + args_parser.add_argument('--email_to', type=str, + help=" - set receiver email.\n") + args_parser.add_argument( + '--email_cc', + type=str, + help=" - set receiver of cc email.\n") + args_parser.add_argument('--email_pwd', type=str, + help=" - set sender email password.\n") + + args_parser.add_argument( + '--cmd', + choices=execution_map.keys(), + nargs='?', + help="run - run test from DB and compare.\n" + "dry_run - run test from file and show response.\n" + "exp_adm_cfg - export admin config into a file.\n" + "add_reqs - run test from provided file and insert with response into " + "the databsse.\n" + "ins_reqs - insert test vectors from provided file into the test db.\n" + "ins_devs - insert device descriptors from provided file " + "into the test db.\n" + "dump_db - dump tables from the database.\n" + "get_nbr_testcases - return number of testcases.\n" + "parse_tests - parse WFA provided tests into a files.\n" + "reacq - reacquision every test from the database and insert new " + "responses.\n" + "cmp_cfg - compare AFC config from the DB to provided from a file.\n" + "stress - run tests in stress mode.\n" + "ver - get version.\n") + + return args_parser + + +def prepare_args(parser, cfg): + """Prepare required parameters""" + app_log.debug(f"{inspect.stack()[0][3]}() {parser.parse_args()}") + cfg.update(vars(parser.parse_args())) + + # check if test indexes and test ids are given + if cfg["testcase_indexes"] and cfg["testcase_ids"]: + # reject the request + app_log.error('Please use either "--testcase_indexes"' + ' or "--testcase_ids" but not both') + return AFC_ERR + + return execution_map[cfg['cmd']][1](cfg) + + +def main_execution(cfg): + """Call all requested commands""" + app_log.debug(f"{inspect.stack()[0][3]}()") + if (isinstance(cfg['prefix_cmd'], list) and + (pre_hook(cfg) == AFC_ERR)): + return AFC_ERR + if isinstance(cfg['cmd'], type(None)): + parser.print_help() + return AFC_ERR + return execution_map[cfg['cmd']][0](cfg) + + +def main(): + """Main function of the utility""" + app_log.setLevel(logging.INFO) + console_log = logging.StreamHandler() + console_log.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) + app_log.addHandler(console_log) + + res = AFC_OK + parser = make_arg_parser() + test_cfg = TestCfg() + if prepare_args(parser, test_cfg) == AFC_ERR: + # error in preparing arguments + res = AFC_ERR + else: + res = main_execution(test_cfg) + sys.exit(res) + + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + sys.exit(1) + + +# Local Variables: +# mode: Python +# indent-tabs-mode: nil +# python-indent: 4 +# End: +# +# vim: sw=4:et:tw=80:cc=+1 diff --git a/tests/brcm_ext.txt b/tests/brcm_ext.txt new file mode 100644 index 0000000..7889397 --- /dev/null +++ b/tests/brcm_ext.txt @@ -0,0 +1,2 @@ +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP1", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP1"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180621, "longitude": -97.560614}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-EXT_FSP1"}], "version": "1.4"}, {"testCaseId": "BRCM.EXT_FSP.1"} +{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP7", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP7"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": -3}, "ellipse": {"center": {"latitude": 33.180621, "longitude": -97.560614}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-EXT_FSP7"}], "version": "1.4"}, {"testCaseId": "BRCM.EXT_FSP.7"} diff --git a/tests/del_admin_cfg.json b/tests/del_admin_cfg.json new file mode 100644 index 0000000..d27c247 --- /dev/null +++ b/tests/del_admin_cfg.json @@ -0,0 +1,3 @@ +{"afcAdminConfig":{"userConfig": {"username": "alpha"},"apConfig":[{"serialNumber":"Alpha001"},{"serialNumber":"Alpha002"},{"serialNumber":"Alpha003"}]}} +{"afcAdminConfig":{"userConfig": {"username": "bravo"},"apConfig":[{"serialNumber":"Bravo001"},{"serialNumber":"Bravo002"},{"serialNumber":"Bravo003"}]}} +{"afcAdminConfig":{"userConfig": {"username": "charlie"},"apConfig":[{"serialNumber":"SN-FSP1"},{"serialNumber":"SN-FSP2"},{"serialNumber":"SN-FSP3"},{"serialNumber":"SN-FSP4"},{"serialNumber":"SN-FSP5"},{"serialNumber":"SN-FSP6"},{"serialNumber":"SN-FSP7"},{"serialNumber":"SN-FSP8"},{"serialNumber":"SN-FSP9"},{"serialNumber":"SN-FSP10"},{"serialNumber":"SN-FSP11"},{"serialNumber":"SN-FSP12"},{"serialNumber":"SN-FSP13"},{"serialNumber":"SN-FSP14"},{"serialNumber":"SN-FSP15"},{"serialNumber":"SN-FSP16"},{"serialNumber":"SN-FSP17"},{"serialNumber":"SN-FSP18"},{"serialNumber":"SN-FSP19"},{"serialNumber":"SN-FSP20"},{"serialNumber":"SN-FSP21"},{"serialNumber":"SN-FSP22"},{"serialNumber":"SN-FSP23"},{"serialNumber":"SN-FSP24"},{"serialNumber":"SN-FSP25"},{"serialNumber":"SN-FSP26"},{"serialNumber":"SN-FSP27"},{"serialNumber":"SN-FSP28"},{"serialNumber":"SN-FSP29"},{"serialNumber":"SN-FSP30"},{"serialNumber":"SN-FSP31"},{"serialNumber":"SN-FSP32"},{"serialNumber":"SN-FSP33"},{"serialNumber":"SN-FSP34"},{"serialNumber":"SN-FSP35"},{"serialNumber":"SN-FSP36"},{"serialNumber":"SN-SIP1"},{"serialNumber":"SN-SIP2"},{"serialNumber":"SN-SIP3"},{"serialNumber":"SN-SIP4"},{"serialNumber":"SN-SIP5"},{"serialNumber":"SN-SIP6"},{"serialNumber":"SN-SIP7"},{"serialNumber":"SN-SIP8"},{"serialNumber":"SN-SIP9"},{"serialNumber":"SN-SIP10"},{"serialNumber":"SN-SIP11"},{"serialNumber":"SN-SIP12"},{"serialNumber":"SN-SIP13"},{"serialNumber":"SN-SIP14"},{"serialNumber":"SN-SIP15"},{"serialNumber":"SN-SRS1"},{"serialNumber":"SN-URS1"},{"serialNumber":"SN-URS2"},{"serialNumber":"SN-URS3"},{"serialNumber":"SN-URS4"},{"serialNumber":"SN-URS5"},{"serialNumber":"SN-URS6"}]}} diff --git a/tests/dump.sql b/tests/dump.sql new file mode 100644 index 0000000..416480c --- /dev/null +++ b/tests/dump.sql @@ -0,0 +1,409 @@ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +CREATE TABLE user_config (user_id integer PRIMARY KEY, user_cfg json); +INSERT INTO user_config VALUES(1,'{"username": "charlie", "password":"123456", "rolename": ["AP","Admin"]}'); +CREATE TABLE afc_config (afc_config_id integer PRIMARY KEY, cfg json, user_id integer, FOREIGN KEY(user_id) REFERENCES user_config(user_id)); +INSERT INTO afc_config VALUES(1,'{"afcConfig": {"APUncertainty": {"height": 5, "maxHorizontalUncertaintyDistance": 650, "maxVerticalUncertainty": 100, "points_per_degree": 3600}, "ITMParameters": {"conductivity": 0.02, "dielectricConst": 25, "ground": "Good Ground", "maxPoints": 1500, "minSpacing": 30, "polarization": "Vertical"}, "allowScanPtsInUncReg": false, "bodyLoss": {"kind": "Fixed Value", "valueIndoor": 0, "valueOutdoor": 0}, "buildingPenetrationLoss": {"kind": "Fixed Value", "value": 20.5}, "cdsmDir": "", "channelResponseAlgorithm": "psd", "clutterAtFS": true, "deniedRegionFile": "", "depDir": "rat_transfer/3dep/1_arcsec", "enableMapInVirtualAp": false, "freqBands": [{"name": "UNII-5", "startFreqMHz": 5925, "stopFreqMHz": 6425}, {"name": "UNII-7", "startFreqMHz": 6525, "stopFreqMHz": 6875}], "fsClutterModel": {"maxFsAglHeight": 6, "p2108Confidence": 5}, "fsDatabaseFile": "rat_transfer/ULS_Database/CONUS_ULS_LATEST.sqlite3", "fsReceiverNoise": {"freqList": [6425], "noiseFloorList": [-110, -109.5]}, "globeDir": "rat_transfer/globe", "indoorFixedHeightAMSL": false, "inquiredFrequencyResolutionMHz": 1, "lidarDir": "rat_transfer/proc_lidar_2019", "maxEIRP": 36, "maxLinkDistance": 130, "minEIRPIndoor": -100, "minEIRPOutdoor": -100, "minPSD": -100, "nearFieldAdjFlag": true, "nfaTableFile": "rat_transfer/nfa/nfa_table_data.csv", "nlcdFile": "rat_transfer/nlcd/nlcd_production", "passiveRepeaterFlag": true, "polarizationMismatchLoss": {"kind": "Fixed Value", "value": 3}, "prTableFile": "rat_transfer/pr/WINNF-TS-1014-V1.2.0-App02.csv", "printSkippedLinksFlag": false, "propagationEnv": "NLCD Point", "propagationModel": {"buildingSource": "None", "fsplUseGroundDistance": false, "itmConfidence": 5, "itmReliability": 20, "kind": "FCC 6GHz Report & Order", "p2108Confidence": 25, "terrainSource": "3DEP (30m)", "win2ConfidenceCombined": 16, "win2ConfidenceLOS": 16, "win2UseGroundDistance": false, "winner2HgtFlag": false, "winner2HgtLOS": 15, "winner2LOSOption": "BLDG_DATA_REQ_TX"}, "radioClimateFile": "rat_transfer/itudata/TropoClim.txt", "rainForestFile": "", "receiverFeederLoss": {"IDU": 3, "ODU": 0, "UNKNOWN": 3}, "regionDir": "rat_transfer/population", "regionStr": "US", "reportErrorRlanHeightLowFlag": false, "reportUnavailPSDdBPerMHz": -40, "reportUnavailableSpectrum": true, "rlanITMTxClutterMethod": "FORCE_TRUE", "roundPSDEIRPFlag": true, "scanPointBelowGroundMethod": "truncate", "srtmDir": "rat_transfer/srtm3arcsecondv003", "surfRefracFile": "rat_transfer/itudata/N050.TXT", "threshold": -6, "ulsDefaultAntennaType": "WINNF-AIP-07", "version": "3.8.12+localbuild", "visibilityThreshold": -6, "worldPopulationFile": "rat_transfer/population/gpw_v4_population_density_rev11_2020_30_sec.tif"}}',1); +CREATE TABLE ap_config (ap_config_id, data json, user_id); +INSERT INTO ap_config VALUES(1,'{"location": "2","serialNumber": "SRS1","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SRS1"}]}',1); +INSERT INTO ap_config VALUES(2,'{"location": "2","serialNumber": "URS1","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": ""}]}',1); +INSERT INTO ap_config VALUES(3,'{"location": "2","serialNumber": "","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-URS2"}]}',1); +INSERT INTO ap_config VALUES(4,'{"location": "2","serialNumber": "URS3","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-URS3"}]}',1); +INSERT INTO ap_config VALUES(5,'{"location": "2","serialNumber": "URS4","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-URS4"}]}',1); +INSERT INTO ap_config VALUES(6,'{"location": "2","serialNumber": "URS5","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-URS5"}]}',1); +INSERT INTO ap_config VALUES(7,'{"location": "2","serialNumber": "URS6","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-URS6"}]}',1); +INSERT INTO ap_config VALUES(8,'{"location": "2","serialNumber": "URS7","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-URS7"}]}',1); +INSERT INTO ap_config VALUES(9,'{"location": "3","serialNumber": "FSP1","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP1"}]}',1); +INSERT INTO ap_config VALUES(10,'{"location": "3","serialNumber": "FSP2","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP2"}]}',1); +INSERT INTO ap_config VALUES(11,'{"location": "3","serialNumber": "FSP3","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP3"}]}',1); +INSERT INTO ap_config VALUES(12,'{"location": "3","serialNumber": "FSP4","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP4"}]}',1); +INSERT INTO ap_config VALUES(13,'{"location": "3","serialNumber": "FSP5","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP5"}]}',1); +INSERT INTO ap_config VALUES(14,'{"location": "3","serialNumber": "FSP6","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP6"}]}',1); +INSERT INTO ap_config VALUES(15,'{"location": "2","serialNumber": "FSP7","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP7"}]}',1); +INSERT INTO ap_config VALUES(16,'{"location": "2","serialNumber": "FSP8","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP8"}]}',1); +INSERT INTO ap_config VALUES(17,'{"location": "2","serialNumber": "FSP9","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP9"}]}',1); +INSERT INTO ap_config VALUES(18,'{"location": "2","serialNumber": "FSP10","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP10"}]}',1); +INSERT INTO ap_config VALUES(19,'{"location": "2","serialNumber": "FSP11","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP11"}]}',1); +INSERT INTO ap_config VALUES(20,'{"location": "2","serialNumber": "FSP12","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP12"}]}',1); +INSERT INTO ap_config VALUES(21,'{"location": "3","serialNumber": "FSP13","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP13"}]}',1); +INSERT INTO ap_config VALUES(22,'{"location": "3","serialNumber": "FSP14","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP14"}]}',1); +INSERT INTO ap_config VALUES(23,'{"location": "3","serialNumber": "FSP15","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP15"}]}',1); +INSERT INTO ap_config VALUES(24,'{"location": "3","serialNumber": "FSP16","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP16"}]}',1); +INSERT INTO ap_config VALUES(25,'{"location": "3","serialNumber": "FSP17","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP17"}]}',1); +INSERT INTO ap_config VALUES(26,'{"location": "3","serialNumber": "FSP18","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP18"}]}',1); +INSERT INTO ap_config VALUES(27,'{"location": "2","serialNumber": "FSP19","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP19"}]}',1); +INSERT INTO ap_config VALUES(28,'{"location": "2","serialNumber": "FSP20","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP20"}]}',1); +INSERT INTO ap_config VALUES(29,'{"location": "2","serialNumber": "FSP21","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP21"}]}',1); +INSERT INTO ap_config VALUES(30,'{"location": "2","serialNumber": "FSP22","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP22"}]}',1); +INSERT INTO ap_config VALUES(31,'{"location": "2","serialNumber": "FSP23","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP23"}]}',1); +INSERT INTO ap_config VALUES(32,'{"location": "2","serialNumber": "FSP24","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP24"}]}',1); +INSERT INTO ap_config VALUES(33,'{"location": "3","serialNumber": "FSP25","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP25"}]}',1); +INSERT INTO ap_config VALUES(34,'{"location": "3","serialNumber": "FSP26","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP26"}]}',1); +INSERT INTO ap_config VALUES(35,'{"location": "3","serialNumber": "FSP27","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP27"}]}',1); +INSERT INTO ap_config VALUES(36,'{"location": "3","serialNumber": "FSP28","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP28"}]}',1); +INSERT INTO ap_config VALUES(37,'{"location": "3","serialNumber": "FSP29","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP29"}]}',1); +INSERT INTO ap_config VALUES(38,'{"location": "3","serialNumber": "FSP30","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP30"}]}',1); +INSERT INTO ap_config VALUES(39,'{"location": "2","serialNumber": "FSP31","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP31"}]}',1); +INSERT INTO ap_config VALUES(40,'{"location": "2","serialNumber": "FSP32","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP32"}]}',1); +INSERT INTO ap_config VALUES(41,'{"location": "2","serialNumber": "FSP33","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP33"}]}',1); +INSERT INTO ap_config VALUES(42,'{"location": "2","serialNumber": "FSP34","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP34"}]}',1); +INSERT INTO ap_config VALUES(43,'{"location": "2","serialNumber": "FSP35","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP35"}]}',1); +INSERT INTO ap_config VALUES(44,'{"location": "2","serialNumber": "FSP36","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP36"}]}',1); +INSERT INTO ap_config VALUES(45,'{"location": "2","serialNumber": "FSP37","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP37"}]}',1); +INSERT INTO ap_config VALUES(46,'{"location": "2","serialNumber": "FSP38","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP38"}]}',1); +INSERT INTO ap_config VALUES(47,'{"location": "2","serialNumber": "FSP39","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP39"}]}',1); +INSERT INTO ap_config VALUES(48,'{"location": "2","serialNumber": "FSP40","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP40"}]}',1); +INSERT INTO ap_config VALUES(49,'{"location": "2","serialNumber": "FSP41","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP41"}]}',1); +INSERT INTO ap_config VALUES(50,'{"location": "2","serialNumber": "FSP42","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP42"}]}',1); +INSERT INTO ap_config VALUES(51,'{"location": "2","serialNumber": "FSP43","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP43"}]}',1); +INSERT INTO ap_config VALUES(52,'{"location": "2","serialNumber": "FSP44","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP44"}]}',1); +INSERT INTO ap_config VALUES(53,'{"location": "2","serialNumber": "FSP45","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP45"}]}',1); +INSERT INTO ap_config VALUES(54,'{"location": "2","serialNumber": "FSP46","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP46"}]}',1); +INSERT INTO ap_config VALUES(55,'{"location": "2","serialNumber": "FSP47","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP47"}]}',1); +INSERT INTO ap_config VALUES(56,'{"location": "2","serialNumber": "FSP48","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP48"}]}',1); +INSERT INTO ap_config VALUES(57,'{"location": "2","serialNumber": "FSP49","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP49"}]}',1); +INSERT INTO ap_config VALUES(58,'{"location": "2","serialNumber": "FSP50","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP50"}]}',1); +INSERT INTO ap_config VALUES(59,'{"location": "2","serialNumber": "FSP51","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP51"}]}',1); +INSERT INTO ap_config VALUES(60,'{"location": "2","serialNumber": "FSP52","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP52"}]}',1); +INSERT INTO ap_config VALUES(61,'{"location": "2","serialNumber": "FSP53","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP53"}]}',1); +INSERT INTO ap_config VALUES(62,'{"location": "2","serialNumber": "FSP54","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP54"}]}',1); +INSERT INTO ap_config VALUES(63,'{"location": "2","serialNumber": "FSP55","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP55"}]}',1); +INSERT INTO ap_config VALUES(64,'{"location": "2","serialNumber": "FSP56","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP56"}]}',1); +INSERT INTO ap_config VALUES(65,'{"location": "2","serialNumber": "FSP57","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP57"}]}',1); +INSERT INTO ap_config VALUES(66,'{"location": "2","serialNumber": "FSP58","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP58"}]}',1); +INSERT INTO ap_config VALUES(67,'{"location": "2","serialNumber": "FSP59","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP59"}]}',1); +INSERT INTO ap_config VALUES(68,'{"location": "2","serialNumber": "FSP60","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP60"}]}',1); +INSERT INTO ap_config VALUES(69,'{"location": "2","serialNumber": "FSP61","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP61"}]}',1); +INSERT INTO ap_config VALUES(70,'{"location": "3","serialNumber": "FSP62","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP62"}]}',1); +INSERT INTO ap_config VALUES(71,'{"location": "3","serialNumber": "FSP63","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP63"}]}',1); +INSERT INTO ap_config VALUES(72,'{"location": "3","serialNumber": "FSP64","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP64"}]}',1); +INSERT INTO ap_config VALUES(73,'{"location": "2","serialNumber": "FSP65","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP65"}]}',1); +INSERT INTO ap_config VALUES(74,'{"location": "2","serialNumber": "FSP66","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP66"}]}',1); +INSERT INTO ap_config VALUES(75,'{"location": "2","serialNumber": "FSP67","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP67"}]}',1); +INSERT INTO ap_config VALUES(76,'{"location": "2","serialNumber": "FSP68","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP68"}]}',1); +INSERT INTO ap_config VALUES(77,'{"location": "2 ","serialNumber": "FSP69","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP69"}]}',1); +INSERT INTO ap_config VALUES(78,'{"location": "2","serialNumber": "FSP70","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP70"}]}',1); +INSERT INTO ap_config VALUES(79,'{"location": "3","serialNumber": "FSP71","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP71"}]}',1); +INSERT INTO ap_config VALUES(80,'{"location": "3","serialNumber": "FSP72","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP72"}]}',1); +INSERT INTO ap_config VALUES(81,'{"location": "2","serialNumber": "FSP73","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP73"}]}',1); +INSERT INTO ap_config VALUES(82,'{"location": "2","serialNumber": "FSP74","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP74"}]}',1); +INSERT INTO ap_config VALUES(83,'{"location": "2","serialNumber": "FSP75","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP75"}]}',1); +INSERT INTO ap_config VALUES(84,'{"location": "2","serialNumber": "FSP76","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP76"}]}',1); +INSERT INTO ap_config VALUES(85,'{"location": "3","serialNumber": "FSP77","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP77"}]}',1); +INSERT INTO ap_config VALUES(86,'{"location": "3","serialNumber": "FSP78","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP78"}]}',1); +INSERT INTO ap_config VALUES(87,'{"location": "2","serialNumber": "FSP79","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP79"}]}',1); +INSERT INTO ap_config VALUES(88,'{"location": "2","serialNumber": "FSP80","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP80"}]}',1); +INSERT INTO ap_config VALUES(89,'{"location": "2","serialNumber": "FSP81","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP81"}]}',1); +INSERT INTO ap_config VALUES(90,'{"location": "2","serialNumber": "FSP82","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP82"}]}',1); +INSERT INTO ap_config VALUES(91,'{"location": "2","serialNumber": "FSP83","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP83"}]}',1); +INSERT INTO ap_config VALUES(92,'{"location": "2","serialNumber": "FSP84","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP84"}]}',1); +INSERT INTO ap_config VALUES(93,'{"location": "3","serialNumber": "FSP85","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP85"}]}',1); +INSERT INTO ap_config VALUES(94,'{"location": "3","serialNumber": "FSP86","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP86"}]}',1); +INSERT INTO ap_config VALUES(95,'{"location": "2","serialNumber": "FSP87","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP87"}]}',1); +INSERT INTO ap_config VALUES(96,'{"location": "2","serialNumber": "FSP88","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP88"}]}',1); +INSERT INTO ap_config VALUES(97,'{"location": "2","serialNumber": "FSP89","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP89"}]}',1); +INSERT INTO ap_config VALUES(98,'{"location": "2","serialNumber": "FSP90","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP90"}]}',1); +INSERT INTO ap_config VALUES(99,'{"location": "2","serialNumber": "FSP91","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP91"}]}',1); +INSERT INTO ap_config VALUES(100,'{"location": "2","serialNumber": "FSP92","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP92"}]}',1); +INSERT INTO ap_config VALUES(101,'{"location": "3","serialNumber": "FSP93","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP93"}]}',1); +INSERT INTO ap_config VALUES(102,'{"location": "3","serialNumber": "FSP94","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP94"}]}',1); +INSERT INTO ap_config VALUES(103,'{"location": "2","serialNumber": "FSP95","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP95"}]}',1); +INSERT INTO ap_config VALUES(104,'{"location": "2","serialNumber": "FSP96","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP96"}]}',1); +INSERT INTO ap_config VALUES(105,'{"location": "2","serialNumber": "FSP97","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP97"}]}',1); +INSERT INTO ap_config VALUES(106,'{"location": "2","serialNumber": "FSP98","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP98"}]}',1); +INSERT INTO ap_config VALUES(107,'{"location": "2","serialNumber": "FSP99","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-FSP99"}]}',1); +INSERT INTO ap_config VALUES(108,'{"location": "2","serialNumber": "IBP1","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP1"}]}',1); +INSERT INTO ap_config VALUES(109,'{"location": "2","serialNumber": "IBP2","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP2"}]}',1); +INSERT INTO ap_config VALUES(110,'{"location": "2","serialNumber": "IBP3","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP3"}]}',1); +INSERT INTO ap_config VALUES(111,'{"location": "2","serialNumber": "IBP4","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP4"}]}',1); +INSERT INTO ap_config VALUES(112,'{"location": "2","serialNumber": "IBP5","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP5"}]}',1); +INSERT INTO ap_config VALUES(113,'{"location": "2","serialNumber": "IBP6","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP6"}]}',1); +INSERT INTO ap_config VALUES(114,'{"location": "2","serialNumber": "IBP7","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP7"}]}',1); +INSERT INTO ap_config VALUES(115,'{"location": "2","serialNumber": "IBP8","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-IBP8"}]}',1); +INSERT INTO ap_config VALUES(116,'{"location": "2","serialNumber": "SIP1","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP1"}]}',1); +INSERT INTO ap_config VALUES(117,'{"location": "2","serialNumber": "SIP2","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP2"}]}',1); +INSERT INTO ap_config VALUES(118,'{"location": "2","serialNumber": "SIP3","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP3"}]}',1); +INSERT INTO ap_config VALUES(119,'{"location": "2","serialNumber": "SIP4","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP4"}]}',1); +INSERT INTO ap_config VALUES(120,'{"location": "2","serialNumber": "SIP5","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP5"}]}',1); +INSERT INTO ap_config VALUES(121,'{"location": "2","serialNumber": "SIP6","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP6"}]}',1); +INSERT INTO ap_config VALUES(122,'{"location": "2","serialNumber": "SIP7","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP7"}]}',1); +INSERT INTO ap_config VALUES(123,'{"location": "2","serialNumber": "SIP8","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP8"}]}',1); +INSERT INTO ap_config VALUES(124,'{"location": "2","serialNumber": "SIP9","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP9"}]}',1); +INSERT INTO ap_config VALUES(125,'{"location": "2","serialNumber": "SIP10","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP10"}]}',1); +INSERT INTO ap_config VALUES(126,'{"location": "2","serialNumber": "SIP11","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP11"}]}',1); +INSERT INTO ap_config VALUES(127,'{"location": "2","serialNumber": "SIP12","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP12"}]}',1); +INSERT INTO ap_config VALUES(128,'{"location": "2","serialNumber": "SIP13","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP13"}]}',1); +INSERT INTO ap_config VALUES(129,'{"location": "2","serialNumber": "SIP14","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP14"}]}',1); +INSERT INTO ap_config VALUES(130,'{"location": "2","serialNumber": "SIP15","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP15"}]}',1); +INSERT INTO ap_config VALUES(131,'{"location": "2","serialNumber": "SIP16","certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E","id": "FCCID-SIP16"}]}',1); +CREATE TABLE test_vectors (test_id varchar(50), data json); +INSERT INTO test_vectors VALUES('AFCS.SRS.1','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SRS1", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SRS1"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180621, "longitude": -97.560614}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-SRS1"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.URS.1','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "URS1", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": ""}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 34.051151, "longitude": -118.255078}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-URS1"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.URS.2','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-URS2"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.723655, "longitude": -87.683357}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-URS2"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.URS.3','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "URS3", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-URS3"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-URS3"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.URS.4','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "URS4", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-URS4"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 29.75077, "longitude": -95.36454}}}, "requestId": "REQ-URS4"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.URS.5','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "URS5", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-URS5"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL"}, "ellipse": {"center": {"latitude": 39.949079, "longitude": -75.161307}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-URS5"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.URS.6','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "URS6", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-URS6"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.449081, "longitude": -112.081081}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-URS6"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.URS.7','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "URS7", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-URS7"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": -51.692741, "longitude": -57.85685}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-URS7"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.1','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP1", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP1"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180621, "longitude": -97.560614}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP1"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.2','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP2", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP2"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.177062, "longitude": -97.546817}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP2"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.3','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP3", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP3"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180553, "longitude": -97.560701}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP3"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.4','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP4", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP4"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.879231, "longitude": -87.636215}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP4"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.5','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP5", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP5"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.878912, "longitude": -87.635929}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP5"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.6','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP6", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP6"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.878912, "longitude": -87.635929}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP6"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.7','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP7", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP7"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": -3}, "ellipse": {"center": {"latitude": 33.180621, "longitude": -97.560614}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP7"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.8','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP8", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP8"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.177062, "longitude": -97.546817}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP8"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.9','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP9", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP9"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180553, "longitude": -97.560701}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP9"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.10','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP10", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP10"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.879231, "longitude": -87.636215}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP10"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.11','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP11", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP11"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.878912, "longitude": -87.635929}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP11"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.12','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP12", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP12"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.878912, "longitude": -87.635929}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP12"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.13','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP13", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP13"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.769641, "longitude": -118.376295}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}}, "requestId": "REQ-FSP13"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.14','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP14", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP14"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.770381, "longitude": -118.376872}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}}, "requestId": "REQ-FSP14"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.15','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP15", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP15"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.770381, "longitude": -118.376872}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}}, "requestId": "REQ-FSP15"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.16','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP16", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP16"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 33.769641, "longitude": -118.376295}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}}, "requestId": "REQ-FSP16"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.17','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP17", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP17"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 33.772642, "longitude": -118.375067}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}}, "requestId": "REQ-FSP17"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.18','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP18", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP18"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 33.772642, "longitude": -118.375067}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}}, "requestId": "REQ-FSP18"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.19','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP19", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP19"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.769641, "longitude": -118.376295}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}}, "requestId": "REQ-FSP19"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.20','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP20", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP20"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.770381, "longitude": -118.376872}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}}, "requestId": "REQ-FSP20"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.21','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP21", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP21"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.770381, "longitude": -118.376872}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}}, "requestId": "REQ-FSP21"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.22','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP22", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP22"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 33.769641, "longitude": -118.376295}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}}, "requestId": "REQ-FSP22"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.23','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP23", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP23"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 33.772642, "longitude": -118.375067}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}}, "requestId": "REQ-FSP23"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.24','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP24", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP24"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 33.772642, "longitude": -118.375067}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}}, "requestId": "REQ-FSP24"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.25','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP25", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP25"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 30.571694, "longitude": -102.230361}, "orientation": 0, "minorAxis": 100, "majorAxis": 100}}, "requestId": "REQ-FSP25"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.26','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP26", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP26"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 30.573949, "longitude": -102.234875}, "orientation": 0, "minorAxis": 300, "majorAxis": 300}}, "requestId": "REQ-FSP26"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.27','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP27", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP27"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 30.086965, "longitude": -101.103761}, "orientation": 70, "minorAxis": 250, "majorAxis": 250}}, "requestId": "REQ-FSP27"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.28','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP28", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP28"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 30.571694, "longitude": -102.230361}, "orientation": 0, "minorAxis": 100, "majorAxis": 100}}, "requestId": "REQ-FSP28"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.29','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP29", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP29"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 30.573949, "longitude": -102.234875}, "orientation": 0, "minorAxis": 300, "majorAxis": 300}}, "requestId": "REQ-FSP29"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.30','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP30", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP30"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 30.086965, "longitude": -101.103761}, "orientation": 70, "minorAxis": 250, "majorAxis": 250}}, "requestId": "REQ-FSP30"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.31','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP31", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP31"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "linearPolygon": {"outerBoundary": [{"latitude": 30.5725933, "longitude": -102.231406}, {"latitude": 30.5725933, "longitude": -102.229316}, {"latitude": 30.570795, "longitude": -102.229316}, {"latitude": 30.570795, "longitude": -102.231406}]}}, "requestId": "REQ-FSP31"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.32','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP32", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP32"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "radialPolygon": {"outerBoundary": [{"length": 300, "angle": 0}, {"length": 300, "angle": 36}, {"length": 300, "angle": 72}, {"length": 300, "angle": 108}, {"length": 300, "angle": 144}, {"length": 300, "angle": 180}, {"length": 300, "angle": 216}, {"length": 300, "angle": 252}, {"length": 300, "angle": 288}, {"length": 300, "angle": 324}], "center": {"latitude": 30.573949, "longitude": -102.234875}}}, "requestId": "REQ-FSP32"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.33','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP33", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP33"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 30.086965, "longitude": -101.103761}, "orientation": 70, "minorAxis": 250, "majorAxis": 250}}, "requestId": "REQ-FSP33"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.34','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP34", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP34"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 30.571694, "longitude": -102.230361}, "orientation": 0, "minorAxis": 100, "majorAxis": 100}}, "requestId": "REQ-FSP34"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.35','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP35", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP35"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 30.573949, "longitude": -102.234875}, "orientation": 0, "minorAxis": 300, "majorAxis": 300}}, "requestId": "REQ-FSP35"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.36','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP36", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP36"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 30.086965, "longitude": -101.103761}, "orientation": 70, "minorAxis": 250, "majorAxis": 250}}, "requestId": "REQ-FSP36"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.37','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP37", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP37"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 9}, "ellipse": {"center": {"latitude": 34.0517490391756, "longitude": -118.174086769162}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP37"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.38','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP38", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP38"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 55}, "ellipse": {"center": {"latitude": 33.44493, "longitude": -112.067148}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP38"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.39','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP39", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP39"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 7}, "ellipse": {"center": {"latitude": 33.867634, "longitude": -118.037267}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP39"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.40','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP40", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP40"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 89}, "ellipse": {"center": {"latitude": 33.4657921944995, "longitude": -111.969947953029}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP40"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.41','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP41", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP41"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 9}, "ellipse": {"center": {"latitude": 32.780716, "longitude": -117.134037}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}}, "requestId": "REQ-FSP41"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.42','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP42", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP42"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 8}, "ellipse": {"center": {"latitude": 32.773875, "longitude": -117.139232}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}}, "requestId": "REQ-FSP42"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.43','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP43", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP43"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 83}, "ellipse": {"center": {"latitude": 39.792935, "longitude": -105.018517}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}}, "requestId": "REQ-FSP43"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.44','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP44", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP44"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 9}, "ellipse": {"center": {"latitude": 34.0517490391756, "longitude": -118.174086769162}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP44"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.45','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP45", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP45"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 55}, "ellipse": {"center": {"latitude": 33.44493, "longitude": -112.067148}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP45"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.46','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP46", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP46"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 7}, "ellipse": {"center": {"latitude": 33.867634, "longitude": -118.037267}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP46"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.47','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP47", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP47"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 89}, "ellipse": {"center": {"latitude": 33.4657921944995, "longitude": -111.969947953029}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP47"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.48','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP48", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP48"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 9}, "ellipse": {"center": {"latitude": 32.780716, "longitude": -117.134037}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}}, "requestId": "REQ-FSP48"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.49','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP49", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP49"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 8}, "ellipse": {"center": {"latitude": 32.773875, "longitude": -117.139232}, "orientation": 10, "minorAxis": 30, "majorAxis": 50}}, "requestId": "REQ-FSP49"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.50','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP50", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP50"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 83}, "ellipse": {"center": {"latitude": 39.792935, "longitude": -105.018517}, "orientation": 45, "minorAxis": 50, "majorAxis": 50}}, "requestId": "REQ-FSP50"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.51','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131, "channelCfi": [21, 25, 29, 33]}, {"globalOperatingClass": 132, "channelCfi": [19, 27, 35]}, {"globalOperatingClass": 133, "channelCfi": [23, 39]}, {"globalOperatingClass": 134, "channelCfi": [15, 47]}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP51", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP51"}]}, "inquiredFrequencyRange": [{"lowFrequency": 6048, "highFrequency": 6109}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.892312, "longitude": -87.609841}, "orientation": 0, "minorAxis": 5, "majorAxis": 10}}, "requestId": "REQ-FSP51"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.52','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131, "channelCfi": [21, 25, 29, 33]}, {"globalOperatingClass": 132, "channelCfi": [19, 27, 35]}, {"globalOperatingClass": 133, "channelCfi": [23, 39]}, {"globalOperatingClass": 134, "channelCfi": [15, 47]}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP52", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP52"}]}, "inquiredFrequencyRange": [{"lowFrequency": 6048, "highFrequency": 6109}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.892312, "longitude": -87.609841}, "orientation": 0, "minorAxis": 5, "majorAxis": 10}}, "requestId": "REQ-FSP52"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.53','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131, "channelCfi": [81, 85, 89]}, {"globalOperatingClass": 132, "channelCfi": [83, 91]}, {"globalOperatingClass": 133, "channelCfi": [87]}, {"globalOperatingClass": 134, "channelCfi": [79]}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP53", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP53"}]}, "inquiredFrequencyRange": [{"lowFrequency": 6360, "highFrequency": 6391}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 42.333582, "longitude": -83.053009}, "orientation": 10, "minorAxis": 5, "majorAxis": 5}}, "requestId": "REQ-FSP53"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.54','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131, "channelCfi": [81, 85, 89]}, {"globalOperatingClass": 132, "channelCfi": [83, 91]}, {"globalOperatingClass": 133, "channelCfi": [87]}, {"globalOperatingClass": 134, "channelCfi": [79]}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP54", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP54"}]}, "inquiredFrequencyRange": [{"lowFrequency": 6360, "highFrequency": 6391}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 42.333582, "longitude": -83.053009}, "orientation": 10, "minorAxis": 5, "majorAxis": 5}}, "requestId": "REQ-FSP54"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.55','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131, "channelCfi": [13, 17, 21, 25]}, {"globalOperatingClass": 132, "channelCfi": [11, 19, 27]}, {"globalOperatingClass": 133, "channelCfi": [7, 23]}, {"globalOperatingClass": 134, "channelCfi": [15]}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP55", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP55"}]}, "inquiredFrequencyRange": [{"lowFrequency": 6019, "highFrequency": 6079}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 39.286173, "longitude": -76.606187}, "orientation": 10, "minorAxis": 5, "majorAxis": 10}}, "requestId": "REQ-FSP55"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.56','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131, "channelCfi": [13, 17, 21, 25]}, {"globalOperatingClass": 132, "channelCfi": [11, 19, 27]}, {"globalOperatingClass": 133, "channelCfi": [7, 23]}, {"globalOperatingClass": 134, "channelCfi": [15]}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP56", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP56"}]}, "inquiredFrequencyRange": [{"lowFrequency": 6019, "highFrequency": 6079}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 39.286173, "longitude": -76.606187}, "orientation": 10, "minorAxis": 5, "majorAxis": 10}}, "requestId": "REQ-FSP56"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.57','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP57", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP57"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 61.068, "longitude": -152.0359}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}}, "requestId": "REQ-FSP57"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.58','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP58", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP58"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 64.203, "longitude": -149.3355}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}}, "requestId": "REQ-FSP58"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.59','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP59", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP59"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 69.154632, "longitude": -148.899575}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}}, "requestId": "REQ-FSP59"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.60','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP60", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP60"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 70.328691, "longitude": -149.63919}, "orientation": 0, "minorAxis": 25, "majorAxis": 25}}, "requestId": "REQ-FSP60"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.61','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP61", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP61"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 70.328372, "longitude": -149.637287}, "orientation": 0, "minorAxis": 25, "majorAxis": 25}}, "requestId": "REQ-FSP61"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.62','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP62", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP62"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 38.823357, "longitude": -120.68475}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP62"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.63','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP63", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP63"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 38.820129, "longitude": -120.68426}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP63"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.64','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP64", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP64"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 38.816705, "longitude": -120.695567}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP64"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.65','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP65", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP65"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 38.823357, "longitude": -120.68475}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP65"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.66','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP66", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP66"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 38.820129, "longitude": -120.68426}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP66"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.67','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP67", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP67"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 38.816705, "longitude": -120.695567}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP67"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.68','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP68", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP68"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 38.823357, "longitude": -120.68475}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP68"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.69','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP69", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP69"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 38.820129, "longitude": -120.68426}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP69"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.70','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP70", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP70"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 38.816705, "longitude": -120.695567}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP70"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.71','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP71", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP71"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.095169, "longitude": -116.42293}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP71"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.72','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP72", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP72"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.073958, "longitude": -116.421737}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP72"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.73','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP73", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP73"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.095169, "longitude": -116.42293}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP73"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.74','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP74", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP74"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.073958, "longitude": -116.421737}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP74"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.75','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP75", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP75"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.095169, "longitude": -116.42293}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP75"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.76','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP76", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP76"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.073958, "longitude": -116.421737}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP76"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.77','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP77", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP77"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 39.523761, "longitude": -121.300259}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP77"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.78','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP78", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP78"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 39.519614, "longitude": -121.275352}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP78"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.79','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP79", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP79"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 39.523761, "longitude": -121.300259}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP79"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.80','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP80", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP80"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 39.51883, "longitude": -121.301513}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP80"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.81','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP81", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP81"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 39.519614, "longitude": -121.275352}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP81"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.82','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP82", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP82"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 39.523761, "longitude": -121.300259}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP82"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.83','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP83", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP83"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 39.51883, "longitude": -121.301513}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP83"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.84','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP84", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP84"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 39.519614, "longitude": -121.275352}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP84"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.85','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP85", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP85"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.684652, "longitude": -76.483668}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP85"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.86','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP86", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP86"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.711802, "longitude": -76.473996}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP86"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.87','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP87", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP87"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.681608, "longitude": -76.482183}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP87"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.88','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP88", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP88"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.684652, "longitude": -76.483668}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP88"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.89','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP89", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP89"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 41.711802, "longitude": -76.473996}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP89"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.90','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP90", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP90"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.681608, "longitude": -76.482183}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP90"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.91','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP91", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP91"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.684652, "longitude": -76.483668}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP91"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.92','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP92", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP92"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.711802, "longitude": -76.473996}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP92"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.93','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP93", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP93"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 47.608377, "longitude": -122.327159}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP93"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.94','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP94", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP94"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 47.608377, "longitude": -122.327159}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP94"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.95','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP95", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP95"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 47.608377, "longitude": -122.327159}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP95"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.96','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP96", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP96"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 47.608377, "longitude": -122.327159}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP96"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.97','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP97", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP97"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 47.608377, "longitude": -122.327159}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP97"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.98','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP98", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP98"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 18}, "ellipse": {"center": {"latitude": 47.747233, "longitude": -121.088367}, "orientation": 45, "minorAxis": 30, "majorAxis": 30}}, "requestId": "REQ-FSP98"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.99','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP99", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP99"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 47.741269, "longitude": -121.077035}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP99"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.FSP.100','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP1", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP1"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180621, "longitude": -97.560614}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP1"}, {"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP2", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP2"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.177062, "longitude": -97.546817}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP2"}, {"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP3", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP3"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180553, "longitude": -97.560701}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP3"}, {"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP4", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP4"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.879231, "longitude": -87.636215}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP4"}, {"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP5", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP5"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.878912, "longitude": -87.635929}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP5"}, {"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP6", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP6"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 10, "heightType": "AGL", "height": 100}, "ellipse": {"center": {"latitude": 41.878912, "longitude": -87.635929}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-FSP6"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.IBP.1','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP1", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-IBP1"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 46.4968, "longitude": -84.331771}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}}, "requestId": "REQ-IBP1"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.IBP.2','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP2", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-IBP2"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 48.359797, "longitude": -92.155281}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}}, "requestId": "REQ-IBP2"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.IBP.3','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP3", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-IBP3"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 54.90827, "longitude": -130.838902}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}}, "requestId": "REQ-IBP3"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.IBP.4','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP4", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-IBP4"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 42.32452, "longitude": -83.054659}, "orientation": 0, "minorAxis": 30, "majorAxis": 30}}, "requestId": "REQ-IBP4"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.IBP.5','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP5", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-IBP5"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"orientation": 0, "minorAxis": 30, "majorAxis": 30}}, "requestId": "REQ-IBP5"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.IBP.6','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP6", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-IBP6"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"orientation": 0, "minorAxis": 30, "majorAxis": 30}}, "requestId": "REQ-IBP6"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.IBP.7','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP7", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-IBP7"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"orientation": 0, "minorAxis": 30, "majorAxis": 30}}, "requestId": "REQ-IBP7"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.IBP.8','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "IBP8", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-IBP8"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 2, "elevation": {"verticalUncertainty": 3, "heightType": "AGL", "height": 3}, "ellipse": {"orientation": 0, "minorAxis": 30, "majorAxis": 30}}, "requestId": "REQ-IBP8"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.1','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP1", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP1"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 10}, "ellipse": {"center": {"latitude": 18.16277, "longitude": -66.722083}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP1"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.2','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP2", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP2"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 30, "heightType": "AGL", "height": 300}, "ellipse": {"center": {"latitude": 38.377266, "longitude": -78.468021}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP2"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.3','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP3", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP3"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 12}, "ellipse": {"center": {"latitude": 33.8291, "longitude": -107.388916}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP3"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.4','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP4", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP4"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 18}, "ellipse": {"center": {"latitude": 48.361254, "longitude": -119.585033}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP4"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.5','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP5", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP5"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 8, "heightType": "AGL", "height": 78}, "ellipse": {"center": {"latitude": 30.351048, "longitude": -103.669369}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP5"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.6','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP6", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP6"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 5, "heightType": "AGL", "height": 45}, "ellipse": {"center": {"latitude": 43.256237, "longitude": -71.675651}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP6"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.7','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP7", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP7"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 6}, "ellipse": {"center": {"latitude": 31.914955, "longitude": -111.304475}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP7"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.8','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP8", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP8"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 8}, "ellipse": {"center": {"latitude": 35.507881, "longitude": -106.377759}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP8"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.9','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP9", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP9"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 15}, "ellipse": {"center": {"latitude": 19.705507, "longitude": -155.122522}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP9"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.10','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP10", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP10"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 8}, "ellipse": {"center": {"latitude": 41.486251, "longitude": -91.432634}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP10"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.11','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP11", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP11"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 11}, "ellipse": {"center": {"latitude": 37.006164, "longitude": -118.014402}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP11"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.12','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP12", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP12"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 12}, "ellipse": {"center": {"latitude": 34.282091, "longitude": -108.385163}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP12"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.13','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP13", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP13"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 6}, "ellipse": {"center": {"latitude": 17.69417, "longitude": -64.864822}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP13"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.14','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP14", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP14"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3.95}, "ellipse": {"center": {"latitude": 37.402836, "longitude": -117.959636}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP14"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.15','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP15", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP15"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 15}, "ellipse": {"center": {"latitude": 40.641732, "longitude": -121.268964}, "orientation": 0, "minorAxis": 150, "majorAxis": 150}}, "requestId": "REQ-SIP15"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('AFCS.SIP.16','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "SIP16", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-SIP16"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 1, "heightType": "AGL", "height": 14.2}, "ellipse": {"center": {"latitude": 48.996, "longitude": -119.62}, "orientation": 0, "minorAxis": 10, "majorAxis": 10}}, "requestId": "REQ-SIP16"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('BRCM.EXT_FSP.1','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP1", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP1"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 0, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": 3}, "ellipse": {"center": {"latitude": 33.180621, "longitude": -97.560614}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-EXT_FSP1"}], "version": "1.4"}'); +INSERT INTO test_vectors VALUES('BRCM.EXT_FSP.7','{"availableSpectrumInquiryRequests": [{"inquiredChannels": [{"globalOperatingClass": 131}, {"globalOperatingClass": 132}, {"globalOperatingClass": 133}, {"globalOperatingClass": 134}, {"globalOperatingClass": 136}, {"globalOperatingClass": 137}], "deviceDescriptor": {"serialNumber": "FSP7", "certificationId": [{"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP7"}]}, "inquiredFrequencyRange": [{"lowFrequency": 5925, "highFrequency": 6425}, {"lowFrequency": 6525, "highFrequency": 6875}], "location": {"indoorDeployment": 1, "elevation": {"verticalUncertainty": 2, "heightType": "AGL", "height": -3}, "ellipse": {"center": {"latitude": 33.180621, "longitude": -97.560614}, "orientation": 45, "minorAxis": 50, "majorAxis": 100}}, "requestId": "REQ-EXT_FSP7"}], "version": "1.4"}'); +CREATE TABLE test_data (test_id varchar(50), data json, hash varchar(255)); +INSERT INTO test_data VALUES('AFCS.SRS.1','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-22.9, -22.9, -22.9, 11.3, 18, 18.1, 29.9, 11.3, -10.3, -10.3, -10.3, -10.3, 12.8, 12.8, 12.9, 30.6, 19.8, 19.8, 33.2, 33.2, -0.4, -0.4, -0.3, -0.3, 36, 36, 36, 36, -27, -27, -27, -27, 35.1, 36, 34.9, 28, 34.5, 0.6, 0.6, 11, 35]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-19.9, -19.8, 17.3, 13.4, -7.3, -7.3, 15.8, 15.9, 22.8, 28.5, 2.6, 2.7, 36, -24, -24, -24, 36, 29.4, 3.6, 14]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-16.9, 10.3, -4.3, 18.9, 25.9, 5.7, -21, -21, 6.6]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-13.8, -1.2, 8.6, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-23]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-10.7, -15]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5951, "lowFrequency": 5930}, "maxPsd": -36}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5951}, "maxPsd": -35.9}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5990}, "maxPsd": 2.9}, {"frequencyRange": {"highFrequency": 6047, "lowFrequency": 6020}, "maxPsd": 5}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6047}, "maxPsd": 5.1}, {"frequencyRange": {"highFrequency": 6058, "lowFrequency": 6050}, "maxPsd": 17.2}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6058}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6078}, "maxPsd": 16.9}, {"frequencyRange": {"highFrequency": 6119, "lowFrequency": 6107}, "maxPsd": -23.4}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6119}, "maxPsd": -23.3}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6221, "lowFrequency": 6182}, "maxPsd": -0.2}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6221}, "maxPsd": -0.1}, {"frequencyRange": {"highFrequency": 6243, "lowFrequency": 6242}, "maxPsd": 17.5}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6243}, "maxPsd": 17.6}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6271}, "maxPsd": 6.8}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6302}, "maxPsd": 20.2}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6359}, "maxPsd": -13.4}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": -13.3}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6620, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6620}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": 14}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6667, "lowFrequency": 6660}, "maxPsd": 1}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6667}, "maxPsd": 1.1}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": 21.9}, {"frequencyRange": {"highFrequency": 6757, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6763, "lowFrequency": 6757}, "maxPsd": 15}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6763}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 14.7}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": -12.4}, {"frequencyRange": {"highFrequency": 6813, "lowFrequency": 6810}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6813}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": -2}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 22}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-SRS1", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','77dcd7ca31353636fef2abf3d1f3e84952f260633e01e3d1d3aa73b675420e2d'); +INSERT INTO test_data VALUES('AFCS.URS.1','{"availableSpectrumInquiryResponses": [{"requestId": "REQ-URS1", "response": {"responseCode": 103, "shortDescription": "One or more fields have an invalid value.", "supplementalInfo": "{\"invalidParams\": [\"certificationId\", \"\"]}"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','b9cd67f895127a6f5ffa27515e4b6fe7d52212b0ffa91c9cd3378a4679db5e93'); +INSERT INTO test_data VALUES('AFCS.URS.2','{"availableSpectrumInquiryResponses": [{"requestId": "REQ-URS2", "response": {"responseCode": 103, "shortDescription": "One or more fields have an invalid value.", "supplementalInfo": "{\"invalidParams\": [\"serialNumber\", \"\"]}"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','eb41c354d4150d0406f30fc5981890374ebed85aa8a981b003549ac0376c1eb9'); +INSERT INTO test_data VALUES('AFCS.URS.3','{"availableSpectrumInquiryResponses": [{"requestId": "REQ-URS3", "response": {"responseCode": 102, "shortDescription": "Missing Param", "supplementalInfo": {"missingParams": ["center"]}}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','9218f0586b552fdd6517fdbb8b7c6c66f4b2ab44f744bf53f845a3de0fd500ea'); +INSERT INTO test_data VALUES('AFCS.URS.4','{"availableSpectrumInquiryResponses": [{"requestId": "REQ-URS4", "response": {"responseCode": 102, "shortDescription": "Missing Param", "supplementalInfo": {"missingParams": ["majorAxis", "minorAxis", "orientation"]}}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','698be6d8122532c45bf9f10610326ca5412ed93d9e574a9fdb9ac6a1297f983c'); +INSERT INTO test_data VALUES('AFCS.URS.5','{"availableSpectrumInquiryResponses": [{"requestId": "REQ-URS5", "response": {"responseCode": 102, "shortDescription": "Missing Param", "supplementalInfo": {"missingParams": ["height"]}}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','7f723778577b1f26b20b7e3cbf5f328d7e847d5579cbb1922356d612a2fc903f'); +INSERT INTO test_data VALUES('AFCS.URS.6','{"availableSpectrumInquiryResponses": [{"requestId": "REQ-URS6", "response": {"responseCode": 102, "shortDescription": "Missing Param", "supplementalInfo": {"missingParams": ["verticalUncertainty"]}}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','4e0c6a4c97d0b1815cc3df1dc6dd486d973c69c5e42d8e503a4dca617a857a52'); +INSERT INTO test_data VALUES('AFCS.URS.7','{"availableSpectrumInquiryResponses": [{"requestId": "REQ-URS7", "response": {"responseCode": 103, "shortDescription": "Invalid Value", "supplementalInfo": {"invalidParams": ["latitude", "longitude"]}}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','0aa0bb975679b4d94b286a1e5ac9a74c906fb8fc557a874ba9e8cd7df43f7d32'); +INSERT INTO test_data VALUES('AFCS.FSP.1','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-2.4, -2.4, -2.4, 31.8, 36, 36, 36, 31.8, 10.2, 10.2, 10.2, 10.2, 33.3, 33.3, 33.4, 36, 36, 36, 36, 36, 20.1, 20.1, 20.2, 20.2, 36, 36, 36, 36, -8, -27, -27, -7.9, 36, 36, 36, 36, 36, 21.1, 21.1, 31.5, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [0.6, 0.7, 36, 33.9, 13.2, 13.2, 36, 36, 36, 36, 23.1, 23.2, 36, -13.1, -24, -13, 36, 36, 24.1, 34.5]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [3.6, 30.8, 16.2, 36, 36, 26.2, -21, -12.9, 27.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [6.7, 19.3, 29.1, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-2.5]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [9.8, 1.7]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5951, "lowFrequency": 5930}, "maxPsd": -15.5}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5951}, "maxPsd": -15.4}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6119, "lowFrequency": 6107}, "maxPsd": -2.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6119}, "maxPsd": -2.8}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6221, "lowFrequency": 6182}, "maxPsd": 20.3}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6221}, "maxPsd": 20.4}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6242}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6359}, "maxPsd": 7.1}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": 7.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6667, "lowFrequency": 6660}, "maxPsd": 21.5}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6667}, "maxPsd": 21.6}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 8.1}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6830}, "maxPsd": 22.9}], "requestId": "REQ-FSP1", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','6cd63205f81e4a0337104c571ad4d17829afc03033ba6886de683ff2970237e9'); +INSERT INTO test_data VALUES('AFCS.FSP.2','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [25.3, 25.3, 25.3, 36, 36, 36, 36, 36, 30.2, 30.2, 30.3, 30.3, 35.1, 35.1, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 19.2, -4.4, -18.9, -18.9, 15.1, 36, 36, 36, 36, 31.5, 16.9, 16.9, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [28.3, 28.3, 36, 36, 33.2, 33.3, 36, 36, 36, 36, 36, 36, 36, 20.3, -15.9, -15.9, 21.2, 36, 19.9, 19.9]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [31.3, 36, 36, 36, 36, 36, -12.9, -12.8, 22.9]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [34.4, 36, 36, -9.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 26.2]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5959}, "maxPsd": 12.3}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6153, "lowFrequency": 6107}, "maxPsd": 17.2}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6153}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6213}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": -17.4}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 22.4}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -31.9}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 18.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 3.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6830}, "maxPsd": 22.9}], "requestId": "REQ-FSP2", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','969489f49d0a8d9d2351a6a79f6ba73b8dbc78f0c7289d98f63e4a132e05675c'); +INSERT INTO test_data VALUES('AFCS.FSP.3','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-2.4, -2.4, -2.4, 31.8, 36, 36, 36, 31.8, 10.2, 10.2, 10.2, 10.2, 33.3, 33.3, 33.4, 36, 36, 36, 36, 36, 20.1, 20.1, 20.2, 20.2, 36, 36, 36, 36, -9.3, -27, -27, -9.2, 36, 36, 36, 36, 36, 21.1, 21.1, 31.5, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [0.6, 0.7, 36, 33.9, 13.2, 13.2, 36, 36, 36, 36, 23.1, 23.2, 36, -14.4, -24, -14.3, 36, 36, 24.1, 34.5]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [3.6, 30.8, 16.2, 36, 36, 26.2, -21, -14.2, 27.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [6.7, 19.3, 29.1, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-2.5]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [9.8, 0.4]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5951, "lowFrequency": 5930}, "maxPsd": -15.5}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5951}, "maxPsd": -15.4}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6119, "lowFrequency": 6107}, "maxPsd": -2.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6119}, "maxPsd": -2.8}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6221, "lowFrequency": 6182}, "maxPsd": 20.3}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6221}, "maxPsd": 20.4}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6242}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6359}, "maxPsd": 7.1}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": 7.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": 21.6}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 8.1}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6830}, "maxPsd": 22.9}], "requestId": "REQ-FSP3", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','de5f69531fc0bbbd9ca53c1d74cc4093f6ecb0e17931352abdda3530c6b246b6'); +INSERT INTO test_data VALUES('AFCS.FSP.4','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 28.7, 28.8, 36, 36, 36, 36, 36, 36, 36, 22.4, 22.4, 22.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 19.4, 12.7, 12.7, 12.7, 36, 35.8, 12.3, 12.3]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 31.7, 31.8, 36, 36, 36, 25.4, 25.5, 36, 36, 36, 36, 36, 36, 36, 15.7, 15.7, 36, 15.3]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 34.8, 36, 28.4, 28.5, 36, 36, 18.7, 18.8]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 31.4, 31.6, 21.6]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [34.3, 34.5]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6048, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6069, "lowFrequency": 6048}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6069}, "maxPsd": 15.8}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 9.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6737, "lowFrequency": 6710}, "maxPsd": 6.4}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6737}, "maxPsd": 6.5}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": -0.3}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6830}, "maxPsd": -0.7}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP4", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','69c858bf58c4304c053e6918ecd39a4e114cfc99494966057ade26a1e5034266'); +INSERT INTO test_data VALUES('AFCS.FSP.5','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 28.7, 28.8, 36, 36, 36, 36, 36, 36, 36, 22.4, 22.4, 22.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 19.4, 12.7, 12.7, 12.7, 36, 35.8, 12.3, 12.3]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 31.7, 31.8, 36, 36, 36, 25.4, 25.5, 36, 36, 36, 36, 36, 36, 36, 15.7, 15.7, 36, 15.3]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 34.8, 36, 28.4, 28.5, 36, 36, 18.7, 18.8]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 31.4, 31.6, 21.6]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [34.3, 34.5]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6048, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6069, "lowFrequency": 6048}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6069}, "maxPsd": 15.8}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 9.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6737, "lowFrequency": 6710}, "maxPsd": 6.4}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6737}, "maxPsd": 6.5}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": -0.3}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6830}, "maxPsd": -0.7}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP5", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','d4315b0a75c44eb85761880e11afc0b2d15f08a47a1c6db7ec1a88a18dccde78'); +INSERT INTO test_data VALUES('AFCS.FSP.6','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 28.7, 28.8, 36, 36, 36, 36, 36, 36, 36, 22.4, 22.4, 22.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 19.4, 12.7, 12.7, 12.7, 36, 35.8, 12.3, 12.3]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 31.7, 31.8, 36, 36, 36, 25.4, 25.5, 36, 36, 36, 36, 36, 36, 36, 15.7, 15.7, 36, 15.3]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 34.8, 36, 28.4, 28.5, 36, 36, 18.7, 18.8]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 31.4, 31.6, 21.6]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [34.3, 34.5]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6048, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6069, "lowFrequency": 6048}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6069}, "maxPsd": 15.8}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 9.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6737, "lowFrequency": 6710}, "maxPsd": 6.4}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6737}, "maxPsd": 6.5}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": -0.3}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6830}, "maxPsd": -0.7}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP6", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','cb9c3a5fb245db5bdf298c3a7d567a75bb0c317991d4cce2fe812057ad5c14ec'); +INSERT INTO test_data VALUES('AFCS.FSP.7','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-19.3, -19.3, -19.2, 15, 18, 18.1, 29.9, 15.7, -6, -6, -5.9, -5.9, 13.5, 13.5, 18.8, 30.6, 19.8, 19.8, 33.2, 33.2, -0.3, -0.3, -0.3, -0.2, 36, 36, 36, 36, -18.6, -27, -27, -18.6, 36, 36, 34.9, 28, 34.5, 0.6, 0.6, 11, 35]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-16.3, -16.2, 20.9, 17.8, -3, -2.9, 16.5, 21.9, 22.8, 28.5, 2.7, 2.8, 36, -23.7, -24, -23.6, 36, 29.4, 3.6, 14]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-13.2, 13.9, 0.1, 19.6, 25.9, 5.7, -21, -21, 6.6]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-10.1, 3.1, 8.7, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-19.3]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-7, -8.9]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5988, "lowFrequency": 5930}, "maxPsd": -32.3}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5988}, "maxPsd": -32.2}, {"frequencyRange": {"highFrequency": 6010, "lowFrequency": 5990}, "maxPsd": 2.9}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 6010}, "maxPsd": 3}, {"frequencyRange": {"highFrequency": 6047, "lowFrequency": 6020}, "maxPsd": 5}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6047}, "maxPsd": 5.1}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6078}, "maxPsd": 16.9}, {"frequencyRange": {"highFrequency": 6153, "lowFrequency": 6107}, "maxPsd": -19}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6153}, "maxPsd": -18.9}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": 0.5}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6213}, "maxPsd": 5.8}, {"frequencyRange": {"highFrequency": 6243, "lowFrequency": 6242}, "maxPsd": 17.5}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6243}, "maxPsd": 17.6}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6271}, "maxPsd": 6.8}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6302}, "maxPsd": 20.2}, {"frequencyRange": {"highFrequency": 6418, "lowFrequency": 6359}, "maxPsd": -13.3}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6418}, "maxPsd": -13.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6620, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6620}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": 14}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6667, "lowFrequency": 6660}, "maxPsd": 1}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6667}, "maxPsd": 1.1}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": 21.9}, {"frequencyRange": {"highFrequency": 6757, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6763, "lowFrequency": 6757}, "maxPsd": 15}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6763}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 19.6}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": -12.4}, {"frequencyRange": {"highFrequency": 6813, "lowFrequency": 6810}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6813}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": -2}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 22}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP7", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','c11e6e1b58e355cc608e0705a2334bd3ea5fce277500c09f0575aa69dd5afdee'); +INSERT INTO test_data VALUES('AFCS.FSP.8','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [4.8, 4.8, 4.8, 16.2, 26.3, 26.3, 30, 30.1, 9.7, 9.7, 9.8, 9.8, 14.6, 14.6, 35.1, 35.2, 24, 24.1, 33.3, 31.3, 17.9, 17.9, 18, 24.2, 36, 36, 36, 24.9, -1.3, -24.9, -27, -27, -5.4, 36, 35.1, 24.4, 35, 11, -3.6, -3.6, 30.4]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [7.8, 7.8, 29.3, 33.1, 12.7, 12.8, 17.6, 36, 27.1, 34.3, 20.9, 21, 36, -0.2, -24, -24, 0.7, 27.4, -0.6, -0.6]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [10.8, 32.3, 15.8, 20.7, 30.1, 24, -21, -21, 2.4]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [13.9, 18.8, 26.9, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [16.1]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [17, 5.7]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5930}, "maxPsd": 3.1}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5959}, "maxPsd": -8.2}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5990}, "maxPsd": 3.1}, {"frequencyRange": {"highFrequency": 6047, "lowFrequency": 6020}, "maxPsd": 13.2}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6047}, "maxPsd": 13.3}, {"frequencyRange": {"highFrequency": 6088, "lowFrequency": 6050}, "maxPsd": 17}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6088}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6153, "lowFrequency": 6107}, "maxPsd": -3.3}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6153}, "maxPsd": -3.2}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": 1.6}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6213}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6243, "lowFrequency": 6241}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6243}, "maxPsd": 22.2}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6271}, "maxPsd": 11}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6300}, "maxPsd": 11.1}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6302}, "maxPsd": 20.3}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6330}, "maxPsd": 18.3}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6359}, "maxPsd": 12.2}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6360}, "maxPsd": 4.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": 11.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6597, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6603, "lowFrequency": 6597}, "maxPsd": 11.9}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6603}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6622, "lowFrequency": 6610}, "maxPsd": 20.6}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6622}, "maxPsd": 20.7}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": -37.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 1.9}, {"frequencyRange": {"highFrequency": 6653, "lowFrequency": 6650}, "maxPsd": 17.5}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6653}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": 21.9}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6757, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6763, "lowFrequency": 6757}, "maxPsd": 11.4}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6763}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": -1.6}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": -2}, {"frequencyRange": {"highFrequency": 6813, "lowFrequency": 6810}, "maxPsd": 18.6}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6813}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": -16.6}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 22.2}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP8", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','f0f56376d6282260aa2c2c65423f9cd118bd3968c26bd61277c5fbbbbbc9a508'); +INSERT INTO test_data VALUES('AFCS.FSP.9','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-22.9, -22.9, -22.9, 11.3, 18, 18.1, 29.9, 11.3, -10.3, -10.3, -10.3, -10.3, 12.8, 12.8, 12.9, 30.6, 19.8, 19.8, 33.2, 33.2, -0.4, -0.4, -0.3, -0.3, 36, 36, 36, 36, -27, -27, -27, -27, 35.1, 36, 34.9, 28, 34.5, 0.6, 0.6, 11, 35]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-19.9, -19.8, 17.3, 13.4, -7.3, -7.3, 15.8, 15.9, 22.8, 28.5, 2.6, 2.7, 36, -24, -24, -24, 36, 29.4, 3.6, 14]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-16.9, 10.3, -4.3, 18.9, 25.9, 5.7, -21, -21, 6.6]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-13.8, -1.2, 8.6, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-23]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-10.7, -15]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5951, "lowFrequency": 5930}, "maxPsd": -36}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5951}, "maxPsd": -35.9}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5990}, "maxPsd": 2.9}, {"frequencyRange": {"highFrequency": 6047, "lowFrequency": 6020}, "maxPsd": 5}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6047}, "maxPsd": 5.1}, {"frequencyRange": {"highFrequency": 6058, "lowFrequency": 6050}, "maxPsd": 17.2}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6058}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6078}, "maxPsd": 16.9}, {"frequencyRange": {"highFrequency": 6119, "lowFrequency": 6107}, "maxPsd": -23.4}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6119}, "maxPsd": -23.3}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6221, "lowFrequency": 6182}, "maxPsd": -0.2}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6221}, "maxPsd": -0.1}, {"frequencyRange": {"highFrequency": 6243, "lowFrequency": 6242}, "maxPsd": 17.5}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6243}, "maxPsd": 17.6}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6271}, "maxPsd": 6.8}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6302}, "maxPsd": 20.2}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6359}, "maxPsd": -13.4}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": -13.3}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6620, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6620}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": 14.1}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": 1.1}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": 21.9}, {"frequencyRange": {"highFrequency": 6757, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6763, "lowFrequency": 6757}, "maxPsd": 15}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6763}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 14.7}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": -12.4}, {"frequencyRange": {"highFrequency": 6813, "lowFrequency": 6810}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6813}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": -2}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 22}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP9", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','c345ad131f6f9ad50c6f491efcbbc6a5a939ca6620d3297e4abe5cbffc023f3d'); +INSERT INTO test_data VALUES('AFCS.FSP.10','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [35.8, 35.9, 35.9, 34.8, 30.7, 8.2, 8.3, 32.9, 31.8, 31.8, 31.9, 30.1, 30.2, 26.8, 1.9, 1.9, 2, 30.4, 32.2, 27.1, 27.1, 27.2, 27.2, 27.2, 36, 33.2, 36, 36, 35.4, 35.4, 35.5, 36, 22.5, -1.1, -7.8, -7.8, -7.8, 26.3, 15.3, -8.2, -8.2]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 11.2, 11.3, 34.8, 33.1, 29.8, 4.9, 5, 30.1, 30.2, 30.2, 36, 36, 36, 23.6, -4.8, -4.8, 16.4, -5.2]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 14.3, 36, 7.9, 8, 33.2, 34.3, -1.8, -1.7]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [17.2, 10.9, 11.1, 1.1]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [13.8, 14]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5969, "lowFrequency": 5959}, "maxPsd": 22.8}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5969}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": 21.8}, {"frequencyRange": {"highFrequency": 6069, "lowFrequency": 6048}, "maxPsd": -4.8}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6069}, "maxPsd": -4.7}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6148, "lowFrequency": 6107}, "maxPsd": 18.8}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6148}, "maxPsd": 18.9}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6192, "lowFrequency": 6182}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6192}, "maxPsd": 17.2}, {"frequencyRange": {"highFrequency": 6231, "lowFrequency": 6211}, "maxPsd": 13.8}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6231}, "maxPsd": 13.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": -11.1}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6300}, "maxPsd": 17.4}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6302}, "maxPsd": 19.2}, {"frequencyRange": {"highFrequency": 6375, "lowFrequency": 6330}, "maxPsd": 14.1}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6375}, "maxPsd": 14.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6553, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6558, "lowFrequency": 6553}, "maxPsd": 20.2}, {"frequencyRange": {"highFrequency": 6620, "lowFrequency": 6558}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6647, "lowFrequency": 6620}, "maxPsd": 22.4}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6647}, "maxPsd": 22.5}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6737, "lowFrequency": 6710}, "maxPsd": -14.1}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6737}, "maxPsd": -14}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": -20.8}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6780}, "maxPsd": 20}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 20.1}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6830}, "maxPsd": -21.2}, {"frequencyRange": {"highFrequency": 6863, "lowFrequency": 6860}, "maxPsd": 17.6}, {"frequencyRange": {"highFrequency": 6870, "lowFrequency": 6863}, "maxPsd": 17.7}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6870}, "maxPsd": 22.9}], "requestId": "REQ-FSP10", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','89dfd6b18f91e6336947e0869ab59f86a734b95e40eb553c73109535bfe8ac29'); +INSERT INTO test_data VALUES('AFCS.FSP.11','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 34.1, 30.7, 8.2, 8.3, 32.9, 32, 32, 32.1, 30.1, 30.1, 27, 1.9, 1.9, 2, 30.3, 32.2, 27.3, 27.3, 27.4, 27.4, 27.4, 36, 33.3, 36, 36, 36, 36, 36, 36, 22.5, -1.1, -7.8, -7.8, -7.8, 26.3, 15.3, -8.2, -8.2]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 11.2, 11.3, 35, 33.1, 30, 4.9, 5, 30.3, 30.4, 30.4, 36, 36, 36, 23.6, -4.8, -4.8, 16.4, -5.2]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 14.3, 36, 7.9, 8, 33.4, 34.3, -1.8, -1.7]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [17.2, 10.9, 11.1, 1.1]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [13.8, 14]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": 21.1}, {"frequencyRange": {"highFrequency": 6069, "lowFrequency": 6048}, "maxPsd": -4.8}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6069}, "maxPsd": -4.7}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6107}, "maxPsd": 18.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6109}, "maxPsd": 19}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6182}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6231, "lowFrequency": 6211}, "maxPsd": 14}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6231}, "maxPsd": 14.1}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": -11.1}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6272}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6302}, "maxPsd": 19.2}, {"frequencyRange": {"highFrequency": 6375, "lowFrequency": 6330}, "maxPsd": 14.3}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6375}, "maxPsd": 14.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6553, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6558, "lowFrequency": 6553}, "maxPsd": 20.3}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6558}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6737, "lowFrequency": 6710}, "maxPsd": -14.1}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6737}, "maxPsd": -14}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": -20.8}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6795, "lowFrequency": 6780}, "maxPsd": 20.2}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6795}, "maxPsd": 20.3}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6830}, "maxPsd": -21.2}, {"frequencyRange": {"highFrequency": 6870, "lowFrequency": 6860}, "maxPsd": 17.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6870}, "maxPsd": 22.9}], "requestId": "REQ-FSP11", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','8613df83ff2dd20066df669181eee3c11bf8dce099fc6ec5ad3e58be08e047ba'); +INSERT INTO test_data VALUES('AFCS.FSP.12','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 34.1, 30.7, 8.2, 8.3, 32.9, 32, 32, 32.1, 30.1, 30.1, 27, 1.9, 1.9, 2, 30.3, 32.2, 27.3, 27.3, 27.4, 27.4, 27.4, 36, 33.3, 36, 36, 36, 36, 36, 36, 22.5, -1.1, -7.8, -7.8, -7.8, 26.3, 15.3, -8.2, -8.2]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 11.2, 11.3, 35, 33.1, 30, 4.9, 5, 30.3, 30.4, 30.4, 36, 36, 36, 23.6, -4.8, -4.8, 16.4, -5.2]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 14.3, 36, 7.9, 8, 33.4, 34.3, -1.8, -1.7]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [17.2, 10.9, 11.1, 1.1]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [13.8, 14]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": 21.1}, {"frequencyRange": {"highFrequency": 6069, "lowFrequency": 6048}, "maxPsd": -4.8}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6069}, "maxPsd": -4.7}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6107}, "maxPsd": 18.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6109}, "maxPsd": 19}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6182}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6231, "lowFrequency": 6211}, "maxPsd": 14}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6231}, "maxPsd": 14.1}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": -11.1}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6272}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6302}, "maxPsd": 19.2}, {"frequencyRange": {"highFrequency": 6375, "lowFrequency": 6330}, "maxPsd": 14.3}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6375}, "maxPsd": 14.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6553, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6558, "lowFrequency": 6553}, "maxPsd": 20.3}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6558}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6737, "lowFrequency": 6710}, "maxPsd": -14.1}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6737}, "maxPsd": -14}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": -20.8}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6795, "lowFrequency": 6780}, "maxPsd": 20.2}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6795}, "maxPsd": 20.3}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6830}, "maxPsd": -21.2}, {"frequencyRange": {"highFrequency": 6870, "lowFrequency": 6860}, "maxPsd": 17.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6870}, "maxPsd": 22.9}], "requestId": "REQ-FSP12", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','f0b184e977b178787996cb2323d4bfa948baed79aab6df016c43905060390052'); +INSERT INTO test_data VALUES('AFCS.FSP.13','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 27.2, -5.7, -22.2, -27, -27, -20, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 23.3, 0, 0, -3.3, 9.3, -2, -1.9, -27, -27, -27, -27, -27, -27, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [33.7, -11.4, -24, -24, -9.8, 36, 36, 36, 36, 36, 24.6, 3, 1, -24, -24, -24, -24, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-14, -21, -13.3, 36, 35.3, 6, -21, -21, -21]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-18, -12.9, 9, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-15, -15]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": -18.7}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6048}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6389}, "maxPsd": -13}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6527, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6530, "lowFrequency": 6527}, "maxPsd": -16.3}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6530}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6548, "lowFrequency": 6540}, "maxPsd": -3.8}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6548}, "maxPsd": -3.7}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": -15}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6700}, "maxPsd": 22.9}], "requestId": "REQ-FSP13", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','e599092b361d388a1209eeeea0a3f028cab12b49c9af76718e51717f6ef4314a'); +INSERT INTO test_data VALUES('AFCS.FSP.14','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 4.7, 4.8, 4.8, 16.7, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 17.2, 17.2, 17.3, 17.3, 22.7, 22.7, 31, -3, -2.9, -2.3, 1.9, 25.5, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 7.7, 7.8, 19.7, 36, 36, 36, 36, 36, 36, 36, 20.2, 20.3, 25.7, 0.1, 0.8, 26.7, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [10.7, 10.8, 36, 36, 36, 23.2, 3.1, 3.8, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [13.8, 36, 26.2, 6.1]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [16.9, 29.1]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6040, "lowFrequency": 6019}, "maxPsd": -8.3}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6040}, "maxPsd": -8.2}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": 3.6}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": 3.7}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6389}, "maxPsd": 4.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6527, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6530, "lowFrequency": 6527}, "maxPsd": 16.8}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6530}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": 4.3}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": 9.7}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6647, "lowFrequency": 6640}, "maxPsd": -16}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6647}, "maxPsd": -15.9}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -15.3}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": -11.1}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6700}, "maxPsd": 22.9}], "requestId": "REQ-FSP14", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','0043d93e7bfc02c2b9ddb8db8bc722b2390f9eef506618f5104ceb272c695f12'); +INSERT INTO test_data VALUES('AFCS.FSP.15','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 4.7, 4.8, 4.8, 16.7, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 17.2, 17.2, 17.3, 17.3, 22.7, 22.7, 31, -3, -2.9, -2.3, 1.9, 25.5, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 7.7, 7.8, 19.7, 36, 36, 36, 36, 36, 36, 36, 20.2, 20.3, 25.7, 0.1, 0.8, 26.7, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [10.7, 10.8, 36, 36, 36, 23.2, 3.1, 3.8, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [13.8, 36, 26.2, 6.1]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [16.9, 29.1]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6040, "lowFrequency": 6019}, "maxPsd": -8.3}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6040}, "maxPsd": -8.2}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": 3.6}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": 3.7}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6389}, "maxPsd": 4.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6527, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6530, "lowFrequency": 6527}, "maxPsd": 16.8}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6530}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": 4.3}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": 9.7}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6647, "lowFrequency": 6640}, "maxPsd": -16}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6647}, "maxPsd": -15.9}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -15.3}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": -11.1}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6700}, "maxPsd": 22.9}], "requestId": "REQ-FSP15", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','d26d4389560c4df4053d2a7f6b5d54f5e2bad18df3ab0570e6163b3adf39e566'); +INSERT INTO test_data VALUES('AFCS.FSP.16','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 32.5, -0.5, -0.5, -0.4, 10.6, 35.3, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 25.8, 2.4, 2.5, 10.3, 10.3, 18.1, 18.1, 36, 2.8, 2.9, 3, 3, 26.6, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 2.5, 2.6, 13.7, 36, 36, 36, 36, 36, 36, 27, 5.5, 13.4, 21.1, 5.9, 6, 27.7, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [5.5, 5.6, 36, 36, 36, 8.5, 8.9, 9, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [8.6, 34.3, 11.4, 11.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [11.7, 14.3]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6019}, "maxPsd": -13.5}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6050}, "maxPsd": -2.4}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6409, "lowFrequency": 6389}, "maxPsd": -10.6}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6409}, "maxPsd": -10.5}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6527, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6530, "lowFrequency": 6527}, "maxPsd": 18.2}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6530}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": -2.7}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": 5.1}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6647, "lowFrequency": 6640}, "maxPsd": -10.2}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6647}, "maxPsd": -10.1}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -10.1}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": -10}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6700}, "maxPsd": 22.9}], "requestId": "REQ-FSP16", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','c61c105e3042db5a188e20bd7c4183dc879eaf14a396202cd5ec19720e8d0bde'); +INSERT INTO test_data VALUES('AFCS.FSP.17','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 29, 29.1, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 35.5, 36, 36, 36, 36, 25.8, 25.9, 26.5, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 32, 32.1, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 28.9, 29.5, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 35.1, 36, 36, 36, 36, 31.8, 32.6, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 34.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6048, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": 16}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": 16.1}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6527, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6530, "lowFrequency": 6527}, "maxPsd": 22.4}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6530}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 12.8}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": 13.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6670}, "maxPsd": 22.9}], "requestId": "REQ-FSP17", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','43c56538aa0f035f423d6c18d07b65eaaef97c874225b703627dbcfaa7f666ca'); +INSERT INTO test_data VALUES('AFCS.FSP.18','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 29, 29.1, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 35.5, 36, 36, 36, 36, 25.8, 25.9, 26.5, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 32, 32.1, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 28.9, 29.5, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 35.1, 36, 36, 36, 36, 31.8, 32.6, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 34.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6048, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": 16}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": 16.1}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6527, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6530, "lowFrequency": 6527}, "maxPsd": 22.4}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6530}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 12.8}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": 13.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6670}, "maxPsd": 22.9}], "requestId": "REQ-FSP18", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','fb9bf76cb5fa698480df8bf1d3dac292c4e6a7db9b36041d87aeae51b7b16b6f'); +INSERT INTO test_data VALUES('AFCS.FSP.19','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 6.7, -26.2, -27, -27, -27, -27, 36, 22.3, 22.3, 22.4, 36, 36, 36, 36, 36, 28.5, 28.6, 28.6, 36, 2.8, -20.5, -20.5, -23.8, -11.2, -22.5, -22.4, -27, -27, -27, -27, -27, -27, 36, 29.2, 28.9, 26, 29.3, 29.4, 27.4]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [13.2, -24, -24, -24, -24, 25.4, 36, 36, 31.5, 31.6, 4.1, -17.5, -19.5, -24, -24, -24, -24, 31.9, 29, 30.4]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-21, -21, -21, 36, 14.8, -14.5, -21, -21, -21]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-18, -18, -11.5, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-15, -15]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": -39.2}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6048}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": 9.3}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6300}, "maxPsd": 15.5}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6330}, "maxPsd": 15.6}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6389}, "maxPsd": -33.5}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6527, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6530, "lowFrequency": 6527}, "maxPsd": -36.8}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6530}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6548, "lowFrequency": 6540}, "maxPsd": -24.3}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6548}, "maxPsd": -24.2}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": -35.5}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": 16.2}, {"frequencyRange": {"highFrequency": 6772, "lowFrequency": 6770}, "maxPsd": 15.8}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6772}, "maxPsd": 15.9}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6795, "lowFrequency": 6790}, "maxPsd": 12.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6795}, "maxPsd": 13}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6827, "lowFrequency": 6820}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6827}, "maxPsd": 16.4}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 14.4}, {"frequencyRange": {"highFrequency": 6872, "lowFrequency": 6860}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6872}, "maxPsd": 18.1}], "requestId": "REQ-FSP19", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','ec2210215b4e5b3ba487e000020c14168591c2ce9ce4309ca4c7884fbeb2ff93'); +INSERT INTO test_data VALUES('AFCS.FSP.20','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 17.2, -15.8, -15.7, -15.7, -3.8, 20.8, 36, 22.4, 22.4, 22.5, 36, 36, 36, 36, 36, 28.6, 28.6, 28.7, 36, 20, -3.3, -3.3, -3.2, -3.2, 2.2, 2.2, 10.5, -23.5, -23.4, -22.8, -18.6, 5, 36, 29.3, 28.9, 26, 29.4, 29.4, 27.5]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [23.6, -12.8, -12.7, -0.8, 25.4, 25.5, 36, 36, 31.6, 31.6, 21.3, -0.3, -0.2, 5.2, -20.4, -19.7, 6.2, 31.9, 29, 30.4]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-9.8, -9.7, 23.2, 36, 32, 2.7, -17.4, -16.7, 17.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-6.7, 19.1, 5.7, -14.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-3.6, 8.6]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6040, "lowFrequency": 6019}, "maxPsd": -28.8}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6040}, "maxPsd": -28.7}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": -16.9}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": -16.8}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": 9.4}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": 15.6}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6389}, "maxPsd": -16.3}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6527, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6530, "lowFrequency": 6527}, "maxPsd": -3.7}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6530}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": -16.2}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": -10.8}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6647, "lowFrequency": 6640}, "maxPsd": -36.5}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6647}, "maxPsd": -36.4}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -35.8}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": -31.6}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 15.9}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 13}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 16.4}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 14.4}, {"frequencyRange": {"highFrequency": 6872, "lowFrequency": 6860}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6872}, "maxPsd": 18.1}], "requestId": "REQ-FSP20", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','2f322ecec3dd74598740a1dfdb5ac4cba9cd43dd42f4fd6970a6afd9bb6d54c1'); +INSERT INTO test_data VALUES('AFCS.FSP.21','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 17.2, -15.8, -15.7, -15.7, -3.8, 20.8, 36, 22.4, 22.4, 22.5, 36, 36, 36, 36, 36, 28.6, 28.6, 28.7, 36, 20, -3.3, -3.3, -3.2, -3.2, 2.2, 2.2, 10.5, -23.5, -23.4, -22.8, -18.6, 5, 36, 29.3, 28.9, 26, 29.4, 29.4, 27.5]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [23.6, -12.8, -12.7, -0.8, 25.4, 25.5, 36, 36, 31.6, 31.6, 21.3, -0.3, -0.2, 5.2, -20.4, -19.7, 6.2, 31.9, 29, 30.4]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-9.8, -9.7, 23.2, 36, 32, 2.7, -17.4, -16.7, 17.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-6.7, 19.1, 5.7, -14.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-3.6, 8.6]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6040, "lowFrequency": 6019}, "maxPsd": -28.8}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6040}, "maxPsd": -28.7}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": -16.9}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": -16.8}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": 9.4}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": 15.6}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6389}, "maxPsd": -16.3}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6527, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6530, "lowFrequency": 6527}, "maxPsd": -3.7}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6530}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": -16.2}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": -10.8}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6647, "lowFrequency": 6640}, "maxPsd": -36.5}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6647}, "maxPsd": -36.4}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -35.8}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": -31.6}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 15.9}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 13}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 16.4}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 14.4}, {"frequencyRange": {"highFrequency": 6872, "lowFrequency": 6860}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6872}, "maxPsd": 18.1}], "requestId": "REQ-FSP21", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','1a42095e380054c787c6b6744d5fe901bc695869c636c0a3a6182be995b281d5'); +INSERT INTO test_data VALUES('AFCS.FSP.22','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 12, -21, -21, -20.9, -9.9, 14.8, 36, 22.3, 22.3, 22.4, 36, 36, 36, 36, 29.1, 28.6, 28.6, 28.7, 36, 5.3, -18.1, -18, -10.2, -10.2, -2.4, -2.4, 16.3, -17.7, -17.6, -17.5, -17.5, 6.1, 36, 29.3, 29, 26.2, 29.4, 29.5, 27.6]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [18.4, -18, -17.9, -6.8, 24.9, 25.3, 36, 36, 31.6, 31.6, 6.5, -15, -7.1, 0.6, -14.6, -14.5, 7.2, 31.9, 29.2, 30.5]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-15, -14.9, 18, 36, 17.3, -12, -11.6, -11.5, 18.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-11.9, 13.8, -9.1, -8.6]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-8.8, -6.2]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6019}, "maxPsd": -34}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6050}, "maxPsd": -22.9}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": 9.3}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6292, "lowFrequency": 6271}, "maxPsd": 16.1}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6292}, "maxPsd": 16.2}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": 15.6}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6409, "lowFrequency": 6389}, "maxPsd": -31.1}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6409}, "maxPsd": -31}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6527, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6530, "lowFrequency": 6527}, "maxPsd": -2.3}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6530}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": -23.2}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": -15.4}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6647, "lowFrequency": 6640}, "maxPsd": -30.7}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6647}, "maxPsd": -30.6}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -30.6}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": -30.5}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 16.7}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6720}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6777, "lowFrequency": 6770}, "maxPsd": 15.9}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6777}, "maxPsd": 16}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 13.1}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6827, "lowFrequency": 6820}, "maxPsd": 16.4}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6827}, "maxPsd": 16.5}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 14.5}, {"frequencyRange": {"highFrequency": 6872, "lowFrequency": 6860}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6872}, "maxPsd": 18.2}], "requestId": "REQ-FSP22", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','b9631bc7cf2ac0ff1d7476d10a5f19563cdebc3c825442a98e1eede57b7f7a92'); +INSERT INTO test_data VALUES('AFCS.FSP.23','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 24.8, 24.8, 8.5, 8.6, 33.2, 36, 22.4, 22.5, 22.5, 36, 36, 36, 36, 29.6, 28.5, 28.5, 28.5, 36, 36, 17.6, 17.7, 15, 23, 21.8, 21.8, 36, 5.3, 5.4, 6, 18.9, 30.7, 36, 29.2, 28.8, 25.9, 29.3, 29.3, 28.7]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 27.8, 11.5, 11.6, 25.4, 25.5, 36, 36, 31.4, 31.5, 36, 20.7, 24.8, 24.8, 8.4, 9, 33.8, 31.8, 28.9, 31.7]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [30.8, 14.6, 28.5, 36, 34.5, 23.6, 11.3, 12.1, 31.9]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [17.5, 31.6, 26.6, 14.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [20.6, 29.5]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": 11.8}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": -4.5}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": -4.4}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6156, "lowFrequency": 6137}, "maxPsd": 9.4}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6156}, "maxPsd": 9.5}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6281, "lowFrequency": 6271}, "maxPsd": 16.5}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6281}, "maxPsd": 16.6}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": 15.5}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6409, "lowFrequency": 6389}, "maxPsd": 4.6}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6409}, "maxPsd": 4.7}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6527, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6530, "lowFrequency": 6527}, "maxPsd": 1.9}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6530}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": 10}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": 8.8}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": -7.7}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -7}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": 5.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 17.7}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6720}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": 16.2}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 15.8}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 12.9}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6872, "lowFrequency": 6860}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6872}, "maxPsd": 18}], "requestId": "REQ-FSP23", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','710f9272a59b62afe4f3ed72b1a3fb195600f3712b06b15a2b95b559dbf924a3'); +INSERT INTO test_data VALUES('AFCS.FSP.24','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 24.8, 24.8, 8.5, 8.6, 33.2, 36, 22.4, 22.5, 22.5, 36, 36, 36, 36, 29.6, 28.5, 28.5, 28.5, 36, 36, 17.6, 17.7, 15, 23, 21.8, 21.8, 36, 5.3, 5.4, 6, 18.9, 30.7, 36, 29.2, 28.8, 25.9, 29.3, 29.3, 28.7]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 27.8, 11.5, 11.6, 25.4, 25.5, 36, 36, 31.4, 31.5, 36, 20.7, 24.8, 24.8, 8.4, 9, 33.8, 31.8, 28.9, 31.7]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [30.8, 14.6, 28.5, 36, 34.5, 23.6, 11.3, 12.1, 31.9]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [17.5, 31.6, 26.6, 14.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [20.6, 29.5]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": 11.8}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": -4.5}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": -4.4}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6156, "lowFrequency": 6137}, "maxPsd": 9.4}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6156}, "maxPsd": 9.5}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6281, "lowFrequency": 6271}, "maxPsd": 16.5}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6281}, "maxPsd": 16.6}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": 15.5}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6409, "lowFrequency": 6389}, "maxPsd": 4.6}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6409}, "maxPsd": 4.7}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6527, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6530, "lowFrequency": 6527}, "maxPsd": 1.9}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6530}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": 10}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": 8.8}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": -7.7}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -7}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": 5.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 17.7}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6720}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": 16.2}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 15.8}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 12.9}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6872, "lowFrequency": 6860}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6872}, "maxPsd": 18}], "requestId": "REQ-FSP24", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','a6e6af7591f2e24ee51d0cee92a3d1f95326a14758cf931b1093257a0996eabb'); +INSERT INTO test_data VALUES('AFCS.FSP.25','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-4.2, -4.2, -4.1, -4.1, 19.9, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-1.2, -1.1, 20.8, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [1.9, 22.9, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [4.9, 34, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [29.6]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [8.1, 32.5]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5999, "lowFrequency": 5959}, "maxPsd": -17.2}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5999}, "maxPsd": -17.1}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6020}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP25", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','3c8c664218fdc3fb4ffc15c7d8f2a44acf3eaae0783f15f8b2d26278a7297cc6'); +INSERT INTO test_data VALUES('AFCS.FSP.26','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [24.5, 24.5, 24.5, 24.6, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [27.5, 27.6, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [30.5, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [33.6, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5959}, "maxPsd": 11.5}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 6019}, "maxPsd": 11.6}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6020}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP26", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','b4107cfc7f48e47b4328247e93490534dedb57707fa2e5de7fd218db1d9ecd04'); +INSERT INTO test_data VALUES('AFCS.FSP.27','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [18.6, 18.7, 18.7, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [21.6, 21.7, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [24.7, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [27.8, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [30.9, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5959}, "maxPsd": 5.6}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5989}, "maxPsd": 5.7}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP27", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','00f3d0ba4404f19a92318fbe047bb2f2ce81d2b8edaaacb6006aa3b791b33e1c'); +INSERT INTO test_data VALUES('AFCS.FSP.28','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-8.5, -8.5, -8.4, -8.4, 15.6, 36, 36, 36, 36, 36, 36, 36, 36, 24.4, 24.5, 24.5, 24.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-5.5, -5.4, 16.5, 36, 36, 36, 27.4, 27.5, 27.6, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-2.4, 18.6, 36, 30.5, 30.6, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [0.6, 29.7, 33.7, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [25.3]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [3.8, 28.2]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5959}, "maxPsd": -21.5}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5990}, "maxPsd": -21.4}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6020}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6211}, "maxPsd": 11.4}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 11.5}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP28", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','d6f8386748bb746c1a9cc61ef20c466aee6025c28298ecf3771d22898cdfd76e'); +INSERT INTO test_data VALUES('AFCS.FSP.29','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [22.7, 22.7, 22.8, 22.8, 36, 36, 36, 36, 36, 36, 36, 36, 36, 24.5, 24.6, 24.6, 24.6, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [25.7, 25.8, 36, 36, 36, 36, 27.5, 27.6, 27.6, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [28.8, 36, 36, 30.6, 30.7, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [31.8, 33.5, 33.8, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [35, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5999, "lowFrequency": 5959}, "maxPsd": 9.7}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5999}, "maxPsd": 9.8}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6020}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6211}, "maxPsd": 11.5}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 11.6}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP29", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','f2fe70be6070b83e8e211575d154af7a86b7fa632b133ae4fd714a9bf26ddba4'); +INSERT INTO test_data VALUES('AFCS.FSP.30','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-1.7, -1.7, -1.7, 32.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 18.1, 18.1, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [1.3, 1.3, 36, 36, 36, 36, 21.1, 21.1, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [4.3, 31.5, 36, 24.1, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [7.4, 27.1, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [32.1]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [10.5, 30.2]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5959}, "maxPsd": -14.7}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6211}, "maxPsd": 5.1}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6242}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP30", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','065238ad7ce006b966a18a1eb6d39e39946904aca0baffb280c889f3381985cf'); +INSERT INTO test_data VALUES('AFCS.FSP.31','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-23.4, -23.3, -23.3, -23.3, 0.8, 31.7, 36, 36, 36, 36, 36, 24.2, 24.3, 22.2, 22.2, 22.3, 22.3, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 29.3, 29.4, 33.4, 33.4, 36, 36, 36, 36, 28.3, 28.3, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-20.3, -20.3, 1.7, 36, 36, 27.2, 25.2, 25.3, 25.4, 36, 36, 36, 36, 32.3, 32.4, 36, 36, 31.3, 31.4, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-17.3, 3.7, 30.2, 28.2, 28.4, 36, 35.4, 36, 34.3]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-14.2, 14.8, 31.5, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [10.4]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-11.1, 13.4]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5959}, "maxPsd": -36.4}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5989}, "maxPsd": -36.3}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6020}, "maxPsd": 18.7}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6050}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6182}, "maxPsd": 11.2}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6211}, "maxPsd": 9.2}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6242}, "maxPsd": 9.3}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6622, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6628, "lowFrequency": 6622}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6662, "lowFrequency": 6628}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6668, "lowFrequency": 6662}, "maxPsd": 20.4}, {"frequencyRange": {"highFrequency": 6782, "lowFrequency": 6668}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6788, "lowFrequency": 6782}, "maxPsd": 15.3}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6788}, "maxPsd": 22.9}], "requestId": "REQ-FSP31", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','6ef7cc5535f66b3a08bccd74fb5dcec4de71b2c82397f87634ec23c47c7abf92'); +INSERT INTO test_data VALUES('AFCS.FSP.32','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [4, 4, 4, 4.1, 28.1, 31.5, 36, 36, 36, 36, 36, 36, 36, 36, 29.5, 29.5, 29.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 28.1, 28.1, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [7, 7.1, 29, 36, 36, 36, 36, 32.5, 32.5, 36, 36, 36, 36, 36, 36, 36, 36, 31.1, 31.1, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [10, 31.1, 36, 35.5, 35.6, 36, 36, 36, 34.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [13.1, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [16.2, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5959}, "maxPsd": -9}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 6019}, "maxPsd": -8.9}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": 18.4}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6048}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6050}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 16.5}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6782, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6788, "lowFrequency": 6782}, "maxPsd": 15.1}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6788}, "maxPsd": 22.9}], "requestId": "REQ-FSP32", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','d58180d2702bc526ce619a93fd032c4b1bac0e4004739b97e0533abd83868384'); +INSERT INTO test_data VALUES('AFCS.FSP.33','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-1.9, -1.8, -1.8, 32.4, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [1.1, 1.2, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [4.2, 31.3, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [7.3, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [31.9]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [10.4, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5959}, "maxPsd": -14.9}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5989}, "maxPsd": -14.8}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP33", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','46d51f0fdfd3128f8f0872fdb7543d4d5852a545717101847b49380786e204bb'); +INSERT INTO test_data VALUES('AFCS.FSP.34','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-27, -27, -27, -27, -4.9, 31.7, 31, 31, 31, 36, 36, 24.2, 24.2, 3.9, 4, 4, 4, 33, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 29.4, 29.4, 33.4, 33.4, 36, 36, 36, 36, 28.3, 28.3, 27.6, 27.6, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-24, -24, -4, 34, 34.1, 27.2, 6.9, 7, 7.1, 36, 36, 36, 36, 32.4, 32.4, 36, 36, 31.3, 30.6, 30.6]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-21, -1.9, 30.2, 10, 10.1, 36, 35.4, 36, 33.6]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-18, 9.2, 13.2, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [4.8]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-15, 7.7]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5959}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6020}, "maxPsd": 18.7}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6078}, "maxPsd": 18}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6109}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6182}, "maxPsd": 11.2}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6211}, "maxPsd": -9.1}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": -9}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 19.9}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6300}, "maxPsd": 20}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6302}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6622, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6628, "lowFrequency": 6622}, "maxPsd": 16.4}, {"frequencyRange": {"highFrequency": 6662, "lowFrequency": 6628}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6668, "lowFrequency": 6662}, "maxPsd": 20.4}, {"frequencyRange": {"highFrequency": 6782, "lowFrequency": 6668}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6788, "lowFrequency": 6782}, "maxPsd": 15.3}, {"frequencyRange": {"highFrequency": 6822, "lowFrequency": 6788}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6828, "lowFrequency": 6822}, "maxPsd": 14.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6828}, "maxPsd": 22.9}], "requestId": "REQ-FSP34", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','cf2516cff6f0985e763d090a11f4487a48a348a47ab9cbb82617bbc616048d4b'); +INSERT INTO test_data VALUES('AFCS.FSP.35','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [2.2, 2.2, 2.3, 2.3, 26.3, 31.5, 31, 31, 31.1, 36, 36, 24.3, 24.3, 4, 4.1, 4.1, 4.1, 33, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 29.5, 29.5, 33.5, 33.6, 36, 36, 36, 36, 28.1, 28.1, 27.6, 27.6, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [5.2, 5.3, 27.2, 34, 34.1, 27.3, 7, 7.1, 7.1, 36, 36, 36, 36, 32.5, 32.5, 36, 36, 31.1, 30.6, 30.7]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [8.3, 29.3, 30.3, 10.1, 10.2, 36, 35.5, 36, 33.6]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [11.3, 13, 13.3, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [14.5, 16.2]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5999, "lowFrequency": 5959}, "maxPsd": -10.8}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5999}, "maxPsd": -10.7}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": 18.4}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6048}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6078}, "maxPsd": 18}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6107}, "maxPsd": 18.1}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6109}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6182}, "maxPsd": 11.3}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6211}, "maxPsd": -9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": -8.9}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 19.9}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6300}, "maxPsd": 20}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6302}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6622, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6628, "lowFrequency": 6622}, "maxPsd": 16.5}, {"frequencyRange": {"highFrequency": 6662, "lowFrequency": 6628}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6668, "lowFrequency": 6662}, "maxPsd": 20.5}, {"frequencyRange": {"highFrequency": 6782, "lowFrequency": 6668}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6788, "lowFrequency": 6782}, "maxPsd": 15.1}, {"frequencyRange": {"highFrequency": 6822, "lowFrequency": 6788}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6828, "lowFrequency": 6822}, "maxPsd": 14.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6828}, "maxPsd": 22.9}], "requestId": "REQ-FSP35", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','d8ddf00b83b8a16580009cdc85145587229ab46cd58dd34b44068592fb2eec7d'); +INSERT INTO test_data VALUES('AFCS.FSP.36','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-22.2, -22.2, -22.2, 12, 36, 36, 36, 36, 36, 36, 36, 36, 22.8, -2.4, -2.4, 19.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 35, 35, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-19.2, -19.2, 18, 36, 36, 32.7, 0.6, 0.6, 30.6, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-16.2, 11, 28.8, 3.6, 28.3, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-13.1, 6.6, 29, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [11.6]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-10, 9.7]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5959}, "maxPsd": -35.2}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6211}, "maxPsd": -15.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6242}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6592, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6598, "lowFrequency": 6592}, "maxPsd": 22}, {"frequencyRange": {"highFrequency": 6612, "lowFrequency": 6598}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6618, "lowFrequency": 6612}, "maxPsd": 22}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6618}, "maxPsd": 22.9}], "requestId": "REQ-FSP36", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','8caa7f75cb1e3646d06ec7f69b4005adba03a81ad228d0aa257a7a407ae6bcd9'); +INSERT INTO test_data VALUES('AFCS.FSP.37','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [12.9, 13, 13, 36, 36, 15.1, 9.6, 9.6, 9.6, 19.4, 19.4, 16.5, 16.6, -3.3, -3.3, 4, -9.1, -9.1, 6.3, -12.5, -13.8, -13.8, -13.7, -3.6, 36, 33.4, 36, 16, 16.1, 13.6, 13.6, 18.8, 25.8, -2.5, -2.5, 1.6, 7.9, -2.6, -2.8, -3.1, -4.5]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [16, 16, 18.1, 12.6, 12.6, 19.5, -0.3, -0.3, -6.1, -9.5, -10.8, -10.7, 36, 19.1, 16.6, 21.9, 0.5, 4.6, 0.2, -1.5]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [19, 15.6, 15.7, 2.7, -6.5, -7.7, 19.6, 3.5, 3.2]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [18.5, 5.7, -4.8, 6.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [31.8]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [8.6, -1.9]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5930}, "maxPsd": 18.8}, {"frequencyRange": {"highFrequency": 5974, "lowFrequency": 5959}, "maxPsd": -0.1}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5974}, "maxPsd": 0}, {"frequencyRange": {"highFrequency": 6000, "lowFrequency": 5990}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6000}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": 2.1}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6078}, "maxPsd": -3.4}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6109}, "maxPsd": 22.7}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": 6.4}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6192, "lowFrequency": 6182}, "maxPsd": 3.5}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6192}, "maxPsd": 3.6}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6211}, "maxPsd": -16.3}, {"frequencyRange": {"highFrequency": 6252, "lowFrequency": 6242}, "maxPsd": 3.8}, {"frequencyRange": {"highFrequency": 6261, "lowFrequency": 6252}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6261}, "maxPsd": -9}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6271}, "maxPsd": -22.1}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6302}, "maxPsd": -6.7}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6330}, "maxPsd": -25.5}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6360}, "maxPsd": -26.8}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6389}, "maxPsd": -26.7}, {"frequencyRange": {"highFrequency": 6400, "lowFrequency": 6391}, "maxPsd": -18}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6400}, "maxPsd": -16.6}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6555, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6558, "lowFrequency": 6555}, "maxPsd": 20.4}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6558}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6600}, "maxPsd": 3}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6610}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": 8.5}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 0.6}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 1}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6670}, "maxPsd": 5.8}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6680}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 12.8}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 0.7}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": -15.5}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6747, "lowFrequency": 6740}, "maxPsd": -11.5}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6747}, "maxPsd": -11.4}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6750}, "maxPsd": 16.5}, {"frequencyRange": {"highFrequency": 6767, "lowFrequency": 6760}, "maxPsd": -5.2}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6767}, "maxPsd": -5.1}, {"frequencyRange": {"highFrequency": 6772, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6778, "lowFrequency": 6772}, "maxPsd": -3.9}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6778}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": 17.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": -15.6}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": -9.2}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": -15.8}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": -15.5}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6840}, "maxPsd": -16.1}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": -15.7}, {"frequencyRange": {"highFrequency": 6870, "lowFrequency": 6860}, "maxPsd": -17.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6870}, "maxPsd": 22.9}], "requestId": "REQ-FSP37", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','61bb246519d47af7271c365d5a03ff05767074e4f62ba4557137fa1f475f0c67'); +INSERT INTO test_data VALUES('AFCS.FSP.38','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [29.3, 29.3, 29.4, 34.4, 34.4, 34.5, 34.5, 34.6, 34.6, 34.8, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 30.7, 21.3, 21.4, 32.2, 36, 34, 34.1, 36, 33.1, 33.2, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [32.3, 32.4, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 24.3, 24.4, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [35.4, 36, 36, 36, 36, 36, 27.3, 27.5, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 30.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5959}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5989}, "maxPsd": 16.4}, {"frequencyRange": {"highFrequency": 6010, "lowFrequency": 5990}, "maxPsd": 21.3}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6010}, "maxPsd": 21.4}, {"frequencyRange": {"highFrequency": 6098, "lowFrequency": 6048}, "maxPsd": 21.5}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6098}, "maxPsd": 21.6}, {"frequencyRange": {"highFrequency": 6129, "lowFrequency": 6109}, "maxPsd": 21.7}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6129}, "maxPsd": 21.8}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6139}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6647, "lowFrequency": 6640}, "maxPsd": 17.7}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6647}, "maxPsd": 17.8}, {"frequencyRange": {"highFrequency": 6668, "lowFrequency": 6660}, "maxPsd": 8.3}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6668}, "maxPsd": 8.4}, {"frequencyRange": {"highFrequency": 6678, "lowFrequency": 6670}, "maxPsd": 19.1}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6678}, "maxPsd": 19.2}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": 21}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6802, "lowFrequency": 6800}, "maxPsd": 20.1}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6802}, "maxPsd": 20.2}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6810}, "maxPsd": 22.9}], "requestId": "REQ-FSP38", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','9e69ab537df88478cf979128be13735040d19a0e662e36b15c708622acd0749f'); +INSERT INTO test_data VALUES('AFCS.FSP.39','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [32.3, 36, 25.2, 25.2, 32.5, 32.5, 23.2, 23.2, 20.6, 20.6, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 29.9, 29.9, 29.9, 36, 36, 36, 36, 36, 36, 36, 36, 35.5, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [35.4, 28.2, 35.5, 26.2, 23.6, 36, 36, 36, 36, 36, 32.9, 33, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [31.2, 29.2, 26.7, 36, 36, 35.9, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [32.1, 29.8, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [32.3]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [32.6, 33]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5930}, "maxPsd": 19.3}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5961}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5989}, "maxPsd": 12.2}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6020}, "maxPsd": 19.5}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6078}, "maxPsd": 10.2}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6108}, "maxPsd": 7.6}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6139}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6360}, "maxPsd": 16.9}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6391}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6670}, "maxPsd": 22.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6680}, "maxPsd": 22.9}], "requestId": "REQ-FSP39", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','c0f38a55039ad9c4e9999510baf77f1b7cbb4daa43678df606d1be6d2b61ac2f'); +INSERT INTO test_data VALUES('AFCS.FSP.40','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [20.3, 36, 20.4, 20.5, 28.7, 20.6, 20.6, 36, 20.7, 20.7, 20.8, 20.8, 36, 36, 36, 36, 36, 29.1, 29.1, 29.1, 26.2, 26.2, 26.3, 36, 35.1, 35.1, 32.6, 32.6, 32.7, 35.7, 36, 32.8, 32.8, 36, 27, 27, 27, 29.9, 29.9, 27.2, 27.2]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [23.4, 23.5, 23.6, 23.6, 23.7, 23.8, 36, 36, 32, 32.1, 29.2, 29.3, 35.6, 35.7, 36, 35.8, 30, 30, 32.9, 30.2]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [26.4, 26.6, 26.8, 36, 35.1, 32.3, 36, 32.9, 33.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [29.5, 29.9, 35.2, 35.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [20.3]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [32.7, 33]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5930}, "maxPsd": 7.3}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5961}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6010, "lowFrequency": 5989}, "maxPsd": 7.4}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 6010}, "maxPsd": 7.5}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6058, "lowFrequency": 6048}, "maxPsd": 7.5}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6058}, "maxPsd": 7.6}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6157, "lowFrequency": 6108}, "maxPsd": 7.7}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6157}, "maxPsd": 7.8}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": 16.1}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6360}, "maxPsd": 13.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6391}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6570, "lowFrequency": 6540}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6570}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6602, "lowFrequency": 6580}, "maxPsd": 19.6}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6602}, "maxPsd": 19.7}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6610}, "maxPsd": 22.7}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6640}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6670}, "maxPsd": 19.8}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": 14}, {"frequencyRange": {"highFrequency": 6787, "lowFrequency": 6770}, "maxPsd": 17}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6787}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6802, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6808, "lowFrequency": 6802}, "maxPsd": 16.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6808}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6830}, "maxPsd": 14.1}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6840}, "maxPsd": 14.2}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP40", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','c0cf947f2b5fedf0193ee721c05cbdf8f564f93683f54c23d06842cd46a275bb'); +INSERT INTO test_data VALUES('AFCS.FSP.41','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 21.6, 21.6, 21.7, 36, 36, 23.7, 21.1, -2.4, 21.1, 16.6, 16.7, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 24.6, 24.7, 22.2, 0.6, 19.7, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 27.7, 3.6, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 30.6, 6.7]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 33.5]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6360, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6360}, "maxPsd": 8.6}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6389}, "maxPsd": 8.7}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6391}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6550}, "maxPsd": 10.7}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6560}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6597, "lowFrequency": 6590}, "maxPsd": -15.5}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6597}, "maxPsd": -15.4}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6600}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6610}, "maxPsd": 9.8}, {"frequencyRange": {"highFrequency": 6657, "lowFrequency": 6630}, "maxPsd": 3.6}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6657}, "maxPsd": 3.7}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6660}, "maxPsd": 22.9}], "requestId": "REQ-FSP41", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','546e76b2c3778c74ee2dba9032467abed1e6ee80002750d9ac7a5f8156004d09'); +INSERT INTO test_data VALUES('AFCS.FSP.42','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP42", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','c0387f52a81d260c5c7fa8e28b0fdd08d1ccbfc8a26232db4f413ddf6bbee3f4'); +INSERT INTO test_data VALUES('AFCS.FSP.43','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-5.3, -5.3, -5.3, -5.3, 18.8, 21, 31.8, 36, 31.4, 31.4, 31.4, 31.5, 14.1, 28.1, 28.2, 28.2, 22.8, 22.8, 34, 34, 36, 36, 36, 36, 36, 36, 34.4, 34.5, 21.2, 21.3, 24.2, 26.4, 36, 32.3, 28.6, 28.6, 25.5, 25.5, 14.2, 14.2, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-2.3, -2.3, 19.7, 34.8, 34.4, 34.5, 17.1, 31.2, 25.8, 36, 36, 36, 36, 24.2, 24.3, 29.4, 31.6, 28.5, 17.2, 17.2]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [0.7, 21.8, 36, 20.2, 28.9, 36, 27.3, 32.5, 20.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [3.8, 23.1, 32, 30.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [22.4]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [6.9, 26.3]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5940, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5951, "lowFrequency": 5940}, "maxPsd": 9.4}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5951}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5959}, "maxPsd": -18.4}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5961}, "maxPsd": -18.3}, {"frequencyRange": {"highFrequency": 6040, "lowFrequency": 6020}, "maxPsd": 7.9}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6040}, "maxPsd": 8}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": 18.7}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": 18.8}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6108}, "maxPsd": 18.3}, {"frequencyRange": {"highFrequency": 6165, "lowFrequency": 6137}, "maxPsd": 18.4}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6165}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6192, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6203, "lowFrequency": 6192}, "maxPsd": 1.1}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6203}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6240, "lowFrequency": 6211}, "maxPsd": 15.1}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6240}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6271}, "maxPsd": 9.8}, {"frequencyRange": {"highFrequency": 6321, "lowFrequency": 6302}, "maxPsd": 20.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6321}, "maxPsd": 21}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6588, "lowFrequency": 6580}, "maxPsd": 21.4}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6588}, "maxPsd": 21.5}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6620, "lowFrequency": 6610}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6620}, "maxPsd": 8.2}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6643, "lowFrequency": 6640}, "maxPsd": 12.7}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6643}, "maxPsd": 12.8}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 11.2}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": 13.4}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 19.3}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6720}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": 15.6}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6772, "lowFrequency": 6770}, "maxPsd": 22}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6772}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": 12.5}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 12.5}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 10}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 1.2}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6830}, "maxPsd": 22.9}], "requestId": "REQ-FSP43", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','edb8e6f961fcc23feea9c86d4c2e19b2a30aa31c69cc4bb355891ce755bd88f6'); +INSERT INTO test_data VALUES('AFCS.FSP.44','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [12.9, 13, 13, 36, 36, 15.1, 9.6, 9.6, 9.6, 19.4, 19.4, 16.5, 16.6, -3.3, -3.3, 4, -9.1, -9.1, 6.3, -12.5, -13.8, -13.8, -13.7, -3.6, 36, 33.4, 36, 16, 16.1, 13.6, 13.6, 18.8, 25.8, -2.5, -2.5, 1.6, 7.9, -2.6, -2.8, -3.1, -4.5]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [16, 16, 18.1, 12.6, 12.6, 19.5, -0.3, -0.3, -6.1, -9.5, -10.8, -10.7, 36, 19.1, 16.6, 21.9, 0.5, 4.6, 0.2, -1.5]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [19, 15.6, 15.7, 2.7, -6.5, -7.7, 19.6, 3.5, 3.2]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [18.5, 5.7, -4.8, 6.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [31.8]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [8.6, -1.9]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5930}, "maxPsd": 18.8}, {"frequencyRange": {"highFrequency": 5974, "lowFrequency": 5959}, "maxPsd": -0.1}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5974}, "maxPsd": 0}, {"frequencyRange": {"highFrequency": 6000, "lowFrequency": 5990}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6000}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": 2.1}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6078}, "maxPsd": -3.4}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6109}, "maxPsd": 22.7}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": 6.4}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6192, "lowFrequency": 6182}, "maxPsd": 3.5}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6192}, "maxPsd": 3.6}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6211}, "maxPsd": -16.3}, {"frequencyRange": {"highFrequency": 6252, "lowFrequency": 6242}, "maxPsd": 3.8}, {"frequencyRange": {"highFrequency": 6261, "lowFrequency": 6252}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6261}, "maxPsd": -9}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6271}, "maxPsd": -22.1}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6302}, "maxPsd": -6.7}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6330}, "maxPsd": -25.5}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6360}, "maxPsd": -26.8}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6389}, "maxPsd": -26.7}, {"frequencyRange": {"highFrequency": 6400, "lowFrequency": 6391}, "maxPsd": -18}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6400}, "maxPsd": -16.6}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6555, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6558, "lowFrequency": 6555}, "maxPsd": 20.4}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6558}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6600}, "maxPsd": 3}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6610}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": 8.5}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 0.6}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 1}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6670}, "maxPsd": 5.8}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6680}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 12.8}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 0.7}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": -15.5}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6747, "lowFrequency": 6740}, "maxPsd": -11.5}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6747}, "maxPsd": -11.4}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6750}, "maxPsd": 16.5}, {"frequencyRange": {"highFrequency": 6767, "lowFrequency": 6760}, "maxPsd": -5.2}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6767}, "maxPsd": -5.1}, {"frequencyRange": {"highFrequency": 6772, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6778, "lowFrequency": 6772}, "maxPsd": -3.9}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6778}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": 17.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": -15.6}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": -9.2}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": -15.8}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": -15.5}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6840}, "maxPsd": -16.1}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": -15.7}, {"frequencyRange": {"highFrequency": 6870, "lowFrequency": 6860}, "maxPsd": -17.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6870}, "maxPsd": 22.9}], "requestId": "REQ-FSP44", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','c6c5df3a9a71e31b5e1926b2c001b121862c158e1a820bf4d6d0ed7449f862bc'); +INSERT INTO test_data VALUES('AFCS.FSP.45','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [29.3, 29.3, 29.4, 34.4, 34.4, 34.5, 34.5, 34.6, 34.6, 34.8, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 30.7, 21.3, 21.4, 32.2, 36, 34, 34.1, 36, 33.1, 33.2, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [32.3, 32.4, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 24.3, 24.4, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [35.4, 36, 36, 36, 36, 36, 27.3, 27.5, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 30.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5959}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5989}, "maxPsd": 16.4}, {"frequencyRange": {"highFrequency": 6010, "lowFrequency": 5990}, "maxPsd": 21.3}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6010}, "maxPsd": 21.4}, {"frequencyRange": {"highFrequency": 6098, "lowFrequency": 6048}, "maxPsd": 21.5}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6098}, "maxPsd": 21.6}, {"frequencyRange": {"highFrequency": 6129, "lowFrequency": 6109}, "maxPsd": 21.7}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6129}, "maxPsd": 21.8}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6139}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6647, "lowFrequency": 6640}, "maxPsd": 17.7}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6647}, "maxPsd": 17.8}, {"frequencyRange": {"highFrequency": 6668, "lowFrequency": 6660}, "maxPsd": 8.3}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6668}, "maxPsd": 8.4}, {"frequencyRange": {"highFrequency": 6678, "lowFrequency": 6670}, "maxPsd": 19.1}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6678}, "maxPsd": 19.2}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": 21}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6802, "lowFrequency": 6800}, "maxPsd": 20.1}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6802}, "maxPsd": 20.2}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6810}, "maxPsd": 22.9}], "requestId": "REQ-FSP45", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','2e873479aad1b6b719a05a030a636988899b9179acc03d84d3fbc4e847682100'); +INSERT INTO test_data VALUES('AFCS.FSP.46','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [32.3, 36, 25.2, 25.2, 32.5, 32.5, 23.2, 23.2, 20.6, 20.6, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 29.9, 29.9, 29.9, 36, 36, 36, 36, 36, 36, 36, 36, 35.5, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [35.4, 28.2, 35.5, 26.2, 23.6, 36, 36, 36, 36, 36, 32.9, 33, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [31.2, 29.2, 26.7, 36, 36, 35.9, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [32.1, 29.8, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [32.3]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [32.6, 33]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5930}, "maxPsd": 19.3}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5961}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5989}, "maxPsd": 12.2}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6020}, "maxPsd": 19.5}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6078}, "maxPsd": 10.2}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6108}, "maxPsd": 7.6}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6139}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6360}, "maxPsd": 16.9}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6391}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6670}, "maxPsd": 22.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6680}, "maxPsd": 22.9}], "requestId": "REQ-FSP46", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','aaa4675a67a822072e66a76713826fa192e8c9591cbd1cd93c06ad45d4518635'); +INSERT INTO test_data VALUES('AFCS.FSP.47','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [20.3, 36, 20.4, 20.5, 28.7, 20.6, 20.6, 36, 20.7, 20.7, 20.8, 20.8, 36, 36, 36, 36, 36, 29.1, 29.1, 29.1, 26.2, 26.2, 26.3, 36, 35.1, 35.1, 32.6, 32.6, 32.7, 35.7, 36, 32.8, 32.8, 36, 27, 27, 27, 29.9, 29.9, 27.2, 27.2]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [23.4, 23.5, 23.6, 23.6, 23.7, 23.8, 36, 36, 32, 32.1, 29.2, 29.3, 35.6, 35.7, 36, 35.8, 30, 30, 32.9, 30.2]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [26.4, 26.6, 26.8, 36, 35.1, 32.3, 36, 32.9, 33.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [29.5, 29.9, 35.2, 35.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [20.3]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [32.7, 33]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5930}, "maxPsd": 7.3}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5961}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6010, "lowFrequency": 5989}, "maxPsd": 7.4}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 6010}, "maxPsd": 7.5}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6058, "lowFrequency": 6048}, "maxPsd": 7.5}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6058}, "maxPsd": 7.6}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6157, "lowFrequency": 6108}, "maxPsd": 7.7}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6157}, "maxPsd": 7.8}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": 16.1}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6360}, "maxPsd": 13.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6391}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6570, "lowFrequency": 6540}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6570}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6602, "lowFrequency": 6580}, "maxPsd": 19.6}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6602}, "maxPsd": 19.7}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6610}, "maxPsd": 22.7}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6640}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6670}, "maxPsd": 19.8}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": 14}, {"frequencyRange": {"highFrequency": 6787, "lowFrequency": 6770}, "maxPsd": 17}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6787}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6802, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6808, "lowFrequency": 6802}, "maxPsd": 16.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6808}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6830}, "maxPsd": 14.1}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6840}, "maxPsd": 14.2}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP47", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','4ab32ff5520331ec4579db5a3688b2652b687a860f57d4c7664b3e1c6290734e'); +INSERT INTO test_data VALUES('AFCS.FSP.48','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 21.6, 21.6, 21.7, 36, 36, 23.7, 21.1, -2.4, 21.1, 16.6, 16.7, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 24.6, 24.7, 22.2, 0.6, 19.7, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 27.7, 3.6, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 30.6, 6.7]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 33.5]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6360, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6360}, "maxPsd": 8.6}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6389}, "maxPsd": 8.7}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6391}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6550}, "maxPsd": 10.7}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6560}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6597, "lowFrequency": 6590}, "maxPsd": -15.5}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6597}, "maxPsd": -15.4}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6600}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6610}, "maxPsd": 9.8}, {"frequencyRange": {"highFrequency": 6657, "lowFrequency": 6630}, "maxPsd": 3.6}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6657}, "maxPsd": 3.7}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6660}, "maxPsd": 22.9}], "requestId": "REQ-FSP48", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','2ec5bacf7e9dd4f2a0a6cce755c409ffe36b7e5dc60c5ce6ef2e51f7d5235bae'); +INSERT INTO test_data VALUES('AFCS.FSP.49','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP49", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','90a9300d35b81190c806997359bb6b3bfcdfb187b3c2788e0b04a6aadd09e126'); +INSERT INTO test_data VALUES('AFCS.FSP.50','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-5.3, -5.3, -5.3, -5.3, 18.8, 21, 31.8, 36, 31.4, 31.4, 31.4, 31.5, 14.1, 28.1, 28.2, 28.2, 22.8, 22.8, 34, 34, 36, 36, 36, 36, 36, 36, 34.4, 34.5, 21.2, 21.3, 24.2, 26.4, 36, 32.3, 28.6, 28.6, 25.5, 25.5, 14.2, 14.2, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-2.3, -2.3, 19.7, 34.8, 34.4, 34.5, 17.1, 31.2, 25.8, 36, 36, 36, 36, 24.2, 24.3, 29.4, 31.6, 28.5, 17.2, 17.2]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [0.7, 21.8, 36, 20.2, 28.9, 36, 27.3, 32.5, 20.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [3.8, 23.1, 32, 30.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [22.4]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [6.9, 26.3]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5940, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5951, "lowFrequency": 5940}, "maxPsd": 9.4}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5951}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5959}, "maxPsd": -18.4}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5961}, "maxPsd": -18.3}, {"frequencyRange": {"highFrequency": 6040, "lowFrequency": 6020}, "maxPsd": 7.9}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6040}, "maxPsd": 8}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": 18.7}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": 18.8}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6108}, "maxPsd": 18.3}, {"frequencyRange": {"highFrequency": 6165, "lowFrequency": 6137}, "maxPsd": 18.4}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6165}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6192, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6203, "lowFrequency": 6192}, "maxPsd": 1.1}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6203}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6240, "lowFrequency": 6211}, "maxPsd": 15.1}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6240}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6271}, "maxPsd": 9.8}, {"frequencyRange": {"highFrequency": 6321, "lowFrequency": 6302}, "maxPsd": 20.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6321}, "maxPsd": 21}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6588, "lowFrequency": 6580}, "maxPsd": 21.4}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6588}, "maxPsd": 21.5}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6620, "lowFrequency": 6610}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6620}, "maxPsd": 8.2}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6643, "lowFrequency": 6640}, "maxPsd": 12.7}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6643}, "maxPsd": 12.8}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 11.2}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": 13.4}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 19.3}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6720}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": 15.6}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6772, "lowFrequency": 6770}, "maxPsd": 22}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6772}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": 12.5}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 12.5}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 10}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 1.2}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6830}, "maxPsd": 22.9}], "requestId": "REQ-FSP50", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','4e5e463d86b02b81634292cc160cef39be1e6d6271e767dfdfa1150062089e17'); +INSERT INTO test_data VALUES('AFCS.FSP.51','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [21, 25, 29, 33], "globalOperatingClass": 131, "maxEirp": [17.2, 17.2, 17.2, 17.3]}, {"channelCfi": [19, 27, 35], "globalOperatingClass": 132, "maxEirp": [20.2, 20.2, 20.3]}, {"channelCfi": [23, 39], "globalOperatingClass": 133, "maxEirp": [23.2, 23.3]}, {"channelCfi": [15, 47], "globalOperatingClass": 134, "maxEirp": [26.1, 26.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [29.3, 29.5]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6058, "lowFrequency": 6048}, "maxPsd": 4.1}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6058}, "maxPsd": 4.2}], "requestId": "REQ-FSP51", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','b6f54936845a0d7de5b19b118cbc54175d5b41d9de32705b50697432fadf1b0d'); +INSERT INTO test_data VALUES('AFCS.FSP.52','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [21, 25, 29, 33], "globalOperatingClass": 131, "maxEirp": [16.3, 16.3, 16.3, 16.4]}, {"channelCfi": [19, 27, 35], "globalOperatingClass": 132, "maxEirp": [19.3, 19.3, 19.4]}, {"channelCfi": [23, 39], "globalOperatingClass": 133, "maxEirp": [22.3, 22.4]}, {"channelCfi": [15, 47], "globalOperatingClass": 134, "maxEirp": [25.3, 25.5]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [28.4, 28.7]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6048}, "maxPsd": 3.3}], "requestId": "REQ-FSP52", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','57348258aad313a570b568f300c99c7be0c0cd89cac1c79470aa3327040408c8'); +INSERT INTO test_data VALUES('AFCS.FSP.53','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [81, 85, 89], "globalOperatingClass": 131, "maxEirp": [24.5, 36, 36]}, {"channelCfi": [83, 91], "globalOperatingClass": 132, "maxEirp": [27.5, 36]}, {"channelCfi": [87], "globalOperatingClass": 133, "maxEirp": [30.6]}, {"channelCfi": [79], "globalOperatingClass": 134, "maxEirp": [28.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [31, 31.3]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6360}, "maxPsd": 11.5}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6361}, "maxPsd": 22.9}], "requestId": "REQ-FSP53", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','85d03e3ab8381e89bedf784ec828924bdfb597ba630da4a6e9de762ad480e732'); +INSERT INTO test_data VALUES('AFCS.FSP.54','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [81, 85, 89], "globalOperatingClass": 131, "maxEirp": [24, 36, 36]}, {"channelCfi": [83, 91], "globalOperatingClass": 132, "maxEirp": [27, 36]}, {"channelCfi": [87], "globalOperatingClass": 133, "maxEirp": [30]}, {"channelCfi": [79], "globalOperatingClass": 134, "maxEirp": [20.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [34.4]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [23, 23.3]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6360}, "maxPsd": 11}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6361}, "maxPsd": 22.9}], "requestId": "REQ-FSP54", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','cd9f13a9c969b7b4007a2c2a83d98c8b3f3079da17a9a5f55e18e6dc00cafb67'); +INSERT INTO test_data VALUES('AFCS.FSP.55','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [13, 17, 21, 25], "globalOperatingClass": 131, "maxEirp": [23.2, 23.2, 23.2, 23.3]}, {"channelCfi": [11, 19, 27], "globalOperatingClass": 132, "maxEirp": [26.1, 26.2, 26.3]}, {"channelCfi": [7, 23], "globalOperatingClass": 133, "maxEirp": [29.1, 29.3]}, {"channelCfi": [15], "globalOperatingClass": 134, "maxEirp": [32.2]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [35.3, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6019}, "maxPsd": 10.2}], "requestId": "REQ-FSP55", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','0d8201b86dea2225301f18011d6c38482afbbe085694f3b49d37b6a3e91d9ddc'); +INSERT INTO test_data VALUES('AFCS.FSP.56','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [13, 17, 21, 25], "globalOperatingClass": 131, "maxEirp": [22, 22, 22.1, 22.1]}, {"channelCfi": [11, 19, 27], "globalOperatingClass": 132, "maxEirp": [25, 25.1, 25.1]}, {"channelCfi": [7, 23], "globalOperatingClass": 133, "maxEirp": [28, 28.1]}, {"channelCfi": [15], "globalOperatingClass": 134, "maxEirp": [31.1]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [24.2, 24.6]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6019}, "maxPsd": 9}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6050}, "maxPsd": 9.1}], "requestId": "REQ-FSP56", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','8fff53cea940ee83c87d4a3547aa362f394808de6b8e2c7d0334520cb23f46ff'); +INSERT INTO test_data VALUES('AFCS.FSP.57','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP57", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','a1c19186ced662982693d2c32245c6422548c6a00f32e82a0f0d746305b007c7'); +INSERT INTO test_data VALUES('AFCS.FSP.58','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [21, 21, 21, 29.1, 36, 15.3, 15.3, 36, 36, 36, 35.7, -0.9, -0.9, -0.9, 8.7, 8.7, 8.7, 19.2, 19.3, 19.3, 23.4, 23.4, 23.5, 36, 36, 36, 36, 13.8, 13.8, 36, 19.6, 19.7, 20.9, 36, 36, 36, 36, 32.8, 9.3, 9.3, 14.9]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [24, 24.1, 18.3, 18.4, 36, 2.1, 2.1, 11.7, 11.8, 22.3, 26.4, 26.5, 36, 16.8, 22.6, 22.7, 36, 36, 12.3, 12.4]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [27, 21.3, 5.1, 5.2, 14.8, 29.5, 19.8, 25.7, 15.3]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [24.3, 8.1, 17.9, 22.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [11, 11.2]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5959}, "maxPsd": 8}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5990}, "maxPsd": 16}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 6019}, "maxPsd": 16.1}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6048}, "maxPsd": 2.3}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": -13.9}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6213}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": -4.3}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6305, "lowFrequency": 6300}, "maxPsd": 6.2}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6305}, "maxPsd": 6.3}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6360}, "maxPsd": 10.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6391}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6598, "lowFrequency": 6590}, "maxPsd": 0.7}, {"frequencyRange": {"highFrequency": 6620, "lowFrequency": 6598}, "maxPsd": 0.8}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6620}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6673, "lowFrequency": 6650}, "maxPsd": 6.6}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6673}, "maxPsd": 6.7}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6680}, "maxPsd": 7.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6810}, "maxPsd": -3.7}, {"frequencyRange": {"highFrequency": 6853, "lowFrequency": 6840}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6858, "lowFrequency": 6853}, "maxPsd": 1.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6858}, "maxPsd": 22.9}], "requestId": "REQ-FSP58", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','e00e4974bddfc7a835fb29fec113f104e346a05d76f0ae92f18206e2d62a18ce'); +INSERT INTO test_data VALUES('AFCS.FSP.59','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [1.2, 1.2, 1.3, 1.3, 1.3, 1.3, 1.4, 1.4, 1.4, 1.5, 24.1, -12.5, -12.5, -12.5, -12.4, -12.4, -12.4, -12.4, -12.3, -12.3, 20.7, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [4.2, 4.3, 4.3, 4.4, 4.5, -9.5, -9.5, -9.4, -9.4, -9.3, 16.3, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [7.3, 7.4, -6.6, -6.4, -6.3, 16.5, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [10.3, -3.5, -3.3, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [35]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-0.6, -0.4]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5959}, "maxPsd": -11.8}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5990}, "maxPsd": 1.1}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6019}, "maxPsd": -11.7}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6078}, "maxPsd": -11.6}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6139}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6182}, "maxPsd": -25.5}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": -25.4}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6300}, "maxPsd": -25.4}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6330}, "maxPsd": -25.3}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP59", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','fde4fdbb91a4b943469542da1c0a4ae2ba396e5109c2f93540c47a44e569df31'); +INSERT INTO test_data VALUES('AFCS.FSP.60','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-0.9, 19.9, -3, -3, -3, -3, -0.8, -0.8, -2.9, -2.9, -2.9, -16.9, -16.9, -16.9, -17.5, -17.4, -17.4, -17.4, -16.7, -16.7, -17.3, -17.3, -17.2, -17.2, 35.9, 18.1, -15.9, -15.9, -15.8, -15.8, -15.8, -15.8, 18.3, 28, 0.5, 0.5, 0.5, 0.5, 0.6, 0.6, 34.6]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [2.1, 0, 0, 2.2, 0.1, -13.9, -13.9, -14.4, -14.4, -13.7, -14.3, -14.2, -12.9, -12.8, -12.8, -12.7, 3.5, 3.5, 3.6, 3.6]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [3, 3.1, -11, -11.5, -11.3, -11.2, -9.8, -9.7, 6.5]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [6, -8.5, -8.3, -6.7]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-1]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-5.6, -5.4]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5930}, "maxPsd": -14}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5959}, "maxPsd": -13.9}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5961}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5989}, "maxPsd": -16.1}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6019}, "maxPsd": -16}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": -13.9}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6078}, "maxPsd": -13.8}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6107}, "maxPsd": -15.9}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": -29.9}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6213}, "maxPsd": 2}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6241}, "maxPsd": -30.5}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6271}, "maxPsd": -30.4}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6302}, "maxPsd": -29.8}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6330}, "maxPsd": -29.7}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6359}, "maxPsd": -30.3}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6580}, "maxPsd": -28.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6610}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": 2.3}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6640}, "maxPsd": -28.8}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6710}, "maxPsd": 15}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": -12.5}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6770}, "maxPsd": 15}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 2.2}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": -12.5}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6810}, "maxPsd": -12.4}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6830}, "maxPsd": 16.2}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6840}, "maxPsd": 22.9}], "requestId": "REQ-FSP60", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','23a72825e7a8b4d18e3c8878df1a379c0c618017650972cf2fa0f33f57dda2cd'); +INSERT INTO test_data VALUES('AFCS.FSP.61','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-1, 20, -2.9, -2.9, -2.9, -2.9, -0.9, -0.9, -2.8, -2.8, -2.7, -20.2, -20.2, -20.2, -20.2, -20.1, -20.1, -20.1, -20, -20, -20, -20, -19.9, -19.9, 35.8, 15.2, -18.8, -18.7, -18.7, -18.7, -18.7, -18.6, 15.4, 29.1, 0.4, 0.5, 0.5, 0.5, 0.5, 0.6, 34.6]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [2, 0.1, 0.1, 2.1, 0.2, -17.2, -17.2, -17.1, -17.1, -17, -17, -16.9, -15.8, -15.7, -15.7, -15.6, 3.4, 3.5, 3.5, 3.6]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [3.1, 3.2, -14.3, -14.1, -14, -13.9, -12.7, -12.6, 6.5]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [6.1, -11.2, -11, -9.6]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-1.1]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-8.3, -8.1]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5945, "lowFrequency": 5930}, "maxPsd": -14.2}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5945}, "maxPsd": -14.1}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5959}, "maxPsd": -14}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5961}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5989}, "maxPsd": -16}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6019}, "maxPsd": -15.9}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6050}, "maxPsd": -13.9}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6107}, "maxPsd": -15.8}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": -15.7}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": -33.2}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6213}, "maxPsd": 1.6}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6241}, "maxPsd": -33.2}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6271}, "maxPsd": -33.1}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": -29.1}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6300}, "maxPsd": -33.1}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6330}, "maxPsd": -33}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": -32.9}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6570, "lowFrequency": 6540}, "maxPsd": 22.8}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6570}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6580}, "maxPsd": -31.8}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6600}, "maxPsd": -31.7}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6610}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": 2.2}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6640}, "maxPsd": -31.7}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6738, "lowFrequency": 6710}, "maxPsd": 16.1}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6738}, "maxPsd": 16.2}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6740}, "maxPsd": -12.6}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": -12.5}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6770}, "maxPsd": 16.2}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 1.6}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6800}, "maxPsd": -12.5}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6830}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6840}, "maxPsd": 22.9}], "requestId": "REQ-FSP61", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','a9e0dfc01923d8367dbc49b0136e43275404f1092da02af0a59136e5c0ed6f7d'); +INSERT INTO test_data VALUES('AFCS.FSP.62','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [23.1, 23.2, 23.2, 36, 36, 23.2, 23.3, 27.6, 23.6, 17.7, 17.7, 17.8, -27, -27, -27, -27, -27, -27, -27, -27, -27, -27, -27, -27, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 34.5, 36, 36, 35.6]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [26.2, 26.2, 26.2, 26.3, 20.7, -24, -24, -24, -24, -24, -24, -24, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [29.2, 29.3, -21, -21, -21, -21, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-18, -18, -18, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [31.4]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-15, -15]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5950, "lowFrequency": 5930}, "maxPsd": 18.4}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5950}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 5969, "lowFrequency": 5959}, "maxPsd": 10.1}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5969}, "maxPsd": 10.2}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6048}, "maxPsd": 10.2}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6079}, "maxPsd": 14.6}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6108}, "maxPsd": 10.6}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": 4.7}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6211}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6242}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6300}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6788, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6788}, "maxPsd": 22.7}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 21.5}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 22.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP62", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','320a3b8a608b1aecebbc711f1b8a03657a3cea4d721a73bcd130ed83e08cc0aa'); +INSERT INTO test_data VALUES('AFCS.FSP.63','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [28.1, 36, 36, 36, 36, 36, 36, 36, 24.6, 24.6, 36, 36, 36, 27.7, 27.7, 36, 36, 27.3, 27.3, 27.3, 22.9, 22.9, 22.9, 24.6, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 29.3, 36, 36, 31]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [31.1, 36, 36, 36, 27.6, 36, 30.7, 30.7, 30.3, 30.3, 25.9, 26, 36, 36, 36, 36, 36, 36, 32.3, 34]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [34.1, 36, 30.7, 33.7, 33.3, 28.9, 36, 36, 35.3]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 33.7, 31.9, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [28.1]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 34.8]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5940, "lowFrequency": 5930}, "maxPsd": 15}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5940}, "maxPsd": 15.1}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 5961}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6108}, "maxPsd": 11.6}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6139}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6211}, "maxPsd": 14.7}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6242}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": 14.3}, {"frequencyRange": {"highFrequency": 6350, "lowFrequency": 6331}, "maxPsd": 14.8}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6350}, "maxPsd": 14.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6360}, "maxPsd": 9.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": 11.5}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6788, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6793, "lowFrequency": 6788}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6793}, "maxPsd": 18.1}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 18}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP63", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','2e8ede16a43f34c8d9ab3bbed390cf41ab20b6c18b61c23fc9e20a5146bf82bb'); +INSERT INTO test_data VALUES('AFCS.FSP.64','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP64", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','95a9009e94cfca8f40af2999b23003dbfcd7124958dffecf670079db2bbe145c'); +INSERT INTO test_data VALUES('AFCS.FSP.65','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [2.6, 2.7, 2.7, 19.9, 20, 2.7, 2.8, 7.1, 3.1, -2.8, -2.8, -2.7, -27, -27, -27, -27, -27, -27, -27, -27, -27, -27, -27, -27, 36, 33.7, 36, 29, 29, 29.6, 24.4, 35.1, 19.9, 28, 32.7, 27.9, 26.8, 14, 20.8, 29.4, 15.1]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [5.7, 5.7, 5.7, 5.8, 0.2, -24, -24, -24, -24, -24, -24, -24, 36, 32, 27.4, 22.9, 31.1, 29.8, 17, 18.1]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [8.7, 8.8, -21, -21, -21, -21, 30.3, 25.9, 20]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-18, -18, -18, 28.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [10.9]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-15, -15]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5950, "lowFrequency": 5930}, "maxPsd": -2.1}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5950}, "maxPsd": -2}, {"frequencyRange": {"highFrequency": 5969, "lowFrequency": 5959}, "maxPsd": -10.4}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5969}, "maxPsd": -10.3}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5990}, "maxPsd": 11.4}, {"frequencyRange": {"highFrequency": 6040, "lowFrequency": 6019}, "maxPsd": 6.9}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6040}, "maxPsd": 7}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6048}, "maxPsd": -10.3}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6079}, "maxPsd": -5.9}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6108}, "maxPsd": -9.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": -15.8}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6211}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6242}, "maxPsd": 12}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6271}, "maxPsd": 12.1}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 12.7}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6300}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6550}, "maxPsd": 20.7}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6560}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6600}, "maxPsd": 16}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6610}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 16.6}, {"frequencyRange": {"highFrequency": 6657, "lowFrequency": 6650}, "maxPsd": 11.3}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6657}, "maxPsd": 11.4}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6660}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6680}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": 6.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 15}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 15.4}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": 22.5}, {"frequencyRange": {"highFrequency": 6733, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6738, "lowFrequency": 6733}, "maxPsd": 19.7}, {"frequencyRange": {"highFrequency": 6748, "lowFrequency": 6738}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6748}, "maxPsd": 19.6}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6750}, "maxPsd": 14.9}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": 17.6}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6788, "lowFrequency": 6780}, "maxPsd": 13.8}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6788}, "maxPsd": 2.2}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 1}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 13.4}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 7.8}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6830}, "maxPsd": 16.4}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6840}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 2.1}, {"frequencyRange": {"highFrequency": 6870, "lowFrequency": 6860}, "maxPsd": 14.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6870}, "maxPsd": 22.9}], "requestId": "REQ-FSP65", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','16ac0b7cbeebf99b5379b30b644e1a6e218621575ceb07597909c9431fe98ab1'); +INSERT INTO test_data VALUES('AFCS.FSP.66','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [7.6, 25.3, 25.4, 20, 20, 18.5, 18.5, 22.7, 4.1, 4.1, 29.4, 36, 32.4, 7.2, 7.2, 25, 25, 6.8, 6.8, 6.8, 2.4, 2.4, 2.4, 4.1, 36, 33.7, 36, 29, 29.1, 29.6, 24.3, 36, 25.3, 28, 32.7, 28.7, 26.8, 8.8, 21.3, 30.1, 10.5]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [10.6, 22.9, 21.5, 21.6, 7.1, 29.7, 10.2, 10.2, 9.8, 9.8, 5.4, 5.5, 36, 32.1, 27.3, 28.3, 31, 29.8, 11.8, 13.5]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [13.6, 24.5, 10.2, 13.2, 12.8, 8.4, 30.3, 31.4, 14.8]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [16.7, 13.2, 11.4, 33.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [7.6]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [16.1, 14.3]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5940, "lowFrequency": 5930}, "maxPsd": -5.5}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5940}, "maxPsd": -5.4}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5961}, "maxPsd": 12.3}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5989}, "maxPsd": 12.4}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6023, "lowFrequency": 6019}, "maxPsd": 6.9}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6023}, "maxPsd": 7}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6048}, "maxPsd": 5.5}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6079}, "maxPsd": 9.7}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6107}, "maxPsd": 9.8}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6108}, "maxPsd": -8.9}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6139}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6211}, "maxPsd": -5.8}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6242}, "maxPsd": 12}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 12.4}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": -6.2}, {"frequencyRange": {"highFrequency": 6350, "lowFrequency": 6331}, "maxPsd": -5.7}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6350}, "maxPsd": -5.6}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6360}, "maxPsd": -10.6}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": -9}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6550}, "maxPsd": 20.7}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6560}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6607, "lowFrequency": 6600}, "maxPsd": 16}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6607}, "maxPsd": 16.1}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6610}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6647, "lowFrequency": 6640}, "maxPsd": 16.6}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6647}, "maxPsd": 16.7}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 11.3}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6660}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": 12.3}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 15}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 15.4}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": 22.5}, {"frequencyRange": {"highFrequency": 6733, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6738, "lowFrequency": 6733}, "maxPsd": 19.7}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6738}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6750}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": 17.6}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6788, "lowFrequency": 6780}, "maxPsd": 13.8}, {"frequencyRange": {"highFrequency": 6793, "lowFrequency": 6788}, "maxPsd": -4.2}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6793}, "maxPsd": -2.4}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 13.5}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 8.3}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6830}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6840}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": -2.5}, {"frequencyRange": {"highFrequency": 6870, "lowFrequency": 6860}, "maxPsd": 14.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6870}, "maxPsd": 22.9}], "requestId": "REQ-FSP66", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','40d9a271cb89b423c4546a4f61e6b6002ea4f2adec213f783a1b61b504a0779c'); +INSERT INTO test_data VALUES('AFCS.FSP.67','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 24.1, 24.1, 36, 36, 36, 36, 36, 36, 36, 17.6, 17.6, 17.6, 24.2, 24.2, 24.2, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 28.5, 36, 36, 31.3]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 27.1, 36, 36, 36, 20.6, 20.6, 27.2, 27.3, 36, 36, 36, 36, 36, 36, 31.5, 34.3]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 30.1, 36, 23.6, 30.2, 36, 36, 34.5]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 33.2, 26.7, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 29.6]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6108, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6108}, "maxPsd": 11.1}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6139}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": 4.6}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6360}, "maxPsd": 11.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6391}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6788, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6793, "lowFrequency": 6788}, "maxPsd": 15.5}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6793}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 18.3}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP67", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','2693db9cbddc914500a468194f8c764407cdfc4d259024c593db72a01a4746df'); +INSERT INTO test_data VALUES('AFCS.FSP.68','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [10.2, 10.3, 10.3, 19.6, 19.6, 4.2, 4.2, 8.4, 3.4, -2.7, -2.7, -2.6, 12, -13.2, -13.2, 8.7, 14.6, -20.3, -20.3, -20.3, -24.9, -24.9, -24.8, -19.2, 36, 34, 36, 29.3, 29.3, 29.9, 24.7, 35.1, 20.2, 28.4, 33, 28.2, 27.1, 14.4, 21.1, 29.7, 15.5]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [13.3, 13.3, 7.2, 7.2, 0.3, 0.4, -10.2, -10.1, -17.3, -17.3, -21.9, -21.8, 36, 32.3, 27.7, 23.2, 31.4, 30.1, 17.4, 18.5]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [16.3, 10.2, 3.3, -7.2, -14.3, -18.8, 30.7, 26.2, 20.4]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [13.2, -4.2, -15.9, 29.2]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [11.3]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-1.3, -13]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5930}, "maxPsd": -1.7}, {"frequencyRange": {"highFrequency": 5980, "lowFrequency": 5959}, "maxPsd": -2.8}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5980}, "maxPsd": -2.7}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5990}, "maxPsd": 11.4}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": 6.6}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6048}, "maxPsd": -8.8}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6079}, "maxPsd": -4.6}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6108}, "maxPsd": -9.6}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": -15.7}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6211}, "maxPsd": -26.2}, {"frequencyRange": {"highFrequency": 6261, "lowFrequency": 6242}, "maxPsd": 12.3}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6261}, "maxPsd": 12.4}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 13}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": -33.3}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6331}, "maxPsd": -28.9}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6360}, "maxPsd": -37.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6389}, "maxPsd": -37.8}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": -32.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6550}, "maxPsd": 21}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6560}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6600}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6610}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 16.9}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 11.7}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6660}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6680}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6695, "lowFrequency": 6690}, "maxPsd": 7.1}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6695}, "maxPsd": 7.2}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 15.3}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": 22.8}, {"frequencyRange": {"highFrequency": 6733, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6738, "lowFrequency": 6733}, "maxPsd": 20}, {"frequencyRange": {"highFrequency": 6748, "lowFrequency": 6738}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6748}, "maxPsd": 19.9}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6750}, "maxPsd": 15.1}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": 17.9}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6788, "lowFrequency": 6780}, "maxPsd": 14.1}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6788}, "maxPsd": 2.5}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 1.4}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 13.7}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 8.1}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6830}, "maxPsd": 16.7}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6840}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 2.5}, {"frequencyRange": {"highFrequency": 6870, "lowFrequency": 6860}, "maxPsd": 14.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6870}, "maxPsd": 22.9}], "requestId": "REQ-FSP68", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','4df77e9226e3b30a7676f6f2947b5c9b2030edb5be73c0966422d1c8e25fdc3c'); +INSERT INTO test_data VALUES('AFCS.FSP.69','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [8.1, 18.5, 18.5, 20.3, 20.3, 18.1, 18.1, 23.1, 4.1, 0.1, 0.1, 0.2, 29.4, 4.2, 4.3, 25.3, 25.4, 3.8, 3.9, 3.9, -0.6, -0.5, -0.5, 1.1, 36, 34, 36, 28.9, 28.9, 29.3, 24.7, 35.1, 25.5, 28.3, 33, 29, 27.1, 10.7, 21.2, 30.4, 11.1]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [11.2, 21.6, 21.1, 21.1, 3.1, 3.2, 7.2, 7.3, 6.8, 6.9, 2.5, 2.5, 36, 31.9, 27.7, 28.5, 31.4, 30.1, 13.7, 14.1]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [14.2, 24.1, 6.1, 10.3, 9.9, 5.5, 30.6, 31.5, 16.7]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [17.3, 9.2, 8.4, 33.7]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [8.1]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [12.1, 11.3]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5930}, "maxPsd": -4.9}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5961}, "maxPsd": 5.5}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5990}, "maxPsd": 11.4}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": 7.3}, {"frequencyRange": {"highFrequency": 6058, "lowFrequency": 6048}, "maxPsd": 5}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6058}, "maxPsd": 5.1}, {"frequencyRange": {"highFrequency": 6098, "lowFrequency": 6079}, "maxPsd": 10}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6098}, "maxPsd": 10.1}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6108}, "maxPsd": -8.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": -12.9}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6211}, "maxPsd": -8.8}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6241}, "maxPsd": -8.7}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6242}, "maxPsd": 12.3}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 12.7}, {"frequencyRange": {"highFrequency": 6310, "lowFrequency": 6300}, "maxPsd": -9.2}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6310}, "maxPsd": -9.1}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6331}, "maxPsd": -8.6}, {"frequencyRange": {"highFrequency": 6381, "lowFrequency": 6360}, "maxPsd": -13.6}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6381}, "maxPsd": -13.5}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": -11.9}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6550}, "maxPsd": 21}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6560}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6600}, "maxPsd": 15.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6610}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 11.6}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6660}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6680}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6693, "lowFrequency": 6690}, "maxPsd": 12.4}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6693}, "maxPsd": 12.5}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 15.3}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": 22.8}, {"frequencyRange": {"highFrequency": 6733, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6738, "lowFrequency": 6733}, "maxPsd": 20}, {"frequencyRange": {"highFrequency": 6748, "lowFrequency": 6738}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6748}, "maxPsd": 20}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6750}, "maxPsd": 15.9}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": 17.9}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6788, "lowFrequency": 6780}, "maxPsd": 14.1}, {"frequencyRange": {"highFrequency": 6793, "lowFrequency": 6788}, "maxPsd": -2.4}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6793}, "maxPsd": -1.7}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 13.4}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 8.2}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6830}, "maxPsd": 17.4}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6840}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": -1.9}, {"frequencyRange": {"highFrequency": 6870, "lowFrequency": 6860}, "maxPsd": 14.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6870}, "maxPsd": 22.9}], "requestId": "REQ-FSP69", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','0cbb0d38a0bd75beb33756bc2e6339a361e87716b5b07d078327bb8320ef2a01'); +INSERT INTO test_data VALUES('AFCS.FSP.70','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [8.6, 25.1, 24.8, 21.5, 21.5, 2.9, 3, 22.8, 4.9, 4.9, 8.4, 8.4, 36, 28.9, 25.1, 25.1, 15.1, -19.8, -19.8, -19.8, 5.1, 5.1, 5.2, 25.8, 36, 33.7, 36, 28.8, 28.8, 29.6, 25.3, 35.5, 22.2, 28, 32.9, 28.5, 27, 8.8, 23.5, 29.8, 11.8]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [11.6, 24.5, 5.9, 6, 7.9, 11.4, 31.9, 20.6, -16.8, -16.8, 8.1, 8.2, 36, 31.8, 28.3, 25.2, 31, 30, 11.9, 14.8]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [14.7, 9, 10.9, 13.2, -13.8, 9, 31.3, 28.2, 14.8]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [11.9, 12.6, -10.7, 31.2]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [8.6]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [13.7, -7.8]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5930}, "maxPsd": -4.4}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5961}, "maxPsd": 12.1}, {"frequencyRange": {"highFrequency": 5999, "lowFrequency": 5989}, "maxPsd": 11.7}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5999}, "maxPsd": 11.8}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": 8.5}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": -10.1}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": -10}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6079}, "maxPsd": 9.7}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6107}, "maxPsd": 9.8}, {"frequencyRange": {"highFrequency": 6113, "lowFrequency": 6108}, "maxPsd": -8.2}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6113}, "maxPsd": -8.1}, {"frequencyRange": {"highFrequency": 6147, "lowFrequency": 6139}, "maxPsd": -4.7}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6147}, "maxPsd": -4.6}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6211}, "maxPsd": 15.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 12.1}, {"frequencyRange": {"highFrequency": 6298, "lowFrequency": 6272}, "maxPsd": 12.6}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6298}, "maxPsd": 12.7}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": -32.8}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6331}, "maxPsd": 16.1}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6360}, "maxPsd": -7.9}, {"frequencyRange": {"highFrequency": 6399, "lowFrequency": 6391}, "maxPsd": 12.7}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6399}, "maxPsd": 12.8}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6550}, "maxPsd": 20.6}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6560}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6600}, "maxPsd": 15.8}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6610}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 16.6}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 12.3}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6660}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6680}, "maxPsd": 22.5}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": 9.2}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 15}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 15.6}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": 22.6}, {"frequencyRange": {"highFrequency": 6733, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6735, "lowFrequency": 6733}, "maxPsd": 19.8}, {"frequencyRange": {"highFrequency": 6738, "lowFrequency": 6735}, "maxPsd": 19.9}, {"frequencyRange": {"highFrequency": 6748, "lowFrequency": 6738}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6748}, "maxPsd": 20}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6750}, "maxPsd": 15.5}, {"frequencyRange": {"highFrequency": 6763, "lowFrequency": 6760}, "maxPsd": 17.7}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6763}, "maxPsd": 17.8}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6788, "lowFrequency": 6780}, "maxPsd": 14}, {"frequencyRange": {"highFrequency": 6793, "lowFrequency": 6788}, "maxPsd": -4.2}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6793}, "maxPsd": -1}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 13.2}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 10.5}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6830}, "maxPsd": 16.8}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6840}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": -1.2}, {"frequencyRange": {"highFrequency": 6870, "lowFrequency": 6860}, "maxPsd": 14.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6870}, "maxPsd": 22.9}], "requestId": "REQ-FSP70", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','4b25a8f9d87ad696c71b6ed219bb2f16483ac3a6005c3916aeb16b9caefee1de'); +INSERT INTO test_data VALUES('AFCS.FSP.71','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 7, 7, 7, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 27, 5.7, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 35.2, 10, 10, 36, 36, 36, 36, 36, 36, 36, 36, 36, 29.3, 8.7, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 13, 13.1, 36, 36, 36, 32, 11.8, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [15.9, 16.2, 36, 14.7]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [19.1, 19.3]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6078, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6078}, "maxPsd": -6}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6109}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6667, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6673, "lowFrequency": 6667}, "maxPsd": -7.3}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6673}, "maxPsd": 22.9}], "requestId": "REQ-FSP71", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','0325e7cd683c8e614ff2924bd01e082e4bbed500dd170170f6c38192eed2c496'); +INSERT INTO test_data VALUES('AFCS.FSP.72','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP72", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','65fe3b96dc3a287c2af658f6025f5e85e3ad86c993da2f4e3b7a8c6649218ee0'); +INSERT INTO test_data VALUES('AFCS.FSP.73','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 18.6, -13.5, -13.5, -13.5, 22.4, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 6.5, -14.8, 16.2, 36, 36, 36, 36, 36, 36, 29.3, 29.3]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 14.7, -10.5, -10.5, 27.5, 36, 36, 36, 36, 36, 36, 36, 36, 8.8, -11.8, 23.8, 36, 36, 32.3]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [24.4, -7.5, -7.4, 31.7, 36, 36, 11.5, -8.7, 29.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-4.6, -4.3, 35.4, -5.8]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-1.4, -1.2]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6078, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6078}, "maxPsd": -26.5}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6109}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6667, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6673, "lowFrequency": 6667}, "maxPsd": -27.8}, {"frequencyRange": {"highFrequency": 6827, "lowFrequency": 6673}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6827}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP73", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','f03ed816ea5cf10a0e33e16a7415b576d71f10870a34dd75ab5073b92fc89d22'); +INSERT INTO test_data VALUES('AFCS.FSP.74','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP74", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','1bb3eb5dedac3a30d07643676dd1b7460a4052518dce440956729ba1a819da83'); +INSERT INTO test_data VALUES('AFCS.FSP.75','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 24.5, -7.6, -7.6, -7.5, 28.4, 36, 35.7, 35.7, 35.7, 36, 36, 36, 33.6, 30.6, 6.7, 6.7, 29.9, 36, 36, 36, 36, 36, 34.9, 36, 36, 10.4, -10.9, 20.1, 36, 27.8, 27.9, 27.9, 34.8, 34.4, 19.1, 19.2]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 20.6, -4.6, -4.5, 33.5, 36, 36, 36, 9.7, 9.7, 36, 36, 36, 12.7, -7.9, 27.7, 30.9, 36, 22.2]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [30.3, -1.6, -1.5, 36, 12.7, 12.8, 15.4, -4.8, 33]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [1.4, 1.6, 15.7, -1.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [4.5, 4.7]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6048, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": 20.9}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6078}, "maxPsd": -20.6}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6109}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": 22.7}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6213}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6300}, "maxPsd": 20.6}, {"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6330}, "maxPsd": -6.3}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6361}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6597, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6603, "lowFrequency": 6597}, "maxPsd": 21.9}, {"frequencyRange": {"highFrequency": 6647, "lowFrequency": 6603}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6653, "lowFrequency": 6647}, "maxPsd": 21.8}, {"frequencyRange": {"highFrequency": 6667, "lowFrequency": 6653}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6673, "lowFrequency": 6667}, "maxPsd": -23.9}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6673}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6740}, "maxPsd": 14.8}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": 14.9}, {"frequencyRange": {"highFrequency": 6772, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6778, "lowFrequency": 6772}, "maxPsd": 14.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6778}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6807, "lowFrequency": 6800}, "maxPsd": 21.8}, {"frequencyRange": {"highFrequency": 6813, "lowFrequency": 6807}, "maxPsd": 21.4}, {"frequencyRange": {"highFrequency": 6827, "lowFrequency": 6813}, "maxPsd": 21.8}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6827}, "maxPsd": 6.1}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP75", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','ed71db7e8f697ed312e0d1e3584cb318412e186a420a12e2d5b328e0336390c9'); +INSERT INTO test_data VALUES('AFCS.FSP.76','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 32.9, 27.8, 27.9, 27.9, 36, 36, 34.7, 34.7, 34.8, 33.3, 33.4, 33.4, 33.3, 33.3, 19.8, 19.9, 36, 36, 36, 36, 36, 36, 33.9, 36, 36, 33.9, 22.1, 36, 36, 24.5, 24.5, 24.6, 34.5, 27.8, 6.5, 6.5]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 35.9, 30.9, 30.9, 36, 36, 36, 36, 22.8, 22.9, 36, 36, 36, 36, 25.1, 27.5, 27.6, 30.1, 9.5]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 33.9, 34, 36, 25.8, 25.9, 36, 28.1, 30.6]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 28.9, 31.1]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 31.8]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6048, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": 19.9}, {"frequencyRange": {"highFrequency": 6089, "lowFrequency": 6078}, "maxPsd": 14.8}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6089}, "maxPsd": 14.9}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6109}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": 21.7}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6213}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6241}, "maxPsd": 20.3}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6271}, "maxPsd": 20.4}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6300}, "maxPsd": 20.3}, {"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6330}, "maxPsd": 6.8}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6361}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6597, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6603, "lowFrequency": 6597}, "maxPsd": 20.9}, {"frequencyRange": {"highFrequency": 6647, "lowFrequency": 6603}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6647}, "maxPsd": 20.8}, {"frequencyRange": {"highFrequency": 6653, "lowFrequency": 6650}, "maxPsd": 20.9}, {"frequencyRange": {"highFrequency": 6667, "lowFrequency": 6653}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6667}, "maxPsd": 9}, {"frequencyRange": {"highFrequency": 6673, "lowFrequency": 6670}, "maxPsd": 9.1}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6673}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": 11.5}, {"frequencyRange": {"highFrequency": 6772, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6778, "lowFrequency": 6772}, "maxPsd": 11.6}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6778}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6807, "lowFrequency": 6800}, "maxPsd": 21.5}, {"frequencyRange": {"highFrequency": 6813, "lowFrequency": 6807}, "maxPsd": 21.1}, {"frequencyRange": {"highFrequency": 6827, "lowFrequency": 6813}, "maxPsd": 21.5}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6827}, "maxPsd": -6.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP76", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','ecfc299eebb0f4930f371a65aa2ab7f286acab951e13cdf7f32445dd3f2d9d4f'); +INSERT INTO test_data VALUES('AFCS.FSP.77','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 19.1, -17.9, -17.9, -27, -27, -27, -27, -22.7, -22.7, 14.4, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, -14.9, -24, -24, -24, -19.6, 18.9, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, -21, -21, 10.9]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 30.4, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [33.3, -8.4]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6582, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6588, "lowFrequency": 6582}, "maxPsd": -30.9}, {"frequencyRange": {"highFrequency": 6618, "lowFrequency": 6588}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6623, "lowFrequency": 6618}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6638, "lowFrequency": 6623}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6638}, "maxPsd": -17.9}, {"frequencyRange": {"highFrequency": 6652, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6658, "lowFrequency": 6652}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6702, "lowFrequency": 6658}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6708, "lowFrequency": 6702}, "maxPsd": -35.7}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6708}, "maxPsd": 22.9}], "requestId": "REQ-FSP77", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','7e4430aec5c9cb06d90d610d278dcac78b853bcb25006333ce9d909fbd58edf9'); +INSERT INTO test_data VALUES('AFCS.FSP.78','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 33, 33, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6798, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6798}, "maxPsd": 20}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6810}, "maxPsd": 22.9}], "requestId": "REQ-FSP78", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','51b868a848aab2e5e1cdc440f1ede0984b1e4ef672af4e32ab550b79bd3a4667'); +INSERT INTO test_data VALUES('AFCS.FSP.79','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [27.7, 27.7, 27.8, 27.8, 27.8, 27.9, 27.9, 27.9, 27.9, 28, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, -1.4, -27, -27, -27, -27, -27, -27, -27, -27, -6.1, 32.3, 23.1, 30.3, 24.1, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [30.7, 30.8, 30.9, 30.9, 31, 36, 36, 36, 36, 36, 36, 36, -24, -24, -24, -24, -24, -1.6, 27.1, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [33.8, 33.9, 34, 36, 36, 36, -21, -21, -9.6]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 9.9, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [27.7]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [12.8, -15]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5930}, "maxPsd": 14.7}, {"frequencyRange": {"highFrequency": 6058, "lowFrequency": 5989}, "maxPsd": 14.8}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6058}, "maxPsd": 14.9}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6137}, "maxPsd": 15}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6139}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6582, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6588, "lowFrequency": 6582}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6618, "lowFrequency": 6588}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6623, "lowFrequency": 6618}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6638, "lowFrequency": 6623}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6638}, "maxPsd": -38.4}, {"frequencyRange": {"highFrequency": 6652, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6658, "lowFrequency": 6652}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6702, "lowFrequency": 6658}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6708, "lowFrequency": 6702}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6742, "lowFrequency": 6708}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6748, "lowFrequency": 6742}, "maxPsd": 19.3}, {"frequencyRange": {"highFrequency": 6778, "lowFrequency": 6748}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6783, "lowFrequency": 6778}, "maxPsd": 10.1}, {"frequencyRange": {"highFrequency": 6798, "lowFrequency": 6783}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6798}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 6812, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6818, "lowFrequency": 6812}, "maxPsd": 11.1}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6818}, "maxPsd": 22.9}], "requestId": "REQ-FSP79", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','d56a27ed267173d25fc09b82ee767a7996ae2ef9302e3b6c39bc86ddf4d808ad'); +INSERT INTO test_data VALUES('AFCS.FSP.80','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 14.2, 14.2, 3.7, 0.2, 0.3, 28.2, 8.8, 8.8, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 17.2, 6.7, 3.3, 11.8, 11.9, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 6.3, 14.8, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 9.3]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6582, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6588, "lowFrequency": 6582}, "maxPsd": 1.2}, {"frequencyRange": {"highFrequency": 6618, "lowFrequency": 6588}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6623, "lowFrequency": 6618}, "maxPsd": -9.3}, {"frequencyRange": {"highFrequency": 6638, "lowFrequency": 6623}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6648, "lowFrequency": 6638}, "maxPsd": -12.8}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6648}, "maxPsd": -12.7}, {"frequencyRange": {"highFrequency": 6652, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6658, "lowFrequency": 6652}, "maxPsd": -10.6}, {"frequencyRange": {"highFrequency": 6702, "lowFrequency": 6658}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6708, "lowFrequency": 6702}, "maxPsd": -4.2}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6708}, "maxPsd": 22.9}], "requestId": "REQ-FSP80", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','33b7a4c310ebd6784fdc12cfdd558792a93070bfc6f0f645aaff8c1e08eb7cf1'); +INSERT INTO test_data VALUES('AFCS.FSP.81','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 12.5, 12.5, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 15.5, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 18.5]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6798, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6798}, "maxPsd": -0.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6810}, "maxPsd": 22.9}], "requestId": "REQ-FSP81", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','408f9b3cc30491d6bf0a4e5c2685fd574afd46dd9c697363c57763edcf82d4bf'); +INSERT INTO test_data VALUES('AFCS.FSP.82','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [8.8, 9.5, 8.8, 8.8, 9.6, 8.9, 8.9, 9.7, 9, 9, 22, 22.1, 29.3, 29.3, 29.4, 29.4, 29.4, 28.2, 28.2, 26.9, 26.9, 29.6, 26.7, 26.7, 34.2, 11.8, -25.2, -25.1, -21.2, -25.6, -25.5, 5, -19, -19, 13.9, 14, 4.2, 12.7, 10.2, 29.3, 7.4]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [11.8, 11.8, 11.9, 12, 12, 25.1, 32.3, 32.4, 31.2, 29.9, 29.9, 29.7, -22.2, -22.1, -22.5, -16, -16, 7.2, 13.2, 10.4]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [14.8, 14.9, 15.1, 35.4, 32.8, 32.7, -19.6, -13, 10.2]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [17.9, 18.1, 23.2, -16.5]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [8.7]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [21, 14.3]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5957, "lowFrequency": 5930}, "maxPsd": -4.3}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5957}, "maxPsd": -4.2}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5961}, "maxPsd": -3.5}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5989}, "maxPsd": -4.2}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": -3.4}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6048}, "maxPsd": -4.1}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6079}, "maxPsd": -3.3}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6108}, "maxPsd": -4}, {"frequencyRange": {"highFrequency": 6164, "lowFrequency": 6139}, "maxPsd": 9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6164}, "maxPsd": 9.1}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6213}, "maxPsd": 21.1}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 16.4}, {"frequencyRange": {"highFrequency": 6275, "lowFrequency": 6272}, "maxPsd": 21.1}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6275}, "maxPsd": 21.2}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6300}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6330}, "maxPsd": 13.9}, {"frequencyRange": {"highFrequency": 6376, "lowFrequency": 6361}, "maxPsd": 16.5}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6376}, "maxPsd": 16.6}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6389}, "maxPsd": 13.7}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": 21.2}, {"frequencyRange": {"highFrequency": 6553, "lowFrequency": 6550}, "maxPsd": 20.9}, {"frequencyRange": {"highFrequency": 6558, "lowFrequency": 6553}, "maxPsd": 17.2}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6558}, "maxPsd": 20.9}, {"frequencyRange": {"highFrequency": 6582, "lowFrequency": 6560}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6588, "lowFrequency": 6582}, "maxPsd": -38.2}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6588}, "maxPsd": 21.6}, {"frequencyRange": {"highFrequency": 6597, "lowFrequency": 6590}, "maxPsd": 17.9}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6597}, "maxPsd": 18}, {"frequencyRange": {"highFrequency": 6618, "lowFrequency": 6600}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6623, "lowFrequency": 6618}, "maxPsd": -34.2}, {"frequencyRange": {"highFrequency": 6628, "lowFrequency": 6623}, "maxPsd": 21.6}, {"frequencyRange": {"highFrequency": 6638, "lowFrequency": 6628}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6638}, "maxPsd": -38.6}, {"frequencyRange": {"highFrequency": 6652, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6658, "lowFrequency": 6652}, "maxPsd": -33.8}, {"frequencyRange": {"highFrequency": 6678, "lowFrequency": 6658}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6678}, "maxPsd": 19.9}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6680}, "maxPsd": 15.5}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": 5.4}, {"frequencyRange": {"highFrequency": 6702, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6708, "lowFrequency": 6702}, "maxPsd": -32}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6708}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": 18.4}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6742, "lowFrequency": 6740}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6748, "lowFrequency": 6742}, "maxPsd": 0.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6748}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6778, "lowFrequency": 6770}, "maxPsd": 15.3}, {"frequencyRange": {"highFrequency": 6783, "lowFrequency": 6778}, "maxPsd": -8.8}, {"frequencyRange": {"highFrequency": 6798, "lowFrequency": 6783}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6798}, "maxPsd": -0.3}, {"frequencyRange": {"highFrequency": 6812, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6818, "lowFrequency": 6812}, "maxPsd": -2.8}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6818}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6840}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6862, "lowFrequency": 6850}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6868, "lowFrequency": 6862}, "maxPsd": -5.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6868}, "maxPsd": 22.9}], "requestId": "REQ-FSP82", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','42bf168223ecd3fa2327274d8b12cdfb95d898261adcc2d46faaec37d59df2ef'); +INSERT INTO test_data VALUES('AFCS.FSP.83','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-1.8, 13.1, -1.8, -1.7, 13.2, -1.7, -1.7, 13.3, -1.6, -1.6, 22, 22, 30.9, 31, 31, 31, 31, 28.1, 28.1, 28.1, 28.1, 31.2, 26.6, 26.6, 34.2, 30.1, 10.2, 10.2, -1.3, -3.8, -3.8, 23.6, 4.4, 4.4, 29.2, 29.2, 5.8, 22.4, 22.5, 25.4, 24.1]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [1.2, 1.3, 1.3, 1.4, 1.4, 24, 34, 34, 31.1, 31.1, 31.1, 29.6, 13.2, 1.7, -0.8, 7.4, 7.4, 8.8, 25.5, 27.1]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [4.2, 4.4, 4.5, 34.6, 34.1, 32.6, 2.2, 10.4, 11.8]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [7.3, 7.5, 35.6, 5.3]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-1.9]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [10.4, 10.6]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5950, "lowFrequency": 5930}, "maxPsd": -14.9}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5950}, "maxPsd": -14.8}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5961}, "maxPsd": 0.1}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5989}, "maxPsd": -14.8}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 6019}, "maxPsd": -14.7}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": 0.2}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6048}, "maxPsd": -14.7}, {"frequencyRange": {"highFrequency": 6089, "lowFrequency": 6079}, "maxPsd": 0.2}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6089}, "maxPsd": 0.3}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6108}, "maxPsd": -14.6}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6139}, "maxPsd": 9}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": 17.9}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6213}, "maxPsd": 20.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 18}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 21}, {"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6300}, "maxPsd": 15.1}, {"frequencyRange": {"highFrequency": 6365, "lowFrequency": 6361}, "maxPsd": 18.1}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6365}, "maxPsd": 18.2}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6389}, "maxPsd": 13.6}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": 21.2}, {"frequencyRange": {"highFrequency": 6553, "lowFrequency": 6550}, "maxPsd": 20.4}, {"frequencyRange": {"highFrequency": 6558, "lowFrequency": 6553}, "maxPsd": 17}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6558}, "maxPsd": 20.4}, {"frequencyRange": {"highFrequency": 6582, "lowFrequency": 6560}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6588, "lowFrequency": 6582}, "maxPsd": -2.8}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6588}, "maxPsd": 21.4}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6590}, "maxPsd": 18.1}, {"frequencyRange": {"highFrequency": 6618, "lowFrequency": 6600}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6623, "lowFrequency": 6618}, "maxPsd": -14.3}, {"frequencyRange": {"highFrequency": 6628, "lowFrequency": 6623}, "maxPsd": 21.4}, {"frequencyRange": {"highFrequency": 6638, "lowFrequency": 6628}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6638}, "maxPsd": -16.8}, {"frequencyRange": {"highFrequency": 6652, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6658, "lowFrequency": 6652}, "maxPsd": -15.2}, {"frequencyRange": {"highFrequency": 6678, "lowFrequency": 6658}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6678}, "maxPsd": 20}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6680}, "maxPsd": 15.4}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": 5.3}, {"frequencyRange": {"highFrequency": 6702, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6708, "lowFrequency": 6702}, "maxPsd": -8.6}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6708}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6728, "lowFrequency": 6720}, "maxPsd": 18.4}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6728}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": 16.2}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6778, "lowFrequency": 6770}, "maxPsd": 16.5}, {"frequencyRange": {"highFrequency": 6783, "lowFrequency": 6778}, "maxPsd": -7.2}, {"frequencyRange": {"highFrequency": 6798, "lowFrequency": 6783}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6798}, "maxPsd": 9.4}, {"frequencyRange": {"highFrequency": 6812, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6818, "lowFrequency": 6812}, "maxPsd": 14.5}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6818}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6840}, "maxPsd": 12.4}, {"frequencyRange": {"highFrequency": 6862, "lowFrequency": 6850}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6868, "lowFrequency": 6862}, "maxPsd": 11.1}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6868}, "maxPsd": 22.9}], "requestId": "REQ-FSP83", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','7dbae14b24ed32831a6bdbc8a5424be9963208b74ee6cf14d18827fa66a852cc'); +INSERT INTO test_data VALUES('AFCS.FSP.84','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [21.5, 36, 21.6, 21.6, 24.6, 21.7, 21.7, 36, 21.8, 21.8, 24.8, 24.8, 30.7, 30.7, 33.4, 36, 33.4, 33.4, 36, 34.7, 33.5, 33.5, 26.5, 26.5, 36, 29.5, 13.9, 14, 21.5, 6.9, 6.9, 28.2, 14.1, 14.1, 36, 36, 36, 8.4, 8.5, 30.1, 30.1]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [24.6, 24.6, 24.7, 24.7, 24.8, 27.8, 33.7, 36, 36, 36, 36, 29.5, 16.9, 17, 9.9, 17.1, 17.1, 36, 11.5, 33.1]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [27.6, 27.7, 27.8, 36, 36, 32.5, 12.9, 20.1, 14.4]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [30.7, 30.9, 35.4, 16]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [21.5]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [33.8, 34]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5930}, "maxPsd": 8.5}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5961}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5989}, "maxPsd": 8.6}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": 11.6}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6048}, "maxPsd": 8.7}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6113, "lowFrequency": 6108}, "maxPsd": 8.7}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6113}, "maxPsd": 8.8}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6139}, "maxPsd": 11.8}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6192, "lowFrequency": 6182}, "maxPsd": 17.6}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6192}, "maxPsd": 17.7}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6213}, "maxPsd": 20.3}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6242}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6271}, "maxPsd": 20.4}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6302}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6330}, "maxPsd": 21.7}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6360}, "maxPsd": 20.5}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6389}, "maxPsd": 13.5}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6548, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6548}, "maxPsd": 22}, {"frequencyRange": {"highFrequency": 6553, "lowFrequency": 6550}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6558, "lowFrequency": 6553}, "maxPsd": 16.5}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6558}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6582, "lowFrequency": 6560}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6588, "lowFrequency": 6582}, "maxPsd": 0.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6588}, "maxPsd": 20.8}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6590}, "maxPsd": 18.7}, {"frequencyRange": {"highFrequency": 6618, "lowFrequency": 6600}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6623, "lowFrequency": 6618}, "maxPsd": 8.5}, {"frequencyRange": {"highFrequency": 6628, "lowFrequency": 6623}, "maxPsd": 20.9}, {"frequencyRange": {"highFrequency": 6637, "lowFrequency": 6628}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6638, "lowFrequency": 6637}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6638}, "maxPsd": -6.1}, {"frequencyRange": {"highFrequency": 6652, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6658, "lowFrequency": 6652}, "maxPsd": 8.5}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6658}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6687, "lowFrequency": 6680}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6687}, "maxPsd": 15.3}, {"frequencyRange": {"highFrequency": 6702, "lowFrequency": 6690}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6708, "lowFrequency": 6702}, "maxPsd": 1.1}, {"frequencyRange": {"highFrequency": 6798, "lowFrequency": 6708}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6798}, "maxPsd": -4.6}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6843, "lowFrequency": 6840}, "maxPsd": 17}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6843}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6850}, "maxPsd": 22.9}], "requestId": "REQ-FSP84", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','99596263e5e0e36da6425d02e93a8150c6a6ce2cca27e5c446fdb4bc3452124d'); +INSERT INTO test_data VALUES('AFCS.FSP.85','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [15.5, 16.8, 16.8, 16.8, 16.8, 16.9, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 17.4, 17.4, 36, 36, 16.2, 18.8, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [18.5, 19.8, 19.9, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 20.4, 36, 19.3, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [21.5, 22.9, 36, 36, 36, 36, 36, 23.4, 22.2]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [24.6, 36, 36, 26.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [15.4]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [27.7, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5930}, "maxPsd": 2.4}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5959}, "maxPsd": 2.5}, {"frequencyRange": {"highFrequency": 5980, "lowFrequency": 5961}, "maxPsd": 3.7}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5980}, "maxPsd": 3.8}, {"frequencyRange": {"highFrequency": 5999, "lowFrequency": 5990}, "maxPsd": 14.5}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5999}, "maxPsd": 14.6}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": 3.8}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6048}, "maxPsd": 3.9}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6050}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": 4.4}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 3.2}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 5.8}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6820}, "maxPsd": 22.9}], "requestId": "REQ-FSP85", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','45505ff1bcb8d57d2d89975d543ebf7f60b5be24cb057d78df65b95b2fb1b93a'); +INSERT INTO test_data VALUES('AFCS.FSP.86','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP86", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','5777f5ff175dd013d86b80d377a9e94f5fb9158c0cb82d28568a54a395af1f9a'); +INSERT INTO test_data VALUES('AFCS.FSP.87','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-27, -27, -27, -27, -27, -27, -8.1, 36, 36, 36, 36, 18.5, 18.5, 18.5, 23.1, 23.1, 23.1, 28.7, 28.7, 28.8, 34, 34, 34.1, 34.1, 36, 36, 36, 36, 36, 18.7, 23.3, 29.3, -7.5, -27, -27, -7.4, -12.8, -27, -27, -24, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-24, -24, -24, -13.8, 36, 21.5, 21.5, 26.1, 26.1, 31.8, 36, 36, 36, 36, 21.7, -12.6, -24, -13.6, -24, -22.8]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-21, -21, -4.2, 24.6, 29.2, 36, -3.1, -21, -21]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-18, -8.3, 32.2, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-27]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-15, -8]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 5930}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6050}, "maxPsd": 14.7}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": 5.5}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6213}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 10.1}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6359}, "maxPsd": 21}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": 5.7}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 10.3}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": 16.2}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 16.2}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6830}, "maxPsd": 22.9}], "requestId": "REQ-FSP87", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','904dc709efcf88a4f58f8ceac2a26174db7686b46b4933c3677fb1bd2c4e068a'); +INSERT INTO test_data VALUES('AFCS.FSP.88','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-5, -3.7, -3.7, -3.7, -3.7, -3.6, 26.3, 36, 36, 36, 36, 18.6, 18.7, 18.7, 23.5, 23.6, 23.6, 24.6, 24.6, 24.7, 33.5, 33.5, 33.6, 33.6, 36, 36, 36, 36, 36, 18.9, 23.9, 27.5, 30.9, -3.1, -3.1, 30.9, 19.3, -4.3, -1.7, 21.9, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-2, -0.7, -0.6, 25.7, 36, 21.6, 21.7, 26.6, 26.6, 27.7, 36, 36, 36, 36, 22, 25.8, -0.1, 20.4, -1.2, 23]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [1, 2.4, 24.6, 24.7, 29.7, 36, 24.9, 2.9, 1.7]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [4.1, 27.7, 32.7, 5.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-5.1]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [7.2, 30.8]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5930}, "maxPsd": -18.1}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5959}, "maxPsd": -18}, {"frequencyRange": {"highFrequency": 5980, "lowFrequency": 5961}, "maxPsd": -16.8}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5980}, "maxPsd": -16.7}, {"frequencyRange": {"highFrequency": 5999, "lowFrequency": 5990}, "maxPsd": -6}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5999}, "maxPsd": -5.9}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": -16.7}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6048}, "maxPsd": -16.6}, {"frequencyRange": {"highFrequency": 6069, "lowFrequency": 6050}, "maxPsd": 13.2}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6069}, "maxPsd": 13.3}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6203, "lowFrequency": 6182}, "maxPsd": 5.6}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6203}, "maxPsd": 5.7}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6213}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6252, "lowFrequency": 6241}, "maxPsd": 10.5}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6252}, "maxPsd": 10.6}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": 11.6}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6395, "lowFrequency": 6359}, "maxPsd": 20.5}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6395}, "maxPsd": 20.6}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": 5.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 10.9}, {"frequencyRange": {"highFrequency": 6663, "lowFrequency": 6660}, "maxPsd": 14.4}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6663}, "maxPsd": 14.5}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": -16.1}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": -17.3}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": -14.7}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 15.1}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6830}, "maxPsd": 22.9}], "requestId": "REQ-FSP88", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','1e54b66254ee9577db7cb3038229ed3e410e2de6021474bc0b8b5776538dcbaf'); +INSERT INTO test_data VALUES('AFCS.FSP.89','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP89", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','6c441128928e12589ce99157cf3cc76976246f40db443c145fbc41e63afc7c39'); +INSERT INTO test_data VALUES('AFCS.FSP.90','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-19.5, -19.4, -19.4, -19.4, -19.3, -19.3, 9.1, 33.8, 32.5, 23.7, 23.7, -0.1, 0, 0, 4.5, 4.5, 4.5, 10.1, 10.1, 10.2, 15.4, 15.4, 15.4, 15.4, 36, 21.6, 36, 28.7, 23.7, 0.2, 4.7, 10.6, 15.2, -18.8, -18.8, 15.2, 7.6, -15.9, -21.4, 2.2, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-16.4, -16.4, -16.3, 10, 26.7, 2.9, 3, 7.5, 7.6, 13.2, 18.4, 18.4, 24.6, 24.8, 3.2, 10, -15.8, 8.8, -18.4, 3.4]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-13.4, -13.3, 5.9, 6, 10.6, 21.4, 6.2, -12.8, -15.4]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-10.3, 9, 13.7, -9.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-16.7]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-7.2, 12.1]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5930}, "maxPsd": -29.7}, {"frequencyRange": {"highFrequency": 5979, "lowFrequency": 5959}, "maxPsd": -32.5}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5979}, "maxPsd": -32.4}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5990}, "maxPsd": -28.4}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": -32.4}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6048}, "maxPsd": -32.3}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6050}, "maxPsd": -3.9}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6107}, "maxPsd": 19.5}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": 10.7}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6192, "lowFrequency": 6182}, "maxPsd": -13.1}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6192}, "maxPsd": -13}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6213}, "maxPsd": 8.8}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": -8.5}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 8.9}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6300}, "maxPsd": -2.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6330}, "maxPsd": -2.8}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6359}, "maxPsd": 2.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6550}, "maxPsd": 8.6}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6560}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6590}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6613, "lowFrequency": 6600}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6618, "lowFrequency": 6613}, "maxPsd": 21.9}, {"frequencyRange": {"highFrequency": 6620, "lowFrequency": 6618}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6620}, "maxPsd": 19.1}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": -12.8}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": -8.3}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -2.4}, {"frequencyRange": {"highFrequency": 6678, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6683, "lowFrequency": 6678}, "maxPsd": 10.1}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6683}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 4.2}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": -31.8}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6768, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6773, "lowFrequency": 6768}, "maxPsd": 7.9}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6773}, "maxPsd": 14.7}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": -28.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": -34.4}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": -2.4}, {"frequencyRange": {"highFrequency": 6838, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6843, "lowFrequency": 6838}, "maxPsd": 11.2}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6843}, "maxPsd": 22.9}], "requestId": "REQ-FSP90", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','0eb99d0059a144d2da8ddb05996bc724f5d5d303092066994e74fe069f004d18'); +INSERT INTO test_data VALUES('AFCS.FSP.91','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-5.7, -5.6, -5.5, -5.5, -5.5, -5.5, 7.4, 32, 32.5, 24, 24.1, 0.1, 0.1, 0.1, 5, 5, 5, 5.4, 5.5, 5.5, 14.9, 14.9, 14.9, 15, 36, 28.2, 36, 28.7, 23.9, 0.4, 5.3, 8.2, 17.8, -5, -4.9, 28.2, 18.6, -4.9, -4.4, 9.2, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-2.6, -2.5, -2.5, 10.4, 27, 3.1, 3.1, 8, 8, 8.5, 17.9, 18, 31.3, 25, 3.4, 11.2, -1.9, 19.7, -1.9, 12.3]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [0.4, 0.6, 6.1, 6.2, 11.1, 21, 6.4, 1, 1.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [3.5, 9.1, 14.1, 4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-5.7]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [6.6, 12.2]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5930}, "maxPsd": -18.7}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5961}, "maxPsd": -18.6}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5990}, "maxPsd": -8.6}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6019}, "maxPsd": -18.5}, {"frequencyRange": {"highFrequency": 6069, "lowFrequency": 6050}, "maxPsd": -5.7}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6069}, "maxPsd": -5.6}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6107}, "maxPsd": 19.5}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": 11}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": -12.9}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6213}, "maxPsd": 14.7}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": -8}, {"frequencyRange": {"highFrequency": 6281, "lowFrequency": 6272}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6281}, "maxPsd": 15.3}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6300}, "maxPsd": -7.6}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6330}, "maxPsd": -7.5}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6359}, "maxPsd": 1.9}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6550}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6560}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6590}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6613, "lowFrequency": 6600}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6618, "lowFrequency": 6613}, "maxPsd": 21.9}, {"frequencyRange": {"highFrequency": 6620, "lowFrequency": 6618}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6620}, "maxPsd": 19.1}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": -12.6}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": -7.7}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -4.8}, {"frequencyRange": {"highFrequency": 6678, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6683, "lowFrequency": 6678}, "maxPsd": 10}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6683}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 4.8}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": -18}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6730}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": 15.1}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6750}, "maxPsd": 17}, {"frequencyRange": {"highFrequency": 6768, "lowFrequency": 6760}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6773, "lowFrequency": 6768}, "maxPsd": 8.2}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6773}, "maxPsd": 14.7}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": -17.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": -17.4}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": -3.8}, {"frequencyRange": {"highFrequency": 6838, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6843, "lowFrequency": 6838}, "maxPsd": 11.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6843}, "maxPsd": 22.9}], "requestId": "REQ-FSP91", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','51c6acf4cdfb6b28b409278bb88d38707b40baa1e379a0f4aa0d9d991097343a'); +INSERT INTO test_data VALUES('AFCS.FSP.92','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [30.3, 30.4, 30.4, 30.5, 30.5, 30.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 31.5, 31.5, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [33.4, 33.5, 33.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 34.5, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5969, "lowFrequency": 5959}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5969}, "maxPsd": 17.4}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6019}, "maxPsd": 17.5}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6050}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6730}, "maxPsd": 22.9}], "requestId": "REQ-FSP92", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','27744923e4c532f5157c773639d52414fc8140b4974920826c19cd4e706072b4'); +INSERT INTO test_data VALUES('AFCS.FSP.93','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP93", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','92d3010367a6ec8f530bc710f04a6bdbb4c30ec078d6e55806ac8ae3c9bc3e58'); +INSERT INTO test_data VALUES('AFCS.FSP.94','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 35.6, 35.6, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": 22.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6550}, "maxPsd": 22.9}], "requestId": "REQ-FSP94", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','9484351f9b8523f8d51154ee379e14eb3641d38adfe97e8b6fd3cdf51dd23139'); +INSERT INTO test_data VALUES('AFCS.FSP.95','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 30.2, 30.2, 36, 18.2, 18.3, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 28.9, 28.9, 36, 36, 36, 18.7, 18.7, 26.8, 26.9, 36, 36, 19.7, 19.7, 36, 36, 21.1, 21.2, 29.6, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 33.2, 21.2, 21.3, 36, 36, 36, 36, 36, 31.9, 32, 36, 21.8, 29.9, 22.7, 22.7, 24.1, 24.2, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 24.3, 36, 36, 34.8, 35, 25.6, 25.8, 27.3]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [27.2, 36, 36, 28.7]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [30.4, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5999, "lowFrequency": 5989}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5999}, "maxPsd": 17.2}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": 5.2}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": 5.3}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6330}, "maxPsd": 15.9}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6361}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": 5.7}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": 13.8}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": 6.7}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6748, "lowFrequency": 6740}, "maxPsd": 8.1}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6748}, "maxPsd": 8.2}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 16.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6780}, "maxPsd": 22.9}], "requestId": "REQ-FSP95", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','03d3f7afffc0614b0eab5287699ce7c662447258a35d038c5d87524edb772d68'); +INSERT INTO test_data VALUES('AFCS.FSP.96','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 29.4, 29.5, 36, 16.8, 16.9, 36, 36, 36, 36, 36, 36, 36, 27.6, 27.7, 27.7, 30.1, 30.1, 27.4, 27.5, 34.3, 36, 36, 15.1, 15.1, 26.1, 26.1, 36, 36, 18.3, 18.3, 36, 36, 17.3, 17.3, 26.9, 36, 32, 32, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 32.5, 19.8, 19.9, 36, 36, 36, 30.7, 30.8, 30.4, 30.5, 36, 18.2, 29.2, 21.3, 21.3, 20.2, 20.3, 35, 35.1]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [35.4, 22.9, 36, 33.6, 33.4, 33.6, 24.3, 23.2, 23.4]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [25.8, 36, 36, 26.2]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [29, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6010, "lowFrequency": 5989}, "maxPsd": 16.4}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 6010}, "maxPsd": 16.5}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": 3.8}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": 3.9}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6243, "lowFrequency": 6241}, "maxPsd": 14.6}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6243}, "maxPsd": 14.7}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6300}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6341, "lowFrequency": 6330}, "maxPsd": 14.4}, {"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6341}, "maxPsd": 14.5}, {"frequencyRange": {"highFrequency": 6370, "lowFrequency": 6361}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6376, "lowFrequency": 6370}, "maxPsd": 21.3}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6376}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": 2.1}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": 13.1}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": 5.3}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": 4.3}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 13.9}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6780}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 19}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6830}, "maxPsd": 22.9}], "requestId": "REQ-FSP96", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','998d6a6b682a5baf75c3f607a0dc8da308c2699621d17659e738e8d93aad39dc'); +INSERT INTO test_data VALUES('AFCS.FSP.97','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 30.2, 30.2, 36, 18.2, 18.3, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 28.9, 28.9, 36, 36, 36, 18.7, 18.7, 26.8, 26.9, 36, 36, 19.7, 19.7, 36, 36, 21.1, 21.2, 29.6, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 33.2, 21.2, 21.3, 36, 36, 36, 36, 36, 31.9, 32, 36, 21.8, 29.9, 22.7, 22.7, 24.1, 24.2, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 24.3, 36, 36, 34.8, 35, 25.6, 25.8, 27.3]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [27.2, 36, 36, 28.7]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [30.4, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5999, "lowFrequency": 5989}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5999}, "maxPsd": 17.2}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": 5.2}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": 5.3}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6330}, "maxPsd": 15.9}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6361}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": 5.7}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": 13.8}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6590}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": 6.7}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6748, "lowFrequency": 6740}, "maxPsd": 8.1}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6748}, "maxPsd": 8.2}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 16.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6780}, "maxPsd": 22.9}], "requestId": "REQ-FSP97", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','547d53969b4b760515d2a302d0873354e49dd22119fa44c4771593e5b3a428b0'); +INSERT INTO test_data VALUES('AFCS.FSP.98','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 33.7, 33.8, 28.4, 5.9, 5.9, 30.6, 36, 36, 36, 36, 36, 36, 36, 36, 36, 6.3, 6.4, 6.4, 34.2, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 8.9, 9, 36, 36, 36, 36, 9.3, 9.4, 35, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 11.9, 36, 36, 12.4, 35.2, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [14.9, 36, 15.4, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [18, 18.3]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5999, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6010, "lowFrequency": 5999}, "maxPsd": 20.7}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6010}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6048}, "maxPsd": -7.1}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6300}, "maxPsd": -6.7}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6330}, "maxPsd": -6.6}, {"frequencyRange": {"highFrequency": 6340, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6351, "lowFrequency": 6340}, "maxPsd": 21.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6351}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP98", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','a10239172a4f96b33a288011ff99b3f02cb408f1a89428d62773a9e2942078a2'); +INSERT INTO test_data VALUES('AFCS.FSP.99','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 12.9, 12.9, 36, 24.4, 24.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 15.9, 27.4, 27.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [18.9, 30.5, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [22, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [25.1, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5989}, "maxPsd": -0.1}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": 11.4}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6078}, "maxPsd": 11.5}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-FSP99", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','4f543fceecd9f74a6772507d8895c956e949f81951857b7239513ec280e2871f'); +INSERT INTO test_data VALUES('AFCS.FSP.100','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-2.4, -2.4, -2.4, 31.8, 36, 36, 36, 31.8, 10.2, 10.2, 10.2, 10.2, 33.3, 33.3, 33.4, 36, 36, 36, 36, 36, 20.1, 20.1, 20.2, 20.2, 36, 36, 36, 36, -8, -27, -27, -7.9, 36, 36, 36, 36, 36, 21.1, 21.1, 31.5, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [0.6, 0.7, 36, 33.9, 13.2, 13.2, 36, 36, 36, 36, 23.1, 23.2, 36, -13.1, -24, -13, 36, 36, 24.1, 34.5]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [3.6, 30.8, 16.2, 36, 36, 26.2, -21, -12.9, 27.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [6.7, 19.3, 29.1, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-2.5]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [9.8, 1.7]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5951, "lowFrequency": 5930}, "maxPsd": -15.5}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5951}, "maxPsd": -15.4}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6119, "lowFrequency": 6107}, "maxPsd": -2.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6119}, "maxPsd": -2.8}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6221, "lowFrequency": 6182}, "maxPsd": 20.3}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6221}, "maxPsd": 20.4}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6242}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6359}, "maxPsd": 7.1}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": 7.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6667, "lowFrequency": 6660}, "maxPsd": 21.5}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6667}, "maxPsd": 21.6}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 8.1}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6830}, "maxPsd": 22.9}], "requestId": "REQ-FSP1", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}, {"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [25.3, 25.3, 25.3, 36, 36, 36, 36, 36, 30.2, 30.2, 30.3, 30.3, 35.1, 35.1, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 19.2, -4.4, -18.9, -18.9, 15.1, 36, 36, 36, 36, 31.5, 16.9, 16.9, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [28.3, 28.3, 36, 36, 33.2, 33.3, 36, 36, 36, 36, 36, 36, 36, 20.3, -15.9, -15.9, 21.2, 36, 19.9, 19.9]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [31.3, 36, 36, 36, 36, 36, -12.9, -12.8, 22.9]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [34.4, 36, 36, -9.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 26.2]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5959}, "maxPsd": 12.3}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6153, "lowFrequency": 6107}, "maxPsd": 17.2}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6153}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6213}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": -17.4}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 22.4}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": -31.9}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 18.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 3.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6830}, "maxPsd": 22.9}], "requestId": "REQ-FSP2", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}, {"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-2.4, -2.4, -2.4, 31.8, 36, 36, 36, 31.8, 10.2, 10.2, 10.2, 10.2, 33.3, 33.3, 33.4, 36, 36, 36, 36, 36, 20.1, 20.1, 20.2, 20.2, 36, 36, 36, 36, -9.3, -27, -27, -9.2, 36, 36, 36, 36, 36, 21.1, 21.1, 31.5, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [0.6, 0.7, 36, 33.9, 13.2, 13.2, 36, 36, 36, 36, 23.1, 23.2, 36, -14.4, -24, -14.3, 36, 36, 24.1, 34.5]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [3.6, 30.8, 16.2, 36, 36, 26.2, -21, -14.2, 27.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [6.7, 19.3, 29.1, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-2.5]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [9.8, 0.4]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5951, "lowFrequency": 5930}, "maxPsd": -15.5}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5951}, "maxPsd": -15.4}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6119, "lowFrequency": 6107}, "maxPsd": -2.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6119}, "maxPsd": -2.8}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6221, "lowFrequency": 6182}, "maxPsd": 20.3}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6221}, "maxPsd": 20.4}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6242}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6359}, "maxPsd": 7.1}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": 7.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6660}, "maxPsd": 21.6}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 8.1}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6830}, "maxPsd": 22.9}], "requestId": "REQ-FSP3", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}, {"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 28.7, 28.8, 36, 36, 36, 36, 36, 36, 36, 22.4, 22.4, 22.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 19.4, 12.7, 12.7, 12.7, 36, 35.8, 12.3, 12.3]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 31.7, 31.8, 36, 36, 36, 25.4, 25.5, 36, 36, 36, 36, 36, 36, 36, 15.7, 15.7, 36, 15.3]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 34.8, 36, 28.4, 28.5, 36, 36, 18.7, 18.8]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 31.4, 31.6, 21.6]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [34.3, 34.5]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6048, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6069, "lowFrequency": 6048}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6069}, "maxPsd": 15.8}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 9.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6737, "lowFrequency": 6710}, "maxPsd": 6.4}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6737}, "maxPsd": 6.5}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": -0.3}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6830}, "maxPsd": -0.7}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP4", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}, {"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 28.7, 28.8, 36, 36, 36, 36, 36, 36, 36, 22.4, 22.4, 22.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 19.4, 12.7, 12.7, 12.7, 36, 35.8, 12.3, 12.3]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 31.7, 31.8, 36, 36, 36, 25.4, 25.5, 36, 36, 36, 36, 36, 36, 36, 15.7, 15.7, 36, 15.3]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 34.8, 36, 28.4, 28.5, 36, 36, 18.7, 18.8]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 31.4, 31.6, 21.6]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [34.3, 34.5]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6048, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6069, "lowFrequency": 6048}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6069}, "maxPsd": 15.8}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 9.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6737, "lowFrequency": 6710}, "maxPsd": 6.4}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6737}, "maxPsd": 6.5}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": -0.3}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6830}, "maxPsd": -0.7}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP5", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}, {"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 28.7, 28.8, 36, 36, 36, 36, 36, 36, 36, 22.4, 22.4, 22.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 19.4, 12.7, 12.7, 12.7, 36, 35.8, 12.3, 12.3]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 31.7, 31.8, 36, 36, 36, 25.4, 25.5, 36, 36, 36, 36, 36, 36, 36, 15.7, 15.7, 36, 15.3]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 34.8, 36, 28.4, 28.5, 36, 36, 18.7, 18.8]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 31.4, 31.6, 21.6]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [34.3, 34.5]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6048, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6069, "lowFrequency": 6048}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6069}, "maxPsd": 15.8}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 9.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6737, "lowFrequency": 6710}, "maxPsd": 6.4}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6737}, "maxPsd": 6.5}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": -0.3}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6830}, "maxPsd": -0.7}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-FSP6", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','6e39eac8fe82ca40f99b65464673212b02e3c5b99201b6c5ac952759c6b4ef0f'); +INSERT INTO test_data VALUES('AFCS.IBP.1','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 19, 19.1, 19.1, 19.2, 19.2, 19.3, 19.3, 19.4, 19.4, 36, 36, 36, 36, 19.5, 19.5, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 28.8, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 22, 22.1, 22.2, 22.3, 22.4, 36, 36, 22.5, 36, 36, 36, 36, 36, 31.8, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 25, 25.2, 25.4, 36, 36, 36, 34.8]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 28.1, 28.5, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [30.9, 31.3]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6183, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6183}, "maxPsd": 6}, {"frequencyRange": {"highFrequency": 6243, "lowFrequency": 6211}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6243}, "maxPsd": 6.2}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6271}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6302}, "maxPsd": 6.3}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6330}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6525}, "maxPsd": 6.5}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6550}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 15.8}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6820}, "maxPsd": 22.9}], "requestId": "REQ-IBP1", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','4355375dc1aba22df867a4cc71bdbab33b45039218f1115442db29cb9f063df5'); +INSERT INTO test_data VALUES('AFCS.IBP.2','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6525}, "maxPsd": 22.9}], "requestId": "REQ-IBP2", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','61245c8e3c4a6537012a927fc48b274ff26b640e6a932c8b95a09cf5e868beb6'); +INSERT INTO test_data VALUES('AFCS.IBP.3','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 23.8, 23.9, 23.9, 22.8, 22.8, 22.9, 24, 36, 36, 23, 23, 23, 23, 36, 36, 36, 36, 36, 36, 36, 36, 36, 23.9, 24, 24, 24, 24, 24.1, 24.1, 24.1]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 26.8, 26.9, 25.8, 25.9, 36, 26, 26, 36, 36, 36, 36, 27, 27, 27.1, 27.1]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 29.8, 28.8, 28.9, 29, 36, 29.9, 30.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 31.8, 32, 32.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [34.7, 34.9]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6182, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6193, "lowFrequency": 6182}, "maxPsd": 10.8}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6193}, "maxPsd": 10.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 9.8}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6272}, "maxPsd": 11}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6302}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6360}, "maxPsd": 10}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6710}, "maxPsd": 10.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6740}, "maxPsd": 11}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6830}, "maxPsd": 11.1}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-IBP3", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','6119999a28130da67959d54f7d4204b171d639f92e1bca2a83f5bb6a13cd1f2f'); +INSERT INTO test_data VALUES('AFCS.IBP.4','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 35.3, 35.3, 36, 36, 36, 36, 32.9, 32.9, 33, 33, 36, 36, 34.4, 34.5, 34.5, 36, 36, 36, 36, 36, 36, 29.5, 29.5, 29.6, 29.7, 36, 36, 36, 36, 35.3, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 35.9, 36, 36, 36, 36, 36, 36, 32.5, 32.7, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 35.5, 35.7, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6108, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6114, "lowFrequency": 6108}, "maxPsd": 22.2}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6114}, "maxPsd": 22.3}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6139}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6241}, "maxPsd": 19.9}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6271}, "maxPsd": 20}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6302}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6380, "lowFrequency": 6360}, "maxPsd": 21.4}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6380}, "maxPsd": 21.5}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6391}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6631, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6649, "lowFrequency": 6631}, "maxPsd": 16.5}, {"frequencyRange": {"highFrequency": 6671, "lowFrequency": 6649}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6688, "lowFrequency": 6671}, "maxPsd": 16.6}, {"frequencyRange": {"highFrequency": 6689, "lowFrequency": 6688}, "maxPsd": 16.7}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6689}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 22.2}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6800}, "maxPsd": 22.9}], "requestId": "REQ-IBP4", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','9505543821bd9cc9f27562c2282276fa003f25acf2f220c42cf928bbdbbdfbcd'); +INSERT INTO test_data VALUES('AFCS.IBP.5','{"availableSpectrumInquiryResponses": [{"requestId": "REQ-IBP5", "response": {"responseCode": 102, "shortDescription": "Missing Param", "supplementalInfo": {"missingParams": ["center"]}}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','8800644dd89303c19e92eb54aa1b7d0cdc775df117faae1b00a73de19fd0d64d'); +INSERT INTO test_data VALUES('AFCS.IBP.6','{"availableSpectrumInquiryResponses": [{"requestId": "REQ-IBP6", "response": {"responseCode": 102, "shortDescription": "Missing Param", "supplementalInfo": {"missingParams": ["center"]}}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','a270ea343148dc0ec439ce8893a0cb5ee20cb00a4aca64e15f86bf01b4ffcc45'); +INSERT INTO test_data VALUES('AFCS.IBP.7','{"availableSpectrumInquiryResponses": [{"requestId": "REQ-IBP7", "response": {"responseCode": 102, "shortDescription": "Missing Param", "supplementalInfo": {"missingParams": ["center"]}}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','62638c297085990e7688b682a6004c35a81b14221a154510d1a01dd21d3742bc'); +INSERT INTO test_data VALUES('AFCS.IBP.8','{"availableSpectrumInquiryResponses": [{"requestId": "REQ-IBP8", "response": {"responseCode": 102, "shortDescription": "Missing Param", "supplementalInfo": {"missingParams": ["center"]}}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','e1656ea5da6d30bf36bb83b04affb676f7b834fcc55fe78841f3d9768efbfd80'); +INSERT INTO test_data VALUES('AFCS.SIP.1','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 5.2, 5.3, 5.3, 5.3, 28.3, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 8.2, 8.3, 8.3, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 11.3, 11.4, 36]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [36, 36, 14.3]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 17.2]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6300, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6310, "lowFrequency": 6300}, "maxPsd": -7.8}, {"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6310}, "maxPsd": -7.7}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6361}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6676}, "maxPsd": 22.9}], "requestId": "REQ-SIP1", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','7abf887fda112bd60ea67d62f2186432c5759001e273b2c80bc54c29b1bb48a7'); +INSERT INTO test_data VALUES('AFCS.SIP.2','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [22.1, 22.1, 22.2, 22.2, 22.2, 22.3, 10.4, 10.5, 10.5, 10.5, 10.6, 10.6, 36, 25.2, 18, 18.1, 18.1, 18.1, 28.2, 28.2, 29.2, 29.2, 29.2, 29.3, 36, 36, 27.6, 27.6, 28.1, 28.2, 26.4, 26.4, 36, 36, 30.9, 23.7, 23.7, 36, 19.4]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [25.1, 25.2, 25.3, 13.5, 13.5, 13.6, 28.2, 21.1, 21.1, 31.2, 32.2, 32.2, 30.6, 30.7, 29.4, 33.9, 26.7, 22.4]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [28.2, 16.4, 16.6, 24.1, 24.2, 35.2, 29.7]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [19.4, 19.6, 27.2]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [22.1]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [22.5, 22.7]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5930}, "maxPsd": 9.1}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 5989}, "maxPsd": 9.2}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6078}, "maxPsd": -2.6}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6107}, "maxPsd": -2.5}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6109}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6166, "lowFrequency": 6137}, "maxPsd": -2.5}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6166}, "maxPsd": -2.4}, {"frequencyRange": {"highFrequency": 6211, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6211}, "maxPsd": 12.2}, {"frequencyRange": {"highFrequency": 6252, "lowFrequency": 6241}, "maxPsd": 5}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6252}, "maxPsd": 5.1}, {"frequencyRange": {"highFrequency": 6310, "lowFrequency": 6302}, "maxPsd": 15.1}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6310}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6331}, "maxPsd": 18.8}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6359}, "maxPsd": 16.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6590, "lowFrequency": 6580}, "maxPsd": 14.6}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6590}, "maxPsd": 22.7}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6600}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6632, "lowFrequency": 6610}, "maxPsd": 15.1}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6632}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6676}, "maxPsd": 17.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6680}, "maxPsd": 21.3}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 13.4}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6710}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6770}, "maxPsd": 17.9}, {"frequencyRange": {"highFrequency": 6803, "lowFrequency": 6800}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6808, "lowFrequency": 6803}, "maxPsd": 10.7}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6808}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 20.9}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6820}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 6.4}, {"frequencyRange": {"highFrequency": 6870, "lowFrequency": 6860}, "maxPsd": 15}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6870}, "maxPsd": 22.9}], "requestId": "REQ-SIP2", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','b52a1c1592ee0187c0f80e26bcfeb217f13f897ede79a029a729554e29251c34'); +INSERT INTO test_data VALUES('AFCS.SIP.3','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 25.5, 25.5, 25.6, 25.6, 25.6, 25.6, 36, 35.4, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 28.5, 28.6, 28.6, 28.7, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 31.5, 31.6, 31.7, 36, 36]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [36, 34.5, 34.8]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6182, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": 12.5}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6213}, "maxPsd": 16.9}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6241}, "maxPsd": 12.6}, {"frequencyRange": {"highFrequency": 6310, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6321, "lowFrequency": 6310}, "maxPsd": 22.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6321}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6676}, "maxPsd": 22.9}], "requestId": "REQ-SIP3", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','b8f59faa2c66edf849e7cfe435e745c0bbfb6a67df2a1981e917892bcd8a7566'); +INSERT INTO test_data VALUES('AFCS.SIP.4','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 33.7, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6108, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6119, "lowFrequency": 6108}, "maxPsd": 20.7}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6119}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6676}, "maxPsd": 22.9}], "requestId": "REQ-SIP4", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','5571a04a8882939a7657d37d275ed6df514ae0fcc0c9e9cd2f92b9eba4343292'); +INSERT INTO test_data VALUES('AFCS.SIP.5','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 20.3, 20.4, 36, 36, 36, 36, 36, 36, 36, 29.3, 29.3, 29.3, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 23.4, 36, 36, 36, 32.2, 32.3, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 26.4, 36, 35.3, 36, 36]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [36, 29.5, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [32.4, 32.7]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6108, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6108}, "maxPsd": 7.3}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6137}, "maxPsd": 7.4}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6139}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": 16.3}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6676}, "maxPsd": 22.9}], "requestId": "REQ-SIP5", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','2765a55fec1ce7955caa9e6ff4932450b438611a8dd50f575d4aa07caa21808d'); +INSERT INTO test_data VALUES('AFCS.SIP.6','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [30, 28.1, 5, 5.1, 29.1, 36, 34.3, 34.3, 30.8, 25.1, 25.2, 25.2, 36, 36, 36, 36, 36, 36, 36, 36, 35.6, 35.6, 35.6, 36, 36, 36, 36, 36, 36, 36, 36, 29.8, 29.8, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [29.5, 8.1, 30, 36, 28.1, 28.2, 36, 36, 36, 36, 36, 36, 36, 36, 32.8, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [11.1, 32.1, 31.2, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [14.1, 34.2, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [17.2, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5959}, "maxPsd": 17}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5989}, "maxPsd": -8}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 6019}, "maxPsd": -7.9}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6020}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6078}, "maxPsd": 21.3}, {"frequencyRange": {"highFrequency": 6118, "lowFrequency": 6108}, "maxPsd": 17.7}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6118}, "maxPsd": 17.8}, {"frequencyRange": {"highFrequency": 6167, "lowFrequency": 6137}, "maxPsd": 12.1}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6167}, "maxPsd": 12.2}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6360}, "maxPsd": 22.6}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6391}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6676}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": 16.8}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6730}, "maxPsd": 22.9}], "requestId": "REQ-SIP6", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','33ca9dcb27b6be6a0f58feed82a7105f99e03be671a2da837b79322100c3ff97'); +INSERT INTO test_data VALUES('AFCS.SIP.7','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [25.9, 36, 36, 35, 35, 35, 35.1, 35.1, 35.1, 19.7, 19.7, 19.7, 21.2, 21.2, 22.3, 36, 28.1, 28.1, 28.1, 28.2, 30.3, 36, 28.2, 28.3, 36, 36, 34.4, 34.4, 34.4, 29.6, 36, 36, 36, 36, 25.1, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [28.9, 36, 36, 36, 22.7, 22.7, 24.2, 25.3, 31.1, 31.1, 33.3, 31.3, 36, 36, 36, 28.1, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [31.9, 36, 25.7, 27.3, 34.1, 34.2, 31.2]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [35, 28.8, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [25.8]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [31.7, 31.9]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5930}, "maxPsd": 12.8}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5959}, "maxPsd": 12.9}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5961}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6019}, "maxPsd": 22}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6078}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6109}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": 6.7}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": 8.2}, {"frequencyRange": {"highFrequency": 6243, "lowFrequency": 6213}, "maxPsd": 9.3}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6243}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6271}, "maxPsd": 15.1}, {"frequencyRange": {"highFrequency": 6351, "lowFrequency": 6331}, "maxPsd": 17.2}, {"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6351}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6361}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6389}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6580, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6580}, "maxPsd": 21.4}, {"frequencyRange": {"highFrequency": 6647, "lowFrequency": 6610}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6647}, "maxPsd": -4.7}, {"frequencyRange": {"highFrequency": 6777, "lowFrequency": 6676}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6783, "lowFrequency": 6777}, "maxPsd": 12.1}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6783}, "maxPsd": 22.9}], "requestId": "REQ-SIP7", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','2c076d35945bab3dfc54094ba16437f76c3b2dde4ca44922eb0f91df10ec6f5e'); +INSERT INTO test_data VALUES('AFCS.SIP.8','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 23.3, 23.4, 23.4, 19.4, 19.4, 19.5, 36, 34, 36, 36, 36, 34.8, 34.9, 28.5, 28.5, 36, 18.9, 18.9, 24.7, 24.8, 36, 36, 36, 36, 36, 36, 36, 36, 25.4, 35.2, 36, 29.6, 32.8, 33.6, 33.7, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 26.3, 26.4, 22.4, 22.5, 36, 36, 36, 31.5, 21.9, 21.9, 27.8, 36, 36, 28.4, 32.6, 35.8, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [29.3, 25.4, 25.5, 36, 24.8, 25, 35.6]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [28.4, 28.6, 27.9]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [31.5, 30.8]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6019}, "maxPsd": 10.3}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6048}, "maxPsd": 10.4}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": 13.3}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6078}, "maxPsd": 6.4}, {"frequencyRange": {"highFrequency": 6147, "lowFrequency": 6109}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6158, "lowFrequency": 6147}, "maxPsd": 21}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6158}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6262, "lowFrequency": 6241}, "maxPsd": 21.8}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6262}, "maxPsd": 21.9}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6271}, "maxPsd": 15.5}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6302}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6330}, "maxPsd": 5.9}, {"frequencyRange": {"highFrequency": 6384, "lowFrequency": 6361}, "maxPsd": 11.7}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6384}, "maxPsd": 11.8}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6391}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6717, "lowFrequency": 6676}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6717}, "maxPsd": 12.4}, {"frequencyRange": {"highFrequency": 6730, "lowFrequency": 6720}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6730}, "maxPsd": 22.2}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6740}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": 16.6}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 19.8}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6800}, "maxPsd": 20.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6830}, "maxPsd": 22.9}], "requestId": "REQ-SIP8", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','bea44413f89e691dbce1f97326fec029c5f04f9c0018a366c29cd6c459e03446'); +INSERT INTO test_data VALUES('AFCS.SIP.9','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-5.7, -1.3, -5.6, -5.6, 5.3, -1.2, -5.5, -5.5, -5.5, -5.4, -5.4, -5.4, 0.2, 0.2, 0.2, 0.3, 0.3, 1.9, 2, 2, 4.6, 6.8, 4.6, 4.6, 26, 2.5, 17.8, 17.9, 2.7, 2.7, 2.4, 0.6, -0.1, -0.1, 0, 0, 0, 0, -2.5]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-2.7, -2.6, 1.8, -2.5, -2.4, -2.4, 3.2, 3.3, 3.3, 5, 7.6, 7.6, 5.5, 5.7, 2.9, 3, 3, 0.4]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [0.4, 0.5, 0.6, 6.2, 6.4, 10.6, 6]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [3.4, 3.7, 9.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-5.7]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [6.6, 6.8]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 2}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5930}, "maxPsd": -18.7}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5961}, "maxPsd": -14.3}, {"frequencyRange": {"highFrequency": 5998, "lowFrequency": 5989}, "maxPsd": -18.7}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5998}, "maxPsd": -18.6}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": -7.7}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6048}, "maxPsd": -14.2}, {"frequencyRange": {"highFrequency": 6109, "lowFrequency": 6078}, "maxPsd": -18.5}, {"frequencyRange": {"highFrequency": 6129, "lowFrequency": 6109}, "maxPsd": -4.8}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6129}, "maxPsd": -4.7}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": -18.4}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": -12.8}, {"frequencyRange": {"highFrequency": 6232, "lowFrequency": 6213}, "maxPsd": -11.2}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6232}, "maxPsd": -11.1}, {"frequencyRange": {"highFrequency": 6262, "lowFrequency": 6241}, "maxPsd": -12.8}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6262}, "maxPsd": -12.7}, {"frequencyRange": {"highFrequency": 6292, "lowFrequency": 6272}, "maxPsd": -8.6}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6292}, "maxPsd": -8.5}, {"frequencyRange": {"highFrequency": 6310, "lowFrequency": 6300}, "maxPsd": -11.1}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6310}, "maxPsd": -11}, {"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6331}, "maxPsd": -8.5}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6361}, "maxPsd": -6.2}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6389}, "maxPsd": -8.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6540, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6550, "lowFrequency": 6540}, "maxPsd": 15.4}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6550}, "maxPsd": -10.5}, {"frequencyRange": {"highFrequency": 6570, "lowFrequency": 6560}, "maxPsd": 15.4}, {"frequencyRange": {"highFrequency": 6582, "lowFrequency": 6570}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6588, "lowFrequency": 6582}, "maxPsd": 4.8}, {"frequencyRange": {"highFrequency": 6592, "lowFrequency": 6588}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6598, "lowFrequency": 6592}, "maxPsd": 5.4}, {"frequencyRange": {"highFrequency": 6602, "lowFrequency": 6598}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6608, "lowFrequency": 6602}, "maxPsd": 11.1}, {"frequencyRange": {"highFrequency": 6620, "lowFrequency": 6608}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6620}, "maxPsd": -10.3}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": 19.1}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6680, "lowFrequency": 6676}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6680}, "maxPsd": 10.4}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": -10.6}, {"frequencyRange": {"highFrequency": 6710, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6720, "lowFrequency": 6710}, "maxPsd": -12.4}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6720}, "maxPsd": 15.4}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": -13.1}, {"frequencyRange": {"highFrequency": 6752, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6758, "lowFrequency": 6752}, "maxPsd": 3.5}, {"frequencyRange": {"highFrequency": 6762, "lowFrequency": 6758}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6768, "lowFrequency": 6762}, "maxPsd": 10.4}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6768}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6780, "lowFrequency": 6770}, "maxPsd": -13.1}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6780}, "maxPsd": -3.4}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 20.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": -13}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6830}, "maxPsd": -13}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6840}, "maxPsd": 8.8}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": -15.6}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-SIP9", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','3c870f363d0087f8cb1ebd0b0fc546b9cb6e71d3fb3a3abd30809c50a019474b'); +INSERT INTO test_data VALUES('AFCS.SIP.10','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [30.1, 30.1, 30.1, 31.5, 31.5, 31.5, 33.2, 36, 28.1, 27, 27, 27, 36, 36, 36, 36, 34.2, 29.6, 29.7, 29.7, 34, 34, 12.6, 12.7, 15.7, 15.8, 16, 30.6, 30.6, 36, 30.2, 30.3, 33.7, 36, 36, 30.2, 30.2, 36, 36, 33.2, 33.2]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [33.1, 33.2, 34.5, 36, 29.9, 30, 36, 36, 32.6, 32.7, 36, 15.6, 18.8, 33.6, 33.2, 33.3, 36, 33.2, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 33, 36, 35.7, 18.6, 36, 36, 36]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [36, 36, 21.6, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 24.5]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5959}, "maxPsd": 17.1}, {"frequencyRange": {"highFrequency": 6019, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6019}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6058, "lowFrequency": 6050}, "maxPsd": 20.1}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6058}, "maxPsd": 20.2}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6079}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6137, "lowFrequency": 6108}, "maxPsd": 15.1}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6137}, "maxPsd": 13.9}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6271}, "maxPsd": 21.2}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6300}, "maxPsd": 16.6}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6330}, "maxPsd": 16.7}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6331}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6360}, "maxPsd": 21}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6389}, "maxPsd": -0.4}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6545, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6547, "lowFrequency": 6545}, "maxPsd": 2.7}, {"frequencyRange": {"highFrequency": 6560, "lowFrequency": 6547}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6570, "lowFrequency": 6560}, "maxPsd": 3}, {"frequencyRange": {"highFrequency": 6600, "lowFrequency": 6570}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6610, "lowFrequency": 6600}, "maxPsd": 17.6}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6610}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6667, "lowFrequency": 6660}, "maxPsd": 17.2}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6667}, "maxPsd": 17.3}, {"frequencyRange": {"highFrequency": 6690, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6700, "lowFrequency": 6690}, "maxPsd": 20.7}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6700}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": 17.2}, {"frequencyRange": {"highFrequency": 6840, "lowFrequency": 6770}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6840}, "maxPsd": 20.2}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6850}, "maxPsd": 22.9}], "requestId": "REQ-SIP10", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','da234915a0ee31f46b10bc908df1c35e51b96a15abbc3145142a73571b1b4dfe'); +INSERT INTO test_data VALUES('AFCS.SIP.11','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6676}, "maxPsd": 22.9}], "requestId": "REQ-SIP11", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','41c8479e1bfa05ed0fb971ab15facdc89fe41c296690ee14200777238148041d'); +INSERT INTO test_data VALUES('AFCS.SIP.12','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 28.8, 28.8, 36, 28.8, 28.9, 36, 28.9, 29, 36, 29, 29, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 31.8, 31.8, 31.9, 31.9, 32, 32, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 34.8, 34.9, 35, 36]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6211, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6221, "lowFrequency": 6211}, "maxPsd": 15.7}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6221}, "maxPsd": 15.8}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6242}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6271}, "maxPsd": 15.8}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6300}, "maxPsd": 15.9}, {"frequencyRange": {"highFrequency": 6330, "lowFrequency": 6302}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6361, "lowFrequency": 6330}, "maxPsd": 15.9}, {"frequencyRange": {"highFrequency": 6389, "lowFrequency": 6361}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6389}, "maxPsd": 16}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6676}, "maxPsd": 22.9}], "requestId": "REQ-SIP12", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','1c5e7b47e694325dea3f978959bb5089218f22048be7da0837074a51c5f4ce77'); +INSERT INTO test_data VALUES('AFCS.SIP.13','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [7.2, 30.3, 7.2, 7.2, 24.5, 2.1, 2.1, 7.4, 2.1, 2.2, 27.4, 7.8, 7.9, 7.9, 7.9, 8, 8, 1.3, 1.3, 1.4, 1.4, 1.4, 1.4, 34, 36, 36, 36, 36, 36, 32.8, 36, 36, 36, 34.5, 34.5, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [10.2, 10.2, 5.1, 5.1, 5.2, 10.8, 10.9, 11, 4.3, 4.4, 4.4, 4.5, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [13.2, 8.1, 8.2, 13.9, 7.3, 7.5, 36]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [11, 11.3, 10.4]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [7.1]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [14.2, 13.3]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5959, "lowFrequency": 5930}, "maxPsd": -5.9}, {"frequencyRange": {"highFrequency": 5961, "lowFrequency": 5959}, "maxPsd": -5.8}, {"frequencyRange": {"highFrequency": 5989, "lowFrequency": 5961}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 5989}, "maxPsd": -5.8}, {"frequencyRange": {"highFrequency": 6048, "lowFrequency": 6020}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6072, "lowFrequency": 6048}, "maxPsd": -11}, {"frequencyRange": {"highFrequency": 6079, "lowFrequency": 6072}, "maxPsd": -10.9}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6079}, "maxPsd": -5.7}, {"frequencyRange": {"highFrequency": 6108, "lowFrequency": 6107}, "maxPsd": -5.6}, {"frequencyRange": {"highFrequency": 6136, "lowFrequency": 6108}, "maxPsd": -10.9}, {"frequencyRange": {"highFrequency": 6139, "lowFrequency": 6136}, "maxPsd": -10.8}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6139}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6204, "lowFrequency": 6182}, "maxPsd": -5.2}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6204}, "maxPsd": -5.1}, {"frequencyRange": {"highFrequency": 6241, "lowFrequency": 6213}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6270, "lowFrequency": 6241}, "maxPsd": -5.1}, {"frequencyRange": {"highFrequency": 6272, "lowFrequency": 6270}, "maxPsd": -5}, {"frequencyRange": {"highFrequency": 6300, "lowFrequency": 6272}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6331, "lowFrequency": 6300}, "maxPsd": -11.7}, {"frequencyRange": {"highFrequency": 6360, "lowFrequency": 6331}, "maxPsd": -4.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6360}, "maxPsd": -11.6}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": 21}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": 19.8}, {"frequencyRange": {"highFrequency": 6760, "lowFrequency": 6676}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6770, "lowFrequency": 6760}, "maxPsd": 21.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6770}, "maxPsd": 22.9}], "requestId": "REQ-SIP13", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','a404e996fb61b59ccb7ec6cfbbe88d6ae2552e17a8e8a4432ddc746a50d45af3'); +INSERT INTO test_data VALUES('AFCS.SIP.14','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6676}, "maxPsd": 22.9}], "requestId": "REQ-SIP14", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','49da5287260ed433f8463ae852d14bda960c9052e8e1511037bc1334078cd6ef'); +INSERT INTO test_data VALUES('AFCS.SIP.15','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6676}, "maxPsd": 22.9}], "requestId": "REQ-SIP15", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','3206e0636f07ec5c8deb5423bb801eddd7f5b72ba5b5b50c2c7e318812498982'); +INSERT INTO test_data VALUES('AFCS.SIP.16','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 167], "globalOperatingClass": 133, "maxEirp": [36, 36, 36, 36, 36, 36, 36]}, {"channelCfi": [15, 47, 79], "globalOperatingClass": 134, "maxEirp": [36, 36, 36]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [36]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [36, 36]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 6425, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6676}, "maxPsd": 22.9}], "requestId": "REQ-SIP16", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','adfb93aa97b54897c25a922f7330dec1177b6b6606fdb4bcb89f68904d7739d7'); +INSERT INTO test_data VALUES('BRCM.EXT_FSP.1','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-2.4, -2.4, -2.4, 31.8, 36, 36, 36, 31.8, 10.2, 10.2, 10.2, 10.2, 33.3, 33.3, 33.4, 36, 36, 36, 36, 36, 20.1, 20.1, 20.2, 20.2, 36, 36, 36, 36, -8, -27, -27, -7.9, 36, 36, 36, 36, 36, 21.1, 21.1, 31.5, 36]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [0.6, 0.7, 36, 33.9, 13.2, 13.2, 36, 36, 36, 36, 23.1, 23.2, 36, -13.1, -24, -13, 36, 36, 24.1, 34.5]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [3.6, 30.8, 16.2, 36, 36, 26.2, -21, -12.9, 27.1]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [6.7, 19.3, 29.1, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-2.5]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [9.8, 1.7]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5951, "lowFrequency": 5930}, "maxPsd": -15.5}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5951}, "maxPsd": -15.4}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 5990}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6119, "lowFrequency": 6107}, "maxPsd": -2.9}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6119}, "maxPsd": -2.8}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6221, "lowFrequency": 6182}, "maxPsd": 20.3}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6221}, "maxPsd": 20.4}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6242}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6391, "lowFrequency": 6359}, "maxPsd": 7.1}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6391}, "maxPsd": 7.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6667, "lowFrequency": 6660}, "maxPsd": 21.5}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6667}, "maxPsd": 21.6}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": 8.1}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6810}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": 18.5}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6830}, "maxPsd": 22.9}], "requestId": "REQ-EXT_FSP1", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','8f727210eee8a303a6cad8aa27684ec59d88c89948acb26d2b6768dc1531d416'); +INSERT INTO test_data VALUES('BRCM.EXT_FSP.7','{"availableSpectrumInquiryResponses": [{"availabilityExpireTime": "0", "availableChannelInfo": [{"channelCfi": [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181], "globalOperatingClass": 131, "maxEirp": [-19.3, -19.3, -19.2, 15, 18, 18.1, 29.9, 15.7, -6, -6, -5.9, -5.9, 13.5, 13.5, 18.8, 30.6, 19.8, 19.8, 33.2, 33.2, -0.3, -0.3, -0.3, -0.2, 36, 36, 36, 36, -18.6, -27, -27, -18.6, 36, 36, 34.9, 28, 34.5, 0.6, 0.6, 11, 35]}, {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 123, 131, 139, 147, 155, 163, 171, 179], "globalOperatingClass": 132, "maxEirp": [-16.3, -16.2, 20.9, 17.8, -3, -2.9, 16.5, 21.9, 22.8, 28.5, 2.7, 2.8, 36, -23.7, -24, -23.6, 36, 29.4, 3.6, 14]}, {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167], "globalOperatingClass": 133, "maxEirp": [-13.2, 13.9, 0.1, 19.6, 25.9, 5.7, -21, -21, 6.6]}, {"channelCfi": [15, 47, 79, 143], "globalOperatingClass": 134, "maxEirp": [-10.1, 3.1, 8.7, -18]}, {"channelCfi": [2], "globalOperatingClass": 136, "maxEirp": [-19.3]}, {"channelCfi": [31, 63], "globalOperatingClass": 137, "maxEirp": [-7, -8.9]}], "availableFrequencyInfo": [{"frequencyRange": {"highFrequency": 5930, "lowFrequency": 5925}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 5988, "lowFrequency": 5930}, "maxPsd": -32.3}, {"frequencyRange": {"highFrequency": 5990, "lowFrequency": 5988}, "maxPsd": -32.2}, {"frequencyRange": {"highFrequency": 6010, "lowFrequency": 5990}, "maxPsd": 2.9}, {"frequencyRange": {"highFrequency": 6020, "lowFrequency": 6010}, "maxPsd": 3}, {"frequencyRange": {"highFrequency": 6047, "lowFrequency": 6020}, "maxPsd": 5}, {"frequencyRange": {"highFrequency": 6050, "lowFrequency": 6047}, "maxPsd": 5.1}, {"frequencyRange": {"highFrequency": 6078, "lowFrequency": 6050}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6107, "lowFrequency": 6078}, "maxPsd": 16.9}, {"frequencyRange": {"highFrequency": 6153, "lowFrequency": 6107}, "maxPsd": -19}, {"frequencyRange": {"highFrequency": 6168, "lowFrequency": 6153}, "maxPsd": -18.9}, {"frequencyRange": {"highFrequency": 6182, "lowFrequency": 6168}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6213, "lowFrequency": 6182}, "maxPsd": 0.5}, {"frequencyRange": {"highFrequency": 6242, "lowFrequency": 6213}, "maxPsd": 5.8}, {"frequencyRange": {"highFrequency": 6243, "lowFrequency": 6242}, "maxPsd": 17.5}, {"frequencyRange": {"highFrequency": 6271, "lowFrequency": 6243}, "maxPsd": 17.6}, {"frequencyRange": {"highFrequency": 6302, "lowFrequency": 6271}, "maxPsd": 6.8}, {"frequencyRange": {"highFrequency": 6359, "lowFrequency": 6302}, "maxPsd": 20.2}, {"frequencyRange": {"highFrequency": 6418, "lowFrequency": 6359}, "maxPsd": -13.3}, {"frequencyRange": {"highFrequency": 6420, "lowFrequency": 6418}, "maxPsd": -13.2}, {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6420}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6620, "lowFrequency": 6525}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6630, "lowFrequency": 6620}, "maxPsd": 22.1}, {"frequencyRange": {"highFrequency": 6640, "lowFrequency": 6630}, "maxPsd": 14}, {"frequencyRange": {"highFrequency": 6650, "lowFrequency": 6640}, "maxPsd": -40}, {"frequencyRange": {"highFrequency": 6660, "lowFrequency": 6650}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6667, "lowFrequency": 6660}, "maxPsd": 1}, {"frequencyRange": {"highFrequency": 6670, "lowFrequency": 6667}, "maxPsd": 1.1}, {"frequencyRange": {"highFrequency": 6740, "lowFrequency": 6670}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6750, "lowFrequency": 6740}, "maxPsd": 21.9}, {"frequencyRange": {"highFrequency": 6757, "lowFrequency": 6750}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6763, "lowFrequency": 6757}, "maxPsd": 15}, {"frequencyRange": {"highFrequency": 6790, "lowFrequency": 6763}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6800, "lowFrequency": 6790}, "maxPsd": 19.6}, {"frequencyRange": {"highFrequency": 6810, "lowFrequency": 6800}, "maxPsd": -12.4}, {"frequencyRange": {"highFrequency": 6813, "lowFrequency": 6810}, "maxPsd": 15.2}, {"frequencyRange": {"highFrequency": 6820, "lowFrequency": 6813}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6830, "lowFrequency": 6820}, "maxPsd": -2}, {"frequencyRange": {"highFrequency": 6850, "lowFrequency": 6830}, "maxPsd": 22.9}, {"frequencyRange": {"highFrequency": 6860, "lowFrequency": 6850}, "maxPsd": 22}, {"frequencyRange": {"highFrequency": 6875, "lowFrequency": 6860}, "maxPsd": 22.9}], "requestId": "REQ-EXT_FSP7", "response": {"responseCode": 0, "shortDescription": "Success"}, "rulesetId": "US_47_CFR_PART_15_SUBPART_E"}], "version": "1.4"}','a544eace0488558cd3a22a4b6b21cfc57ced69598d3dcf10c24bd19e5571522a'); +COMMIT; diff --git a/tests/regression/.env b/tests/regression/.env new file mode 100644 index 0000000..3fb26a2 --- /dev/null +++ b/tests/regression/.env @@ -0,0 +1,103 @@ +# --------------------------------------------------- # +# docker-compose.yaml variables # +# convention: Host volume VOL_H_XXX will be mapped # +# as container's volume VOL_C_YYY # +# VOL_H_XXX:VOL_C_YYY # +# --------------------------------------------------- # + +# -= MUST BE defined =- +# Hostname for AFC server +AFC_SERVER_NAME="_" +# Wether to forward all http requests to https +AFC_ENFORCE_HTTPS=TRUE + +# Host static DB root dir +VOL_H_DB=/opt/afc/databases/rat_transfer + +# Container's static DB root dir (dont change it !) +VOL_C_DB=/mnt/nfs/rat_transfer + +#RAT user to be used in containers +UID=1003 +GID=1003 + +# AFC service external PORTs configuration +# syntax: +# [IP]: +# like 172.31.11.188:80-180 +# where: +# IP is 172.31.11.188 +# port range is 80-180 + +# Here we configuring range of external ports to be used by the service +# docker-compose randomly uses one port from the range + +# Note 1: +# The IP arrdess can be skipped if there is only one external +# IP address (i.e. 80-180 w/o IP address is acceptable as well) + +# Note 2: +# range of ports can be skipped . and just one port is acceptable as well + +# all these valuase are acaptable: +# PORT=172.31.11.188:80-180 +# PORT=172.31.11.188:80 +# PORT=80-180 +# PORT=80 + + +# http ports range +EXT_PORT=172.31.11.188:80-180 + +# https host ports range +EXT_PORT_S=172.31.11.188:443-543 + + +# -= ALS CONFIGURATION STUFF =- + +# Port on which ALS Kafka server listens for clients +ALS_KAFKA_CLIENT_PORT_=9092 + +# ALS Kafka server host name +ALS_KAFKA_SERVER_=als_kafka + +# Maximum ALS message size (default 1MB is too tight for GUI AFC Response) +ALS_KAFKA_MAX_REQUEST_SIZE_=10485760 + + +# -= FS(ULS) DOWNLOADER CONFIGURATION STUFF =- + +# Symlink pointing to current ULS database +ULS_CURRENT_DB_SYMLINK=FS_LATEST.sqlite3 + + +# -= RCACHE SERVICE CONFIGURATION STUFF =- + +# True (1, t, on, y, yes) to enable use of Rcache. False (0, f, off, n, no) to +# use legacy file-based cache. Default is True +RCACHE_ENABLED=True + +# Port Rcache service listens os +RCACHE_CLIENT_PORT=8000 + + +# -= SECRETS STUFF =- + +# Host directory containing secret files +VOL_H_SECRETS=../../tools/secrets/empty_secrets +#VOL_H_SECRETS=/opt/afc/secrets + +# Directory inside container where to secrets are mounted (always /run/secrets +# in Compose, may vary in Kubernetes) +VOL_C_SECRETS=/run/secrets + + + +# -= OPTIONAL =- +# to work without tls/mtls,remove these variables from here +# if you have tls/mtls configuration, keep configuration +# files in these host volumes +VOL_H_SSL=./ssl +VOL_C_SSL=/usr/share/ca-certificates/certs +VOL_H_NGNX=./ssl/nginx +VOL_C_NGNX=/certificates/servers diff --git a/tests/regression/build_imgs.sh b/tests/regression/build_imgs.sh new file mode 100755 index 0000000..0e4ded9 --- /dev/null +++ b/tests/regression/build_imgs.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# +# Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +set -x +wd=${1} # full path to the afc project dir +tag=${2} # tag to be used for new docker images +push=${3:-1} # whether push new docker images into repo [0/1] +source $wd/tests/regression/regression.sh + +build_dev_server $1 $2 $3 + +# Local Variables: +# vim: sw=2:et:tw=80:cc=+1 \ No newline at end of file diff --git a/tests/regression/clean.sh b/tests/regression/clean.sh new file mode 100755 index 0000000..e6821df --- /dev/null +++ b/tests/regression/clean.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# +# Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +set -x +hostname +wd=${1} +rand=${2} +rm -fr ${wd} diff --git a/tests/regression/docker-compose.yaml b/tests/regression/docker-compose.yaml new file mode 100755 index 0000000..0a7ba70 --- /dev/null +++ b/tests/regression/docker-compose.yaml @@ -0,0 +1,262 @@ +# check .env file for env vars info and values + +version: '3.2' +services: + ratdb: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/ratdb-image:${TAG:-latest} + restart: always + dns_search: [.] + + rmq: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/rmq-image:${TAG:-latest} + restart: always + dns_search: [.] + + dispatcher: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/dispatcher-image:${TAG:-latest} + restart: always + ports: + - "${EXT_PORT}:80" + - "${EXT_PORT_S}:443" + volumes: + - ${VOL_H_NGNX:-/tmp}:${VOL_C_NGNX:-/dummyngnx} + environment: + - AFC_SERVER_NAME=${AFC_SERVER_NAME:-_} + - AFC_ENFORCE_HTTPS=${AFC_ENFORCE_HTTPS:-TRUE} + # set to true if required to enforce mTLS check + - AFC_ENFORCE_MTLS=false + - AFC_MSGHND_NAME=msghnd + - AFC_MSGHND_PORT=8000 + - AFC_WEBUI_NAME=rat_server + - AFC_WEBUI_PORT=80 + # Filestorage params: + - AFC_OBJST_HOST=objst + - AFC_OBJST_PORT=5000 + - AFC_OBJST_SCHEME=HTTP + depends_on: + - msghnd + - rat_server + dns_search: [.] + + rat_server: + image: ${PRIV_REPO:-110738915961.dkr.ecr.us-east-1.amazonaws.com}/afc-server:${TAG:-latest} + volumes: + - ${VOL_H_DB}:${VOL_C_DB} + - ./pipe:/pipe + depends_on: + - ratdb + - rmq + - objst + - als_kafka + - als_siphon + - bulk_postgres + - rcache + secrets: + - NOTIFIER_MAIL.json + - OIDC.json + - REGISTRATION.json + - REGISTRATION_CAPTCHA.json + dns_search: [.] + environment: + # RabbitMQ server name: + - BROKER_TYPE=external + - BROKER_FQDN=rmq + # Filestorage params: + - AFC_OBJST_HOST=objst + - AFC_OBJST_PORT=5000 + - AFC_OBJST_SCHEME=HTTP + # ALS params + - ALS_KAFKA_SERVER_ID=rat_server + - ALS_KAFKA_CLIENT_BOOTSTRAP_SERVERS=${ALS_KAFKA_SERVER_}:${ALS_KAFKA_CLIENT_PORT_} + - ALS_KAFKA_MAX_REQUEST_SIZE=${ALS_KAFKA_MAX_REQUEST_SIZE_} + # Rcache parameters + - RCACHE_ENABLED=${RCACHE_ENABLED} + - RCACHE_POSTGRES_DSN=postgresql://postgres:postgres@bulk_postgres/rcache + - RCACHE_SERVICE_URL=http://rcache:${RCACHE_CLIENT_PORT} + - RCACHE_RMQ_DSN=amqp://rcache:rcache@rmq:5672/rcache + + msghnd: + image: ${PRIV_REPO:-110738915961.dkr.ecr.us-east-1.amazonaws.com}/afc-msghnd:${TAG:-latest} + environment: + # RabbitMQ server name: + - BROKER_TYPE=external + - BROKER_FQDN=rmq + # Filestorage params: + - AFC_OBJST_HOST=objst + - AFC_OBJST_PORT=5000 + - AFC_OBJST_SCHEME=HTTP + # ALS params + - ALS_KAFKA_SERVER_ID=msghnd + - ALS_KAFKA_CLIENT_BOOTSTRAP_SERVERS=${ALS_KAFKA_SERVER_}:${ALS_KAFKA_CLIENT_PORT_} + - ALS_KAFKA_MAX_REQUEST_SIZE=${ALS_KAFKA_MAX_REQUEST_SIZE_} + # Rcache parameters + - RCACHE_ENABLED=${RCACHE_ENABLED} + - RCACHE_POSTGRES_DSN=postgresql://postgres:postgres@bulk_postgres/rcache + - RCACHE_SERVICE_URL=http://rcache:${RCACHE_CLIENT_PORT} + - RCACHE_RMQ_DSN=amqp://rcache:rcache@rmq:5672/rcache + dns_search: [.] + depends_on: + - ratdb + - rmq + - objst + - als_kafka + - als_siphon + - bulk_postgres + - rcache + + objst: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/objstorage-image:${TAG:-latest} + environment: + - AFC_OBJST_PORT=5000 + - AFC_OBJST_HIST_PORT=4999 + - AFC_OBJST_LOCAL_DIR=/storage + dns_search: [.] + worker: + image: ${PRIV_REPO:-110738915961.dkr.ecr.us-east-1.amazonaws.com}/afc-worker:${TAG:-latest} + volumes: + - ${VOL_H_DB}:${VOL_C_DB} + - ./pipe:/pipe + environment: + # Filestorage params: + - AFC_OBJST_HOST=objst + - AFC_OBJST_PORT=5000 + - AFC_OBJST_SCHEME=HTTP + # worker params + - AFC_WORKER_CELERY_WORKERS=rat_1 rat_2 + # RabbitMQ server name: + - BROKER_TYPE=external + - BROKER_FQDN=rmq + # afc-engine preload lib params + - AFC_AEP_ENABLE=1 + - AFC_AEP_DEBUG=1 + - AFC_AEP_REAL_MOUNTPOINT=${VOL_C_DB}/3dep/1_arcsec + # Rcache parameters + - RCACHE_ENABLED=${RCACHE_ENABLED} + - RCACHE_SERVICE_URL=http://rcache:${RCACHE_CLIENT_PORT} + - RCACHE_RMQ_DSN=amqp://rcache:rcache@rmq:5672/rcache + # ALS params + - ALS_KAFKA_SERVER_ID=worker + - ALS_KAFKA_CLIENT_BOOTSTRAP_SERVERS=${ALS_KAFKA_SERVER_}:${ALS_KAFKA_CLIENT_PORT_} + - ALS_KAFKA_MAX_REQUEST_SIZE=${ALS_KAFKA_MAX_REQUEST_SIZE_} + depends_on: + - ratdb + - rmq + - objst + - rcache + - als_kafka + dns_search: [.] + + als_kafka: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/als-kafka-image:${TAG:-latest} + restart: always + environment: + - KAFKA_ADVERTISED_HOST=${ALS_KAFKA_SERVER_} + - KAFKA_CLIENT_PORT=${ALS_KAFKA_CLIENT_PORT_} + - KAFKA_MAX_REQUEST_SIZE=${ALS_KAFKA_MAX_REQUEST_SIZE_} + dns_search: [.] + + als_siphon: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/als-siphon-image:${TAG:-latest} + restart: always + environment: + - KAFKA_SERVERS=${ALS_KAFKA_SERVER_}:${ALS_KAFKA_CLIENT_PORT_} + - POSTGRES_HOST=bulk_postgres + - INIT_IF_EXISTS=skip + - KAFKA_MAX_REQUEST_SIZE=${ALS_KAFKA_MAX_REQUEST_SIZE_} + depends_on: + - als_kafka + - bulk_postgres + dns_search: [.] + + bulk_postgres: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/bulk-postgres-image:${TAG:-latest} + dns_search: [.] + + uls_downloader: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/uls-downloader:${TAG:-latest} + restart: always + environment: + - ULS_AFC_URL=http://msghnd:8000/fbrat/ap-afc/availableSpectrumInquiryInternal?nocache=True + - ULS_DELAY_HR=1 + - ULS_SERVICE_STATE_DB_DSN=postgresql://postgres:postgres@bulk_postgres/fs_state + - ULS_PROMETHEUS_PORT=8000 + # Rcache parameters + - RCACHE_ENABLED=${RCACHE_ENABLED} + - RCACHE_SERVICE_URL=http://rcache:${RCACHE_CLIENT_PORT} + volumes: + - ${VOL_H_DB}:/rat_transfer + secrets: + - NOTIFIER_MAIL.json + depends_on: + - bulk_postgres + dns_search: [.] + + cert_db: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/cert_db:${TAG:-latest} + depends_on: + - ratdb + links: + - ratdb + - als_kafka + environment: + - ALS_KAFKA_SERVER_ID=cert_db + - ALS_KAFKA_CLIENT_BOOTSTRAP_SERVERS=${ALS_KAFKA_SERVER_}:${ALS_KAFKA_CLIENT_PORT_} + - ALS_KAFKA_MAX_REQUEST_SIZE=${ALS_KAFKA_MAX_REQUEST_SIZE_} + + rcache: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/rcache-image:${TAG:-latest} + restart: always + environment: + - RCACHE_ENABLED=${RCACHE_ENABLED} + - RCACHE_CLIENT_PORT=${RCACHE_CLIENT_PORT} + - RCACHE_POSTGRES_DSN=postgresql://postgres:postgres@bulk_postgres/rcache + - RCACHE_AFC_REQ_URL=http://msghnd:8000/fbrat/ap-afc/availableSpectrumInquiry?nocache=True + - RCACHE_RULESETS_URL=http://rat_server/fbrat/ratapi/v1/GetRulesetIDs + - RCACHE_CONFIG_RETRIEVAL_URL=http://rat_server/fbrat/ratapi/v1/GetAfcConfigByRulesetID + depends_on: + - bulk_postgres + dns_search: [.] + + grafana: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/grafana-image:${TAG:-latest} + restart: always + depends_on: + - prometheus + - bulk_postgres + dns_search: [.] + + prometheus: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/prometheus-image:${TAG:-latest} + restart: always + depends_on: + - cadvisor + - nginxexporter + dns_search: [.] + + cadvisor: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/cadvisor-image:${TAG:-latest} + restart: always + volumes: + - /:/rootfs:ro + - /var/run:/var/run:rw + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + - /dev/disk/:/dev/disk:ro + dns_search: [.] + + nginxexporter: + image: ${PUB_REPO:-public.ecr.aws/w9v6y1o0/openafc}/nginxexporter-image:${TAG:-latest} + restart: always + depends_on: + - dispatcher + dns_search: [.] + +secrets: + NOTIFIER_MAIL.json: + file: ${VOL_H_SECRETS}/NOTIFIER_MAIL.json + OIDC.json: + file: ${VOL_H_SECRETS}/OIDC.json + REGISTRATION.json: + file: ${VOL_H_SECRETS}/REGISTRATION.json + REGISTRATION_CAPTCHA.json: + file: ${VOL_H_SECRETS}/REGISTRATION_CAPTCHA.json diff --git a/tests/regression/docker_build_and_push.sh b/tests/regression/docker_build_and_push.sh new file mode 100755 index 0000000..bbe1312 --- /dev/null +++ b/tests/regression/docker_build_and_push.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +set -x +echo `pwd` +source ./tests/regression/regression.sh + file=${1} # Name of the Dockerfile + image=${2} # Name and optionally a tag in the 'name:tag' format +# args=${3:-" "} # extra args for docker build command + docker_build_and_push ${file} ${image} "$3" diff --git a/tests/regression/docker_build_and_push_server.sh b/tests/regression/docker_build_and_push_server.sh new file mode 100755 index 0000000..699e560 --- /dev/null +++ b/tests/regression/docker_build_and_push_server.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# +# Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +set -x +source ./tests/regression/regression.sh + file=${1} # Name of the Dockerfile + image=${2} # Name and optionally a tag in the 'name:tag' format + d4b_tag=${3} # base docker_for_build tag + preinst_tag=${4} # base preinst image tag + d4b_name=${5} # base docker_for_build image name + preinst_name=${6} # base preinst image name + docker_build_and_push_server ${file} ${image} ${d4b_tag} ${preinst_tag} ${d4b_name} ${preinst_name} diff --git a/tests/regression/drepo_login.sh b/tests/regression/drepo_login.sh new file mode 100644 index 0000000..cfdd2b3 --- /dev/null +++ b/tests/regression/drepo_login.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# +# Copyright 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +set -x +docker_login() { + aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/w9v6y1o0 + aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 110738915961.dkr.ecr.us-east-1.amazonaws.com +} + +docker_login \ No newline at end of file diff --git a/tests/regression/git_clone.sh b/tests/regression/git_clone.sh new file mode 100755 index 0000000..bbe1312 --- /dev/null +++ b/tests/regression/git_clone.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +set -x +echo `pwd` +source ./tests/regression/regression.sh + file=${1} # Name of the Dockerfile + image=${2} # Name and optionally a tag in the 'name:tag' format +# args=${3:-" "} # extra args for docker build command + docker_build_and_push ${file} ${image} "$3" diff --git a/tests/regression/regression.sh b/tests/regression/regression.sh new file mode 100644 index 0000000..70775f4 --- /dev/null +++ b/tests/regression/regression.sh @@ -0,0 +1,184 @@ +#!/bin/bash +# +# Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +PRIV_REPO="${PRIV_REPO:=110738915961.dkr.ecr.us-east-1.amazonaws.com}" +PUB_REPO="${PUB_REPO:=public.ecr.aws/w9v6y1o0/openafc}" + +SRV="${PRIV_REPO}/afc-server" # server image +MSGHND="${PRIV_REPO}/afc-msghnd" # msghnd image +OBJST="${PUB_REPO}/objstorage-image" # object storage +RATDB=${PUB_REPO}"/ratdb-image" # ratdb image +RMQ="${PUB_REPO}/rmq-image" # rabbitmq image +DISPATCHER="${PUB_REPO}/dispatcher-image" # dispatcher image +ALS_SIPHON="${PUB_REPO}/als-siphon-image" # ALS Siphon image +ALS_KAFKA="${PUB_REPO}/als-kafka-image" # Kafka for ALS +BULK_POSTGRES="${PUB_REPO}/bulk-postgres-image" # PostgreSQL for bulk stuff (ALS, req cache, etc.) +RCACHE="${PUB_REPO}/rcache-image" # Request cache +GRAFANA="${PUB_REPO}/grafana-image" # Grafana +PROMETHEUS="${PUB_REPO}/prometheus-image" # Prometheus +CADVISOR="${PUB_REPO}/cadvisor-image" # Cadvisor +NGINXEXPORTER="${PUB_REPO}/nginxexporter-image" # Nginx-exporter + +WORKER=${PRIV_REPO}"/afc-worker" # msghnd image +WORKER_AL_D4B="${PUB_REPO}/worker-al-build-image" # Alpine worker build img +WORKER_AL_PRINST="${PUB_REPO}/worker-al-preinstall" # Alpine worker preinst + +ULS_UPDATER=${PRIV_REPO}"/uls-updater" # ULS Updater image +ULS_DOWNLOADER="${PUB_REPO}/uls-downloader" # ULS Downloader image + +CERT_DB="${PUB_REPO}/cert_db" # CERT DB image +RTEST_DI="rtest" # regression tests image + + +# FUNCS +msg() { + echo -e "\e[34m \e[1m$1\e[0m" +} +err() { + echo -e "\e[31m$1\e[0m" +} +ok() { + echo -e "\e[32m$1\e[0m" +} + +check_ret() { + ret=${1} # only 0 is OK + + if [ ${ret} -eq 0 ]; then + ok "OK" + else err "FAIL"; exit ${ret} + fi +} + +docker_build() { + file=${1} # Name of the Dockerfile + image=${2} # Name and optionally a tag in the 'name:tag' format + args=${3} + msg "docker build ${file} file into ${image} image extra args: ${args}" + docker build . -f ${file} -t ${image} ${args} + check_ret $? +} + +docker_push() { + image=${1} # Name and optionally a tag in the 'name:tag' format + + msg "docker push ${image} image" + docker push ${image} + check_ret $? +} + +docker_build_and_push() { + file=${1} # Name of the Dockerfile + image=${2} # Name and optionally a tag in the 'name:tag' format + push=${3:-1} # whether push new docker images into repo [0/1] + args=${4} # extra arguments + + msg " docker_build_and_push push:${push} args:${args}" + + docker_build ${file} ${image} "${args}" + + if [ $? -eq 0 ]; then + if [ ${push} -eq 1 ]; then + docker_push ${image} + fi + fi +} +docker_login () { + pub_repo_login=${1} + priv_repo_login=${2} + if test ${pub_repo_login}; then + err "FAIL \"${pub_repo_login}" not defined"; exit $? + fi + if test ${priv_repo_login}; then + err "FAIL \"${priv_repo_login}" not defined"; exit $? + fi + "${pub_repo_login}" && "${priv_repo_login}" + check_ret $? +} + + +build_dev_server() { + wd=${1} # full path to the afc project dir + tag=${2} # tag to be used for new docker images + push=${3:-1} # whether push new docker images into repo [0/1] + # cd to a work dir + cd ${wd} + # get last git commit hash number + BUILDREV=`git rev-parse --short HEAD` + +# if login if docker push required +# if [ ${push} -eq 1 ]; then +# docker_login "${pub_repo_login}" "${priv_repo_login}" +# fi + + # build regression test docker image + cd ${wd} + docker_build tests/Dockerfile ${RTEST_DI}:${tag} + check_ret $? + + # build in parallel server docker prereq images (preinstall and docker_for_build) + docker_build_and_push ${wd}/worker/Dockerfile.build ${WORKER_AL_D4B}:${tag} ${push} & + docker_build_and_push ${wd}/worker/Dockerfile.preinstall ${WORKER_AL_PRINST}:${tag} ${push} & + + msg "wait for prereqs to be built" + # wait for background jobs to be done + wait + msg "prereqs are built" + + # build of ULS dockers + EXT_ARGS="--build-arg BLD_TAG=${tag} --build-arg PRINST_TAG=${tag} --build-arg BLD_NAME=${WORKER_AL_D4B} --build-arg PRINST_NAME=${WORKER_AL_PRINST} --build-arg BUILDREV=${BUILDREV}" + docker_build_and_push ${wd}/uls/Dockerfile-uls_service ${ULS_DOWNLOADER}:${tag} ${push} "${EXT_ARGS}" & + + # build msghnd (flask + gunicorn) + docker_build_and_push ${wd}/msghnd/Dockerfile ${MSGHND}:${tag} ${push} & + + # build worker image + EXT_ARGS="--build-arg BLD_TAG=${tag} --build-arg PRINST_TAG=${tag} --build-arg BLD_NAME=${WORKER_AL_D4B} --build-arg PRINST_NAME=${WORKER_AL_PRINST} --build-arg BUILDREV=worker" + docker_build_and_push ${wd}/worker/Dockerfile ${WORKER}:${tag} ${push} "${EXT_ARGS}" & + + # build afc ratdb docker image + docker_build_and_push ${wd}/ratdb/Dockerfile ${RATDB}:${tag} ${push} & + + # build afc dynamic data storage image + docker_build_and_push ${wd}/objstorage/Dockerfile ${OBJST}:${tag} ${push}& + cd ${wd} + + # build afc rabbit MQ docker image + docker_build_and_push ${wd}/rabbitmq/Dockerfile ${RMQ}:${tag} ${push} & + + # build afc dispatcher docker image + docker_build_and_push ${wd}/dispatcher/Dockerfile ${DISPATCHER}:${tag} ${push} & + + # build afc server docker image + EXT_ARGS="--build-arg BUILDREV=${BUILDREV}" + docker_build_and_push ${wd}/rat_server/Dockerfile ${SRV}:${tag} ${push} "${EXT_ARGS}" + + # build ALS-related images + cd ${wd}/als && docker_build_and_push Dockerfile.siphon ${ALS_SIPHON}:${tag} ${push} & + cd ${wd}/als && docker_build_and_push Dockerfile.kafka ${ALS_KAFKA}:${tag} ${push} & + cd ${wd}/bulk_postgres && docker_build_and_push Dockerfile ${BULK_POSTGRES}:${tag} ${push} & + cd ${wd} + + # build cert db image + docker_build_and_push ${wd}/cert_db/Dockerfile ${CERT_DB}:${tag} ${push} & + + # Build Request Cache + docker_build_and_push ${wd}/rcache/Dockerfile ${RCACHE}:${tag} ${push} & + + # Build Prometheus-related images + cd ${wd}/prometheus && docker_build_and_push Dockerfile-prometheus ${PROMETHEUS}:${tag} ${push} & + cd ${wd}/prometheus && docker_build_and_push Dockerfile-cadvisor ${CADVISOR}:${tag} ${push} & + cd ${wd}/prometheus && docker_build_and_push Dockerfile-nginxexporter ${NGINXEXPORTER}:${tag} ${push} & + cd ${wd}/prometheus && docker_build_and_push Dockerfile-grafana ${GRAFANA}:${tag} ${push} & + + msg "wait for all images to be built" + wait + msg "-done-" +} +# Local Variables: +# vim: sw=2:et:tw=80:cc=+1 diff --git a/tests/regression/run_srvr.sh b/tests/regression/run_srvr.sh new file mode 100755 index 0000000..8025ddd --- /dev/null +++ b/tests/regression/run_srvr.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# +# Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +set -x +# ./run_srvr.sh /work/open-afc MY_TAG 0 +# 1. starts AFC server server +# 2. export configuration form test database ( users, APs, afc-config) +# 3. integrates the settings from #2 into the AFC server to prepare it to autiomation tests + +wd=${1} # full path to the afc project path +TAG=${2} # docker images tag +randdir=${3:-0} # whether to use a dir with random name that includes tag name. + # like $wd/tests/regression_${TAG} If so, it should be already existing + # Usually used by regression test infra. not requred for user's unit testing +export TAG + +source $wd/tests/regression/regression.sh + +hostname +# export test dut configuration +cd $wd && docker build . -t ${RTEST_DI}:${TAG} -f tests/Dockerfile + +# whether to use a dir with random name +tests_dir=$wd/tests/regression +if [ ${randdir} -eq 1 ]; then + tests_dir=$wd/tests/regression_${TAG} +fi +# go to test dir +cd $tests_dir + +# create pipe and afc_config dir if does not exist +[ ! -d pipe ] && mkdir pipe + +# export configuration from regression test container +docker run --rm -v `pwd`/pipe:/pipe ${RTEST_DI}:${TAG} --cmd exp_adm_cfg --outfile /pipe/export_admin_cfg.json +check_ret $? +# copy regr server tls/mtls config (if existing) +[ -d ~/template_regrtest/rat_server-conf ] && cp -ar ~/template_regrtest/rat_server-conf . +[ -d ~/template_regrtest/ssl ] && cp -a ~/template_regrtest/ssl . + +# run srvr +docker-compose down -v && docker-compose up -d && docker ps -a +check_ret $? +sleep 5 + +# set default srvr configuration +docker-compose exec -T rat_server rat-manage-api db-create +docker-compose exec -T rat_server rat-manage-api cfg add src=/pipe/export_admin_cfg.json +docker-compose exec -T rat_server rat-manage-api user create --role Super --role Admin \ +--role AP --role Analysis --org fcc "admin@afc.com" "openafc" +docker-compose exec -T rat_server rat-manage-api cert_id create --location 3 \ +--cert_id FsDownloaderCertIdUS --ruleset_id US_47_CFR_PART_15_SUBPART_E +docker-compose exec -T rat_server rat-manage-api cert_id create --location 3 \ +--cert_id FsDownloaderCertIdCA --ruleset_id CA_RES_DBS-06 +docker-compose exec -T rat_server rat-manage-api cert_id create --location 3 \ +--cert_id FsDownloaderCertIdBR --ruleset_id BRAZIL_RULESETID + +# Local Variables: +# vim: sw=2:et:tw=80:cc=+1 diff --git a/tests/regression/run_tests.sh b/tests/regression/run_tests.sh new file mode 100755 index 0000000..47279d4 --- /dev/null +++ b/tests/regression/run_tests.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# +# Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program + +trap 'kill 0' SIGINT +echo `pwd` +wd=${1} +di_name=${2} +addr=${3} +port=${4:-443} +prot=${5:-"https"} +burst=${6:-8} +ext_args=${7} +ap_count=$(docker run --rm ${di_name} --cmd get_nbr_testcases;echo $?) + +source $wd/tests/regression/regression.sh +retval=0 +check_retval() { + ret=${1} # only 0 is OK + + if [ ${ret} -eq 0 ]; then + ok "OK" + else err "FAIL"; retval=1 + fi +} + +loop() { + start=0 + max=${1} + step=${2:-10} + s=${start} + verify_tls='' + if [ "$prot" == "https" ]; then + verify_tls='--verif' + fi + + echo "verify_tls - $verify_tls" + + while [ $s -le $max ] + do + e=$(($((s + ${step}))<=${max} ? $((s + ${step})) : ${max})) + echo "from $s to $e" + # run processes and store pids in array + for i in `seq $((s+1)) ${e}`; do + docker run --rm ${di_name} --addr=${addr} --port=${port} \ + --prot=${prot} --cmd=run --testcase_indexes=${i} \ + --prefix_cmd /usr/app/certs.sh cert_client \ + ${verify_tls} ${ext_args} \ + --cli_cert /usr/app/test_cli/test_cli_crt.pem \ + --cli_key /usr/app/test_cli/test_cli_key.pem & + pids+=( $! ) + done + s=$((s + ${step})) + # wait for all pids + for pid in ${pids[*]}; do + wait $pid + check_retval $? + done + unset pids + done + return $retval +} + +cd $wd +loop $ap_count ${burst} + + +# Local Variables: +# vim: sw=2:et:tw=80:cc=+1 diff --git a/tests/regression/stop_srvr.sh b/tests/regression/stop_srvr.sh new file mode 100755 index 0000000..ee56fc3 --- /dev/null +++ b/tests/regression/stop_srvr.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# +# Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +set -x +hostname +wd=${1} +TAG=${2} +export TAG +cd $wd/tests/regression_${TAG} +docker ps -a +docker-compose logs +docker-compose down -v +docker ps -a diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..48329f3 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,10 @@ +beautifulsoup4==4.12.2 +bs4==0.0.1 +certifi +charset-normalizer==2.0.11 +idna==3.3 +numpy>=1.12.1 +requests==2.31.0 +openpyxl==3.0.9 +ordered-set==4.1.0 +deepdiff==5.8.1 diff --git a/tools/certs.sh b/tools/certs.sh new file mode 100755 index 0000000..578a530 --- /dev/null +++ b/tools/certs.sh @@ -0,0 +1,80 @@ +#!/bin/sh +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +make_key() +{ + local k_name=$1 + local cmd="openssl genrsa -out $k_name/$k_name"_key.pem" 4096 > /dev/null 2>&1" + eval $cmd + if [ "$?" -ne "0" ]; then echo -e "ERROR: Failed to generate key $k_name\n" ; return 1 ; fi +} +make_client_cert() +{ + local cli_name=${1:-test}"_cli" + local ca_path=${AFC_CA_CERT_PATH:-.} + local ca_crt="test_ca_crt.pem" + local ca_key="test_ca_key.pem" + #local cli_addr=$(hostname -i) + local cli_addr=$(ifconfig eth0 | grep 'inet ' | awk '{ print $2}') + + mkdir $cli_name + make_key $cli_name + openssl req -new -key $cli_name/$cli_name"_key.pem" \ + -out $cli_name/$cli_name".csr" -sha256 \ + -subj "/C=IL/ST=Israel/L=Tel Aviv/O=Broadcom/CN=$cli_name" > /dev/null 2>&1 + + cat << EOF > $cli_name"_ext.cnf" +authorityKeyIdentifier=keyid,issuer +basicConstraints = CA:FALSE +extendedKeyUsage=clientAuth +keyUsage = critical, digitalSignature, keyEncipherment +subjectAltName = IP:$cli_addr +subjectKeyIdentifier=hash +EOF + cat << EOF > $cli_name".cnf" +HOME = . + +[ ca ] +default_ca = CA_default + +[ CA_default ] +dir = $cli_name +certs = $cli_name +database = $cli_name/index.txt +new_certs_dir = $cli_name +serial = $cli_name/serial.txt +policy = policy_default +default_md = sha256 + +[ policy_default ] +EOF + touch $cli_name/index.txt + echo 01 > $cli_name/serial.txt + openssl ca -config $cli_name".cnf" \ + -startdate $(date --date='-7 days' +'%y%m%d000000Z') \ + -days 29 -batch -notext \ + -out $cli_name/$cli_name"_crt.pem" -cert $ca_path/$ca_crt \ + -keyfile $ca_path/$ca_key \ + -in $cli_name/$cli_name".csr" -extfile $cli_name"_ext.cnf" > /dev/null 2>&1 + # echo "\nCreated certificate." + openssl x509 -startdate -enddate -noout -in $cli_name/$cli_name"_crt.pem" > /dev/null + return 0 +} +# +# +# +case $1 in + cert_client) + shift + make_client_cert $1 + ;; + ''|*) + echo -e "Nothing todo ($1)\n" + ;; +esac + +exit diff --git a/tools/db_tools/update_db.sh b/tools/db_tools/update_db.sh new file mode 100755 index 0000000..6071a91 --- /dev/null +++ b/tools/db_tools/update_db.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -e +set -x + +if [ $# -ne 4 ] || ! [ -d $1 ]; then + echo "Usage: $0 " + exit +fi + +pgdata=$(readlink -f $1) +pass=$2 +db_name=fbrat +ver_old=$3 +ver_new=$4 +wd=$(dirname "$pgdata") + +mkdir -p "$wd"/dbtmp + +docker pull postgres:$ver_old +container=$(docker run --rm -d -e POSTGRES_PASSWORD="$pass" -e PGDATA=/var/lib/pgsql/data -e POSTGRES_DB="$db_name" -v "$pgdata":/var/lib/pgsql/data --name postgres_old postgres:$ver_old) +docker logs -f postgres_old & +sleep 3 +docker exec -it $container chown postgres:postgres /var/lib/pgsql/data -R +docker exec -it $container pg_dumpall -U postgres > "$wd"/dbtmp/dump.sql +docker stop postgres_old + +mv "$pgdata" "$pgdata".back +mkdir "$wd"/pgdata + +docker pull postgres:$ver_new +container=$(docker run --rm -d -e POSTGRES_PASSWORD="$pass" -e PGDATA=/var/lib/pgsql/data -e POSTGRES_DB="$db_name" -v "$pgdata":/var/lib/pgsql/data -v "$wd"/dbtmp:/dbtmp --name postgres_new postgres:$ver_new) +docker logs -f postgres_new & +sleep 3 +docker exec -it $container chown postgres:postgres /var/lib/pgsql/data -R +docker exec -it $container psql -U postgres -f /dbtmp/dump.sql +docker stop postgres_new + +sed -i "s/scram-sha-256$/trust/" "$pgdata"/pg_hba.conf + +rm -rf "$wd"/dbtmp + + + + + + diff --git a/tools/editing/README.md b/tools/editing/README.md new file mode 100644 index 0000000..20c539c --- /dev/null +++ b/tools/editing/README.md @@ -0,0 +1,84 @@ +# Code Format # + +In order to keep the code consistent, the use of automated code formatters is requested for a PR to be accepted. Mis-formatted code will fail a build. You may wish to set up your editor to re-format on save depending on the editors you use. + +The formatters we expect are: +For C++: [clang-format](https://clang.llvm.org/docs/ClangFormat.html) +For python: [pycodestyle] (https://pycodestyle.pycqa.org/en/latest/) is used to check and [autopep8](https://pypi.org/project/autopep8/) to format +For javascript: [prettier] (https://prettier.io/) + + +## C++ ## +Install `clang-format` on your build machine: +``` +sudo apt install clang-format +``` +or use docker container like silkeh/clang + +Check the version: +``` +clang-format --version +``` +or docker based: +``` +docker run --rm silkeh/clang:14-stretch clang-format --version +``` +Needed version: 14 or higher + +The `clang-format` configuration file is present in root directory as `.clang-format` which is symlink to `tools/editing/clang_format_configs`. + +How to format a given file (format full file): +``` +clang-format -i .// +# Ex: clang-format -i ./src/afc-engine/AfcManager.cpp +``` +or docker based: +``` +docker run --rm --user `id -u`:`id -g` --group-add `id -G | sed "s/ / --group-add /g"` -v `pwd`:/open-afc -w /open-afc silkeh/clang:14-stretch clang-format -i ./src/afc-engine/AfcManager.cpp +``` + +How to format git staged changes: +``` +git-clang-format --staged .// +``` + +How to format git unstaged changes: +``` +git-clang-format --force .// +``` + +## Python ## + +Install pycodestyle +``` +pip install pycodestyle +``` +There is a configuration file [pycodestyle.cfg](pycodestyle.cfg) inside tools/editing that specifies the set up of the checks that are run against pull requests. To run this from the root directory: + +``` +pycodestyle --config=tools/editing/pycodestyle.cfg . +``` + +We use the autopep8 code formatter to automate making most of the fixes that are required. + +``` +pip install autopep8 +``` + +To run on an individual file, also from the root: +``` + autopep8 --in-place --global-config=tools/editing/pycodestyle.cfg -a -a +``` + +## JavaScript ## + +For JavaScript files, we use prettier. It is installed as a dev dependency in the [package.json](../../src/web/package.json). + +To run a check +``` +yarn prettier src --check +``` +and to run a re-format +``` +yarn prettier src --write +``` \ No newline at end of file diff --git a/tools/editing/clang_format_configs b/tools/editing/clang_format_configs new file mode 100644 index 0000000..e8a04b4 --- /dev/null +++ b/tools/editing/clang_format_configs @@ -0,0 +1,153 @@ +--- +Language: Cpp +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AlignConsecutiveBitFields: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign +AlignOperands: Align +AlignTrailingComments: false +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: Never + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: false + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakInheritanceList: AfterColon +BreakBeforeTernaryOperators: false +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: AfterColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 100 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 8 +ContinuationIndentWidth: 8 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +ForEachMacros: + - YVE_ITERATE + - YVE_VFS_ITERATE + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + - Regex: '.*' + Priority: 1 + SortPriority: 0 +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentAccessModifiers: true +IndentCaseBlocks: false +IndentCaseLabels: true +IndentGotoLabels: true +IndentPPDirectives: BeforeHash +IndentExternBlock: AfterExternBlock +IndentWidth: 8 +IndentWrappedFunctionNames: false +InsertTrailingCommas: None +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: 'BTM_LIST_BEGIN|CMX_MASK_ITER_BEGIN' +MacroBlockEnd: 'BTM_LIST_END|CMX_MASK_ITER_END' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 8 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: false +PackConstructorInitializers: NextLine +PenaltyBreakAssignment: 110 +PenaltyBreakBeforeFirstCallParameter: 100 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 74 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Right +ReferenceAlignment: Right +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: Custom +SpaceBeforeParensOptions: + AfterControlStatements: true + AfterForeachMacros: true + AfterFunctionDefinitionName: false +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: c++11 +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Always +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE +... diff --git a/tools/editing/pycodestyle.cfg b/tools/editing/pycodestyle.cfg new file mode 100644 index 0000000..f653896 --- /dev/null +++ b/tools/editing/pycodestyle.cfg @@ -0,0 +1,5 @@ +[pycodestyle] +max-line-length = 150 +exclude = src/ratapi/ratapi/migrations/versions +statistics = True +ignore = E121, E123, E126, E133, E226, E241, E242, E704, W503, W504, W505, E712 \ No newline at end of file diff --git a/tools/geo_converters/Dockerfile b/tools/geo_converters/Dockerfile new file mode 100644 index 0000000..1628f75 --- /dev/null +++ b/tools/geo_converters/Dockerfile @@ -0,0 +1,28 @@ +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# This Dockerfile contains various scripts that use GDAL library and utilities. +# It might be useful in environments where DDAL can't be properly installed, +# otherwise there are no reasons to run those scripts outside the container. + + + +FROM osgeo/gdal:alpine-normal-3.6.3 + +RUN apk add py3-pip +RUN pip install pyaml jsonschema +WORKDIR /usr/app +COPY geoutils.py /usr/app/ +COPY dir_md5.py g8l_info_schema.json /usr/app/ +COPY nlcd_wgs84.py nlcd_wgs84.yaml /usr/app/ +COPY tiler.py to_wgs84.py lidar_merge.py to_png.py /usr/app/ +COPY make_population_db.py /usr/app/ + +# If container is created on Windows 'chmod' is necessary. No harm on *nix +RUN chmod a+x /usr/app/*.py +ENV PATH=$PATH:/usr/app + +CMD sh diff --git a/tools/geo_converters/README.md b/tools/geo_converters/README.md new file mode 100644 index 0000000..2723dcb --- /dev/null +++ b/tools/geo_converters/README.md @@ -0,0 +1,792 @@ +Copyright (C) 2022 Broadcom. All rights reserved.\ +The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate that +owns the software below. This work is licensed under the OpenAFC Project +License, a copy of which is included with this software program. + +# Geodetic File Converter Scripts + +## Table of Contents + +- [General considerations](#general_considerations) + - [Running scripts: standalone or docker (dependencies)?](#docker) + - [GDAL version selection](#gdal) + - [Performance (`--threads`, `--nice`, conversion order)](#performance) + - [Ctrl-C](#ctrlc) + - [Restarting (vs `--overwrite`)](#restarting) + - [Pixel size (`--pixel_size`, `--pixels_per_degree`, `--round_pixels_to_degree`)](#pixel_size_alignment) + - [Cropping/realigning (`--top`, `--bottom`, `--left`, `--right`, `--round_pixels_to_degree`)](#crop_realign) + - [Geoids (`--src_geoid`, `--dst_geoid`, order of operations)](#geoids) + - [Resampling (`--resampling`)](#resampling) + - [GeoTiff format options (`--format_param`)](#geotiff_format) + - [PNG data representation (`--scale`, `--no_data`, `--data_type`)](#png_representation) +- [Scripts](#scripts) + - [*dir_md5.py* - computing MD5 hash over geodetic file directory](#dir_md5) + - [*nlcd_wgs84.py* - converting land usage data to AFC-compatible format](#nlcd_wgs84) + - [*lidar_merge.py* - flattens lidar files](#lidar_merge) + - [*to_wgs84.py* - change coordinate system](#to_wgs84) + - [*to_png.py* - converts files to PNG format](#to_png) + - [*tiler.py* - cuts source files to 1x1 degree tiles](#tiler) + - [*make_population_db.py* - converts population density image to SQLite for load test](#make_population_db) +- [Conversion routines](#conversion_routines) + - [Geoids](#geoid_rouitines) + - [USA geoids](#usa_geoids_routines) + - [Canada geoid](#canada_geoid_procedure) + - [World geoids](#world_geoids_routines) + - [USA/Canada/Mexico 3DEP terrain model](#3dep_routine) + - [Coarse SRTM/GLOBE world terrain](#coarse_terrain_routine) + - [High resolution terrain data (aka LiDAR)](#lidar_routine) + - [Canada CDSM surface model](#cdsm_routine) + - [Land usage files](#land_usage_routines) + - [NLCD land usage files](#nlcd_routine) + - [NOOA land usage files](#noaa_routine) + - [Canada land usage files](#canada_land_usage_routine) + - [Corine land cover files](#corine_land_cover_files) +- [*proc_gdal* - creating LiDAR files](#proc_gdal) + +## General considerations + +This chapter describes characteristics of all scripts. + +### Running scripts: standalone vs Docker (dependencies) + +All Geodetic File Converter scripts described here may be run standalone - and examples for doing this will be provided. + +However some of them require GDAL - sometimes GDAL utilities, sometimes Python GDAL libraries. GDAL utilities are hard to install. GDAL Python bindings even harder to install. So, while running standalone is more convenient, Docker may need to be resorted to (Dockerfile is provided). + +Here are scripts' dependencies that require separate installation: + +|Script|GDAL utilities?|GDAL Python bindings?|Other dependencies| +|------|---------------|---------------------|-------------------| +|dir_md5.py|No|No|jsonschema(optional)| +|lidar_merge.py|Yes|No|| +|nlcd_wgs84.py|Yes|For non-NLCD sources|pyyaml| +|tiler.py|Yes|No|| +|to_png.py|Yes|No|| +|to_wgs84.py|Yes|No|| + +Since files operated upon are located 'in the outer world' (outside the container's file system) proper use of mapping (`-v`, `--user` and `--group_add` in `docker run` command line), absolute paths etc. is necessary (refer Docker manuals/gurus for proper instruction). + +For example, the `to_wgs84.py`(see more detail later) utility can be run with: + +``` +docker run --rm -it --user 16256:16256 --group-add 16256 --group-add 20 --group-add 970 \ + --group-add 1426 --group-add 3167 --group-add 3179 --group-add 3186 --group-add 3199 \ + --group-add 55669 --group-add 61637 -v /home/fs936724:/files \ + geo_converters --resampling cubic to_wgs84.py /files/a.tif /files/b.tif +``` + +### GDAL version selection + +GDAL (set of libraries and utilities that do the job in almost all scripts here) is constantly improving - both in capabilities and in reliability, So it makes sense to use as late (stable) version as possible. As of time of this writing the latest version is 3.6.3. + +GDAL version may be checked with `gdalinfo --version` + +### Performance (`--threads`, `--nice`, conversion order) + +Geodetic data conversion (performed mainly by means of `gdal_transform` and `gdalwarp` utilities) is a slow process, so all scripts presented here support parallelization. It is controlled by `--threads` parameter that has following forms: + +|Form|Action| +|----|------| +|| Default (no `--threads` parameter) - use all CPUs| +|--threads **N**|Use N CPUs| +|--threads -**N**|Use all CPUs but N. E.g. if there are 8 CPUs `--threads -2` will take 6 (8-2) CPUs| +|--threads **N**%| Use N% of CPUs. E.g. if there are 8 CPUs `--threads 25%` will use 2 CPUs| + +Note that `gdal_transform` and `gdalwarp` on large files use a lot of memory, so using too much CPUs will cause thrashing (excessive swapping). So it may make sense to limit the number of CPUs. + +All scripts have `--nice` parameter to lower its priority (and thus do not impede user-interacting processes). On Windows it only works if 'psutil' Python module is installed. + +`gdalwarp` (used by `to_wgs84.py` and `nlcd_wgs84.py`) works especially slow on large (e.g. 20GB) files. Hence LiDAR conversion process first slices source data to tiles then runs `to_wgs84.py` to apply geoids. This is not always possible though, for example, `nlcd_wgs84.py` operates on monolithic files for CONUS and Canada and there is nothing that can be done to improve speed. + + +### Ctrl-C + +Python multithreading has an eternal bug of Ctrl-C mishandling. There is a hack that fixes it on *nix platforms (used in all scripts presented here), but unfortunately there is no remedy for Windows. + +Hence in *nix (including WSL, PuTTY, etc.) one can terminate scripts presented here with Ctrl-C, whereas on Windows one can only stop script by, say, closing terminal window. + +### Restarting (vs `--overwrite`) + +`tiler.py` always produces and `lidar_merge.py`, `to_wgs84.py`, `to_png.py` may produce multiple output files. If some of these files already exist they just skip them. This makes the conversion process restartable - when started next time process will pick from where it left off (excluding half-done files - they'll be redone). + +If this behavior is undesirable (e.g. if conversion parameter or source data has changed), there is an `--overwrite` switch that overwrite already existing files. + +### Pixel size (`--pixel_size`, `--pixels_per_degree`, `--round_pixels_to_degree`) + +Raster geospatial files (those that contain heights, land usage, population density, etc.) are, essentially image files (usually TIFF or PNG) with some metadata. Geospatial data is encoded as pixel values (data stored in pixel may be signed/unsigned int8/16/32/64, float 32/64, complex int/float of those sizes). Pixel size (distance between adjacent pixels) and alignment on degree mesh might be essential. + +By default GDAL keeps pixel size and alignment during conversions - and it works well in many cases. In some cases however (e.g. when converting from MAP data - as in case of land usage source files or when pixels are slightly off their intended positions) pixel size and alignment should be specified/corrected explicitly (this is named resampling - more on it later). Here are command line parameters for setting/changing pixel size: + +|Parameter|Meaning| +|---------|-------| +|--pixel_size **SIZE**|Pixel size in degrees. E.g. `--pixel_size 0.00001` means 100000 pixels per degree| +|--pixels_per_degree **N**|Number of pixels per degree. E.g. `--pixels_per_degree 3600` means 3600 pixels per degree (1 pixel per arcsecond)| +|--round_pixels_to_degree|Take existing pixel sizes and round it to nearest whole number of pixels per degree. E.g. if pixel size is 0.008333333333000, it will be rounded to 0.008333333333333| + +### Cropping/realigning (`--top`, `--bottom`, `--left`, `--right`, `--round_pixels_to_degree`) + +As of time of this writing some geospatial data (specifically - land usage) is represented by a large files that cover some region (e.g. CONUS, Canada, Alaska, Hawaii, etc.). Problems may occur if such files intersect (as CONUS and uncropped Alaska do, despite no CONUS data on Alaska file). + +While large geospatial files are being gradually phased out (in favor of 1X1 degree tile files), as an intermediate solution it is possible to crop source file(s) to avoid intersection. This may be made with `--top MAXLAT`, `--bottom MINLAT`, `--left MINLON`, `--right MAXLON` parameters that set a crop values (MINLAT/MAXLAT are signed, north-positive degrees, MINLON/MAXLON are signed east-positive degrees). + +It is not necessary to set all of them - some boundaries may be left as is. + +Also pixels in file may be not aligned on degree mesh. While it can't be fixed directly, there is a `--round_pixels_to_degree` option that extends boundaries to the next whole degree. Resulting pixels are degree-aligned on the left/top boundaries, alignment on other boundaries may be achieved by setting pixel size (see chapter on pixel size parameters above). + +### Geoids (`--src_geoid`, `--dst_geoid`, order of operations) + +Geoid is a surface, perpendicular to vertical (lead line). Calm water surface is an example of geoid. There are infinitely many geoids, but there is just one geoid that goes through a particular point in space. For USA and Canada such point in space is Father Point, in Rimouski, Canada, so their geoids are compatible. Yet Earth shape changes, surveying techniques improve and so each country has many releases of its geoid models (GEOID96, GEOID99, ... GEOID12B, GEOID18 for USA, HTv2 1997, 2002, 2010). + +Geoids are GDAL files (usually with .gtx extension, albeit .bin and .byn extensions also happen). Contiguous countries (like Canada) usually have single-file geoid models covers it all, while noncontiguous countries (like USA) have multifile geoid models that cover its various parts. + +The AFC Engine expects all heights (in particular - terrain and building heights) to be expressed relative to the WGS84 ellipsoid. There is a perfect sense in it - it makes computation simple and closed (not depending on any external parameters). However, all raw terrain data contains the geoidal heights (heights relative to geoid). + +To reconcile these differences, the `to_wgs84.py` script converts heights from geoidal to WGS84 by specifying the geoid model for source data with `--src_geoid` parameter. Since geoid models may be multifile, its name may be a filename of mask, e.g. `--src_geoid 'g2018?0.gtx'`. Note the quotes around name - they prevent the shell from globbing it. + +If many files are converted, different files may need to have different geoid models, so `--src_geoid` switch may be used several times e.g.: + +``` +--src_geoid 'g2018?0.gtx' --src_geoid 'g2012b?0.gtx' --src_geoid HT2_2010v70.gtx +``` + +For North America data, where the GEOID18 model is preferred over CONUS, GEOID12B wherever it doesn't cover (e.g. Hawaii), and HTv2 for Canada. + + +Important performance observation. If `to_wgs84.py` source files are not in WGS84 geodetic (latitude/longitude) coordinate system - it is better to **first convert them into WGS84 geodetic coordinate system (with `to_wgs84.py`) and then, with a separate run of `to_wgs84.py ... --src_geoid ...` convert heights to WGS84**. Doing both conversion in one swoop involves geoid backprojection which is either impossible (which manifests itself with funny error messages) or extremely (hundreds of times) slower. + +### Resampling (`--resampling`) + +Changing pixel sizes, cropping, applying geoids, changing of coordinate system changes pixel values in geospatial file so that resulting values are somehow computed from source values of 'surrounding pixels'. + +This process is named **resampling** and it is may be performed per many various methods (`nearest`, `bilinear`, `cubic`, `cubicspline`, `lanczos`, `average`, `rms`, `mode`, `max`, `min`, `med`, `Q1`, `Q3`, etc.). + +The resampling method used is be specified by `--resampling METHOD` parameter. + +By default for byte source or destination data (e.g. land usage) `nearest` method is used, for all other cases `cubic` method is used. + +### GeoTiff format options (`--format_param`) + +Scripts that generate geospatial files (i.e. all but `dir_md5.py`) have `--format_param` option to specify nonstandard file generation parameters. Several such parameters may be specified. + +GeoTiff files (files with .tif, .hgt, .bil extensions) are big. Sometimes too big for default options. Here are some useful options for GeoTiff file generation: + +|Option|Meaning| +|------|-------| +|--format_param COMPRESS=PACKBITS|Makes land usage files 10 time smaller without performance penalty. Also useful on terrain files with lots of NoData or sea| +|--format_param COMPRESS=LZW|Works well on terrain files (compress ratio 2-3 times), but slows things down. Might be a good tradeoff when working with large LiDAR files on tight disk space| +|--format_param COMPRESS=ZSTD|Works better and faster than LZW. Reportedly not universally available| +|--format_param BIGTIFF=YES|Sometimes conversions fail if the resulting file is too big (in this case error message appeals to use BIGTIFF=YES), this parameter enables BigTiff mode| +|--format_param BIGTIFF=IF_NEEDED|Enabled BigTiff mode if file expected to require it. Doesn't work well with compression| + +### PNG data representation (`--scale`, `--no_data`, `--data_type`) + +PNG pixel may only contain 8 or 16 bit unsigned integers. Former used for e.g. land usage codes, latter - for terrain heights. Source terrain files however contain integer or floating point heights in meters. Latter need to be somehow mapped into former. + +Also terrain files contain a special value of 'NoData` (as a rule - somewhere far from range of valid values), that also need to be properly mapped to PNG data value range. + +PNG-generation scripts (`to_png.py`, `tiler.py`) provide the following options to control the process: + +|Option|Meaning| +|------|-------| +|--data_type **DATA_TYPE**|Target data type. For PNG: `Byte` or `UInt16`| +|--scale **SRC_MIN SRC_MAX DST_MIN DST_MAX**|Maps source range of [SRC_MIN, SRC_MAX] to target (PNG) range [DST_MIN, DST_MAX]. By default for *integer source* and unsigned 16-bit target (PNG data) `--scale -1000 0 0 1000` is assumed (leaving 1 m source resolution, but shifting data range), whereas for *floating point source* and unsigned 16-bit target (PNG data) `--scale -1000 0 0 5000` is assumed (20cm resolution, shifting data range)| +|--no_data **NO_DATA**|NoData value to use. By default for unsigned 16-bit target (PNG data) 65535 is used| + + +## Scripts + +## *dir_md5.py* - computing MD5 hash over geodetic file directory + +Groups of geospatial file (same data, same purpose, same region) have (or at least - should have) `..._version_info.json` files containing identification and provenance of these file groups. These `..._version_info.json` files have `md5` field containing MD5 of files belonging to group. + +`dir_md5.py` computes MD5 of given group of files. It may also update `..._version_info.json` file of this group or verify MD5 in file against actual MD5. Also it verifies the correctness of `..._version_info.json` against its schema (stored in `g8l_info_schema.json`). + +MD5 of files in group computed in filenames' lexicographical order. Names themselves are not included into MD5. + +Usually this utility works with ~0.5 GB/sec speed. + +`$ dir_md5.py subcommand [options] [FILES_AND_DIRS]` + +`FILES_AND_DIRS` explicitly specify files over which to compute MD5 (wildcards are allowed), may be used in `list` and `compute` subcommands. Yet it is recommended to use `..._version_info.json` files to specify file groups. + +Subcommands: + +|Subcommand|Function| +|----------|--------| +|list|Do not compute MD5, just list files that will be used in computation in order they'll be used| +|compute|Compute and print MD5| +|verify|Compute MD5 and compare it with one, stored in `..._version_info.json` file| +|update|Compute MD5 and write it to `..._version_info.json` file| +|help|Print help on a particular subcommand| + + +Options: + +|Option|Function| +|------|--------| +|--json **JSON_FILE**|JSON file containing description of file group(s). Since it is located in the same directory as files, its name implicitly defines their location| +|--mask **WILDCARD**|Explicitly specified file group. This option may be defined several times. Note that order of files in MD5 defined by filenames, not by order of how they were listed| +|--recursive|Also include files in subdirectories (may be used for LiDAR files)| +|--progress|Print progress information| +|--stats|Print statistics| +|--threads [-]**N**[%]|How many CPUs to use (if positive), leave unused (if negative) or percent of CPUs (if followed by %)| +|--nice|Lower priority (on Windows required `psutil` Python module)| + +### *nlcd_wgs84.py* - converting land usage data to AFC-compatible format + +AFC Engine employs land usage data (essentially - urban/rural classification) in form of NLCD files. NLCD files used by AFC are in WGS84 geodetic (latitude/longitude) coordinate system. + +AFC Engine expects land usage files to have WGS84 geodetic (latitude/longitude) coordinate system. It expects NLCD land usage codes to be used (see *nlcd_wgs84.yaml* for more information). + +Source data for these files released in different forms: +* NLCD files as such - they have map projection (not geodetic) coordinate system. + +* NOAA files - contain (as of time of this writing) more recent and detailed land usage data for Hawaii, Puerto Rico, Virgin Islands. These files also have map projection (not geodetic) coordinate system, also they have different land usage codes (that need to be translated to NLCD land usage codes). + +* Canada land usage file - covers entire Canada, uses map projection coordinate system and another land usage codes. + +* Corine land cover files - covers the EU, uses map projection coordinate system and another set of land usage codes. + + +`nlcd_wgs84.py` script converts NLCD data files from the source format to one, used by AFC Engine. + +There is a *nlcd_wgs84.yaml* file that accompanies this script. It defines NLCD encoding properties (code meaning and colors) and other encodings (code meaning and translation to NLCD). Presence of this file in same directory as `nlcd_wgs84.py` is mandatory. + +`nlcd_wgs84.py [options] SOURCE_FILE DEST_FILE` + +Options: + +|Option|Function| +|------|--------| +|--pixel_size **DEGREES**|Pixel size of resulting file in degrees| +|--pixels_per_degree **NUMBER**|Pixel size of resulting file in form of number of pixels per degree. As of time of this writing *3600* recommended for both NLCD and NOAA source data. If pixel size not specified in any way, `gdalwarp` utility will decide - this is not recommended| +|--top **MAX_LAT**|Optional upper crop boundary. **MAX_LAT** is north-positive latitude in degrees| +|--bottom **MIN_LAT**|Optional lower crop boundary. **MIN_LAT** is north-positive latitude in degrees| +|--left **MIN_LON**|Optional left crop boundary. **MIN_LON** is east-positive longitude in degrees| +|--right **MAX_LON**|Optional right crop boundary. **MAX_LON** is east-positive longitude in degrees| +|--encoding **ENCODING**|Translate land codes from given encoding (as of time of this writing - 'noaa', 'canada', or 'corine'). All encodings defined in *nlcd_wgs84.yaml* file| +|--format **FORMAT**|Output file format (short) name. By default guessed from output file extension. See [GDAL Raster Drivers](https://gdal.org/drivers/raster/index.html) for more information| +|--format_param **NAME=VALUE**|Set output format option. See [GDAL Raster Drivers](https://gdal.org/drivers/raster/index.html) for more information| +|--resampling **METHOD**|Resampling method to use. Default for land usage is 'nearest'. See [gdalwarp -r](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r) for more details| +|--threads [-]**N**[%]|How many CPUs to use (if positive), leave unused (if negative) or percent of CPUs (if followed by %)| +|--nice|Lower priority (on Windows required `psutil` Python module)| +|--overwrite|Overwrite target file if it exists| + +### *lidar_merge.py* - flattens lidar files + +_**Note:** this script represents work still in progress. Not currently used for production OpenAFC._ + +LiDAR files are high-resolution (one meter for USA) surface models that contain both terrain and building data. Their original representation is a complicated multilevel directory structure (which is a product of `proc_lidar` script). + +`lidar_merge.py` converts mentioned multilevel structure into flat geospatial files (one file per agglomeration). + +`lidar_merge.py [options] SRC_DIR DST_DIR` + +Here `SRC_DIR` is a root of multilevel structure (contains per-agglomeration .csv files), `DST_DIR` is a directory for resulting flat files. Options are: + +|Option|Function| +|------|--------| +|--overwrite|Overwrite already existing resulting files. By default already existing resulting files considered to be completed, thus facilitating process restartability| +|--out_ext **EXT**|Extension of output files. By default same as input terrain files' extension (i.e. *.tif*)| +|--locality **LOCALITY**|Do conversion only for given locality/localities (parameter may be specified several times). **LOCALITY** is base name of correspondent .csv file (e.g. *San_Francisco_CA*)| +|--verbose|Do conversion one locality at a time with immediate print of utilities' output. Slow. For debug purposes| +|--format **FORMAT**|Output file format (short) name. By default guessed from output file extension. See [GDAL Raster Drivers](https://gdal.org/drivers/raster/index.html) for more information| +|--format_param **NAME=VALUE**|Set output format option. See [GDAL Raster Drivers](https://gdal.org/drivers/raster/index.html) for more information| +|--resampling **METHOD**|Resampling method to use. See [gdalwarp -r](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r) for more details. Default is 'cubic'| +|--threads [-]**N**[%]|How many CPUs to use (if positive), leave unused (if negative) or percent of CPUs (if followed by %)| +|--nice|Lower priority (on Windows required `psutil` Python module)| + +### *to_wgs84.py* - change coordinate system + +All terrain source files have heights relative to geoids - whereas AFC Engine expects heights to be relative to WGS84 ellipsoid. + +All land usage and some terrain source files are in map projection coordinate system whereas AFC Engine expects them to be in geodetic (latitude/longitude) coordinate system. + +Some source files are in geodetic coordinate system that is not WGS84. + +`to_wgs84.py` script converts source files to ones with WGS84 horizontal (latitude/longitude) and vertical (heights relative to WGS84 ellipsoid - where applicable) coordinate systems. In certain cases it may convert both vertical and horizontal coordinate systems in one swoop, but this is **not recommended** as performance degrades dramatically. Instead it is recommended to fix horizontal coordinate system (if needed) first and then vertical (if needed). + +`to_wgs84.py [options] FILES` + +Here `FILES` may be: +- `SRC_FILE DST_FILE` pair - if `--out_dir` option not specified +- One or more filenames that may include wildcards - if `--out_dir` specified, but `--recursive` not specified +- One or more `BASE_DIR/*.EXT` specifiers - if both `--out_dir` and `--recursive` not specified. In this case files are being looked up in subdirectories and structure of these subdirectories is copied to output directory. + +|Option|Function| +|------|--------| +|--pixel_size **DEGREES**|Pixel size of resulting file in degrees| +|--pixels_per_degree **NUMBER**|Pixel size of resulting file in form of number of pixels per degree| +|--round_pixels_to_degree|Round current pixel size to whole number of pixels per degree. Latitudinal and longitudinal sizes rounded independently| +|--top **MAX_LAT**|Optional upper crop boundary. **MAX_LAT** is north-positive latitude in degrees| +|--bottom **MIN_LAT**|Optional lower crop boundary. **MIN_LAT** is north-positive latitude in degrees| +|--left **MIN_LON**|Optional left crop boundary. **MIN_LON** is east-positive longitude in degrees| +|--right **MAX_LON**|Optional right crop boundary. **MAX_LON** is east-positive longitude in degrees| +|--round_boundaries_to_degree|Extend boundaries to next whole degree outward. This (along with `pixels_per_degree` or `--round_pixels_to_degree`) makes pixel mesh nicely aligned to degrees, that help create nicer tiles afterwards| +|--src_geoid **GEOID_PATTERN**|Assume source file has geoidal heights, relative to given geoid. **GEOID_PATTERN** is geoid file name(s) - if geoid consists of several files (e.g. US geoids) **GEOID_PATTERN** may contain wildcards (to handle geoids consisting of several files - like US geoids). This option may be specified several times (for source file set that covers large spaces)| +|--dst_geoid **GEOID_PATTERN**|Make resulting file contain geoidal heights, relative to given geoid (e.g. to test compatibility with other AFC implementations). This option may be specified several times, **GEOID_PATTERN** may contain wildcards - just like for `--src_geoid`| +|--extend_geoid_coverage **AMOUNT**|Artificially extend geoid file coverage by given **AMOUNT** (specified in degrees) when testing which geoid file covers file being converted. Useful when source files have margins| +|--format **FORMAT**|Output file format (short) name. By default guessed from output file extension. See [GDAL Raster Drivers](https://gdal.org/drivers/raster/index.html) for more information| +|--format_param **NAME=VALUE**|Set output format option. See [GDAL Raster Drivers](https://gdal.org/drivers/raster/index.html) for more information| +|--resampling **METHOD**|Resampling method to use. See [gdalwarp -r](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r) for more details. Default is 'nearest' for byte data (e.g. land usage), 'cubic' otherwise| +|--out_dir **DIRECTORY**|Do the mass conversion to given directory. In this case `FILES` in command line is a list of files to convert| +|--out_ext **EXT**|Extension of resulting files. May be used in mass conversion. By default source files' extension is kept| +|--keep_ext **EXT**|Don't remove generated files with given extension. Useful for file formats that involve files of several extensions (e.g. EHdr that involves .bil, .hdr and .prj). This parameter may be specified several times| +|--overwrite|Overwrite already existing resulting files. By default already existing resulting files considered to be completed, thus facilitating process restartability| +|--remove_src|Remove source files after successful conversion. May help save disk space (e.g. when dealing with huge LiDAR files)| +|--threads [-]**N**[%]|How many CPUs to use (if positive), leave unused (if negative) or percent of CPUs (if followed by %)| +|--nice|Lower priority (on Windows required `psutil` Python module)| + +### *to_png.py* - converts files to PNG format + +_**Note:** this script represents work still in progress. Not currently used for production OpenAFC._ + +Convert source files to PNG. PNG is special as it may only contain 1 or 2 byte unsigned integer pixel data. To represent heights (that may be negative and sub-meter) it requires offsetting and scaling. + +Also for some strange reason PNG files contain very limited metadata information (what coordinate system used, etc.), so they should only be used as terminal files in any sentence of conversions. + +`to_png.py` script converts source data files to PNG format. + +`to_png.py [options] FILES` + +Here `FILES` may be pair `SRC_FILE DST_FILE` or, if `--out_dir` option specified, `FILES` are source files (in any number, may include wildcards). + +|Option|Function| +|------|--------| +|--pixel_size **DEGREES**|Pixel size of resulting file in degrees| +|--pixels_per_degree **NUMBER**|Pixel size of resulting file in form of number of pixels per degree| +|--round_pixels_to_degree|Round current pixel size to whole number of pixels per degree. Latitudinal and longitudinal sizes rounded independently| +|--top **MAX_LAT**|Optional upper crop boundary. **MAX_LAT** is north-positive latitude in degrees| +|--bottom **MIN_LAT**|Optional lower crop boundary. **MIN_LAT** is north-positive latitude in degrees| +|--left **MIN_LON**|Optional left crop boundary. **MIN_LON** is east-positive longitude in degrees| +|--right **MAX_LON**|Optional right crop boundary. **MAX_LON** is east-positive longitude in degrees| +|--round_boundaries_to_degree|Extend boundaries to next whole degree outward. This (along with `pixels_per_degree` or `--round_pixels_to_degree`) makes pixel mesh nicely aligned to degrees, that help create nicer tiles afterwards| +|--no_data **VALUE**|Value to use as NoData. By default - same as in source for 1-byte pixels (e.g. land usage, 65535 for 2-byte pixels| +|--scale **SRC_MIN SRC MAX DST_MIN DST_MAX**|Maps source data to destination in a way tat [**SRC_MIN**, **SRC_MAX**] interval maps to [**DST_MIN**, **DST_MAX**]. Default is identical no mapping for 1-byte target data, -1000 0 0 1000 for 2-byte target and integer source data (keeping 1 m resolution), -1000 0 0 5000 for 2-byte target and floating point source data (20cm resolution)| +|--data_type **DATA_TYPE**|Pixel data type: 'Byte' or 'UInt16'| +|--format **FORMAT**|Output file format (short) name. By default guessed from output file extension. See [GDAL Raster Drivers](https://gdal.org/drivers/raster/index.html) for more information| +|--format_param **NAME=VALUE**|Set output format option. See [GDAL Raster Drivers](https://gdal.org/drivers/raster/index.html) for more information| +|--resampling **METHOD**|Resampling method to use. See [gdalwarp -r](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r) for more details. Default is 'nearest' for byte data (e.g. land usage), 'cubic' otherwise| +|--out_dir **DIRECTORY**|Do the mass conversion to given directory. In this case `FILES` in command line is a list of files to convert| +|--recursive|Look in subdirectories of given source file parameters and copy subdirectory structure to output directory (that must be specified)| +|--out_ext **EXT**|Extension of resulting files. May be used in mass conversion. By default source files' extension is kept| +|--overwrite|Overwrite already existing resulting files. By default already existing resulting files considered to be completed, thus facilitating process restartability| +|--remove_src|Remove source files after successful conversion. May help save disk space (e.g. when dealing with huge LiDAR files)| +|--threads [-]**N**[%]|How many CPUs to use (if positive), leave unused (if negative) or percent of CPUs (if followed by %)| +|--nice|Lower priority (on Windows required `psutil` Python module)| + +### *tiler.py* - cuts source files to 1x1 degree tiles + +Ultimately all geospatial files should be tiled to 1x1 degree files (actually, slightly wider, as tiles may have outside 'margins' several pixel wide). Source files for tiling may be many and they may overlap, in overlap zones some source files are preferable with respect to other. + +Some resulting tiles, filled only with NoData or some other ignored values should be dropped. + +Tile files should be named according to latitude and longitude of their location. + +`tiler.py` script cuts source files to tiles. + +It names tile files according to given pattern that contains *{VALUE[:FORMAT]}* inserts. Here *FORMAT* is integer format specifier (e.g. 03 - 3 character wide with zero padding). Following values for *VALUE* are accepted: + +|Value|Meaning| +|-----|-------| +|lat_hem|Lowercase tile latitude hemisphere (**n** or **s**)| +|LAT_HEM|Uppercase tile latitude hemisphere (**N** or **S**)| +|lat_u|Latitude of tile top| +|lat_l|Latitude of tile bottom| +|lon_hem|Lowercase tile longitude hemisphere (**e** or **w**)| +|LON_HEM|Uppercase tile longitude hemisphere (**E** or **W**)| +|lon_u|Longitude of tile left| +|lon_l|Longitude of tile right| + +E.g. standard 3DEP file names have the following pattern: *USGS_1_{lat_hem}{lat_u}{lon_hem}{lon_l}.tif* + +`tiler.py [options] FILES` + +Here `FILES` specify names of source files that need to be tiled (filenames may contain wildcards). Files specified in order of preference - preferable come first. + +|Option|Function| +|------|--------| +|--tile_pattern **PATTERN**|Pattern for tile file name. May include directory. See above in this chapter| +|--remove_value **VALUE**|Drop tiles that contain only this value and NoData value. This option may be specified several times, however only 'monochrome' tiles are dropped| +|--pixel_size **DEGREES**|Pixel size of resulting file in degrees| +|--pixels_per_degree **NUMBER**|Pixel size of resulting file in form of number of pixels per degree| +|--round_pixels_to_degree|Round current pixel size to whole number of pixels per degree. Latitudinal and longitudinal sizes rounded independently| +|--top **MAX_LAT**|Optional upper crop boundary. **MAX_LAT** is north-positive latitude in degrees| +|--bottom **MIN_LAT**|Optional lower crop boundary. **MIN_LAT** is north-positive latitude in degrees| +|--left **MIN_LON**|Optional left crop boundary. **MIN_LON** is east-positive longitude in degrees| +|--right **MAX_LON**|Optional right crop boundary. **MAX_LON** is east-positive longitude in degrees| +|--margin **NUM_PIXELS**|Makes outer margin **NUM_PIXELS** wide around created tiles| +|--no_data **VALUE**|Value to use as NoData. By default - same as in source for 1-byte pixels (e.g. land usage, 65535 for 2-byte pixels| +|--scale **SRC_MIN SRC MAX DST_MIN DST_MAX**|Maps source data to destination in a way that [**SRC_MIN**, **SRC_MAX**] interval maps to [**DST_MIN**, **DST_MAX**]. Default is identical no mapping for 1-byte target data, -1000 0 0 1000 for 2-byte target and integer source data (keeping 1 m resolution), -1000 0 0 5000 for 2-byte target and floating point source data (20cm resolution)| +|--data_type **DATA_TYPE**|Pixel data type. See [gdal_translate -ot](https://gdal.org/programs/gdal_translate.html#cmdoption-gdal_translate-ot) fro more details|| +|--format **FORMAT**|Output file format (short) name. By default guessed from output file extension. See [GDAL Raster Drivers](https://gdal.org/drivers/raster/index.html) for more information| +|--format_param **NAME=VALUE**|Set output format option. See [GDAL Raster Drivers](https://gdal.org/drivers/raster/index.html) for more information| +|--resampling **METHOD**|Resampling method to use. See [gdalwarp -r](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r) for more details. Default is 'nearest' for byte data (e.g. land usage), 'cubic' otherwise| +|--out_dir **DIRECTORY**|Do the mass conversion to given directory. In this case `FILES` in command line is a list of files to convert| +|--overwrite|Overwrite already existing resulting files. By default already existing resulting files considered to be completed, thus facilitating process restartability| +|--verbose|Create one tile at a time, printing all output in real time. Slow. For debug purposes| +|--threads [-]**N**[%]|How many CPUs to use (if positive), leave unused (if negative) or percent of CPUs (if followed by %)| +|--nice|Lower priority (on Windows required `psutil` Python module)| + +### *make_population_db.py* - converts population density image to SQLite for load test + +Prepares population database for `tools/load_test/afc_load_test.py`. Fully documented in `tools/load_test/README.md`; however, the docker container here (which contains the appropriate GDAL bindings) should be used to run this script. + +## Conversion routines + +For sake of simplicity, all examples will be in non-Docker form (i.e. assumes either being run inside the docker container, or the relevant GDAL modules are available). Also for sake of simplicity `--threads` and `--nice` will be omitted. + +All arguments containing wildcard, etc. are in single quotes. This is not necessary for positional parameters in non-Docker operation, yet for Docker operation it is always necessary, so it is done to simplify switching to Docker. Feel free to omit them for positional parameters. + + +### Geoids + +#### USA geoids + +The most recent USA geoid is GEOID18, it only covers CONUS and Puerto Rico. Previous geoid is GEOID12B, it covers CONUS, Alaska, Guam, Hawaii, Guam, Hawaii, Puerto Rico. + +1. *GEOID18* + - Download from [Geoid 18 Data](https://vdatum.noaa.gov/download.php) as [NGS data (.bin) zip](https://www.myfloridagps.com/Geoid/NGS.zip) + - CONUS: `gdal_translate -of GTX g2018u0.bin usa_ge_prd_g2018u0.gtx` + - Puerto Rico: `gdal_translate -of GTX g2018p0.bin usa_ge_prd_g2018p0.gtx` +2. *GEOID12B* + - Download from [NOAA/NOS's VDatum: Download VDatum](https://vdatum.noaa.gov/download.php) as [GEOID12B](https://vdatum.noaa.gov/download/data/vdatum_GEOID12B.zip). + - vdatum/core/geoid12b/g2012ba0.gtx for Alaska -> usa_ge_prd_g2012ba0.gtx + - vdatum/core/geoid12b/g2012bg0.gtx for Guam -> usa_ge_prd_g2012bg0.gtx + - vdatum/core/geoid12b/g2012bh0.gtx for Hawaii -> usa_ge_prd_g2012bh0.gtx + - vdatum/core/geoid12b/g2012bp0.gtx for Puerto Rico -> usa_ge_prd_g2012bp0.gtx + - vdatum/core/geoid12b/g2012bs0.gtx for Samoa -> usa_ge_prd_g2012bs0.gtx + - vdatum/core/geoid12b/g2012bu0.gtx for CONUS. -> usa_ge_prd_g2012bu0.gtx + +#### Canada geoid + +Most recent Canada geoid is HTv2, Epoch 2010. + +- Download from [Geoid Models](https://webapp.csrs-scrs.nrcan-rncan.gc.ca/geod/data-donnees/geoid.php?locale=en) (registration required) as [HTv2.0, GeoTIFF, 2010](https://webapp.csrs-scrs.nrcan-rncan.gc.ca/geod/process/download-helper.php?file_id=HT2_2010_tif) +- Convert to .gtx: `gdal_translate -of GTX HT2_2010v70.tif can_ge_prd_HT2_2010v70.gtx` + +#### World geoids + +EGM1996 (aka EGM96) is rather small (low resolution), EGM2008 (aka EGM08) is rather large (~1GB). Use whatever you prefer. + +1. *EGM1996* + - Download from [NOAA/NOS's VDatum: Download VDatum](https://vdatum.noaa.gov/download.php) as [EGM1996](https://vdatum.noaa.gov/download/data/vdatum_EGM1996.zip). + - vdatum/core/egm1996/egm1996.gtx -> wrd_ge_prd_egm1996.gtx +2. *EGM2008* + - Download from [NOAA/NOS's VDatum: Download VDatum](https://vdatum.noaa.gov/download.php) as [EGM1996](https://vdatum.noaa.gov/download/data/vdatum_EGM2008.zip). + - vdatum/core/egm1996/egm2008.gtx -> wrd_ge_prd_egm2008.gtx + + +#### Geoid directory structure + +The next sections assume the geoid files are in the following directory structure: +``` +GEIOD_DIR +├── wrd_ge +│   └── wrd_ge_prd_egm2008.gtx +│   └── wrd_ge_prd_egm1996.gtx +├── usa_ge +│   ├── usa_ge_prd_g2012ba0.gtx +│   ├── usa_ge_prd_g2012bg0.gtx +│   ├── usa_ge_prd_g2012bh0.gtx +│   ├── usa_ge_prd_g2012bp0.gtx +│   ├── usa_ge_prd_g2012bs0.gtx +│   ├── usa_ge_prd_g2012bu0.gtx +│   ├── usa_ge_prd_g2018p0.gtx +│   └── usa_ge_prd_g2018u0.gtx +└── can_ge + └── can_ge_prd_HT2_2010v70.gtx +``` + +Other organization is possible, but the commands in the subsequent sections would need to be modified accordingly. + +### USA/Canada/Mexico 3DEP terrain model + +This terrain model is a set of 1x1 degree tile files, horizontal coordinate system is WGS84, heights are geoidal (since USA and Canada geoids tied to same point, any of them might be used) + +1. Download from [Rockyweb FTP server](https://rockyweb.usgs.gov/vdelivery/Datasets/Staged/Elevation/1/TIFF/current/) (only USGS_1_*.tif files are needed). They should be put to single directory (let it be `DOWNLOAD_DIR`) +2. Convert 3DEP files in `DOWNLOAD_DIR` to WGS84 horizontal coordinates (leaving heights geoidal). Result is in `3dep_wgs84_geoidal_tif` directory: +``` +to_wgs84.py --out_dir 3dep_wgs84_geoidal_tif 'DOWNLOAD_DIR/*.tif +``` +Takes ~40 minutes on 8 CPUs. YMMV + +3. Convert heights of files in `3dep_wgs84_geoidal_tif` to ellipsoidal. Result in `3dep_wgs84_tif` +``` +to_wgs84.py --src_geoid 'GEOID_DIR/usa_ge/usa_ge_prd_g2018?0.gtx' \ + --src_geoid 'GEOID_DIR/usa_ge/usa_ge_prd_g2012b?0.gtx' \ + --src_geoid GEOID_DIR/can_ge/can_ge_prd_HT2_2010v70.gtx \ + --format_param COMPRESS=PACKBITS \ + --out_dir 3dep_wgs84_tif '3dep_wgs84_geoidal_tif/*.tif` +``` +Some Mexican tiles will be dropped, as they are not covered by USA or Canada geoids +Takes ~1.5 hours on 8 CPUs. YMMV + +4. Convert TIFF tiles in `3dep_wgs84_tif` to PNG tiles in `3dep_wgs84_png` +``` +to_png.py --out_dir 3dep_wgs84_png '3dep_wgs84_tif/*.tif' +``` +Takes 13 minutes on 8 CPUs. YMMV. _The conversion to PNG is in progress. Skip this step for now._ + + +### Coarse SRTM/GLOBE world terrain + +SRTM is terrain model of 3 arcsecond resolution that covers Earth between 60N and 56S latitudes. It consists of 1x1 degree tile files that have WGS84 horizontal coordinates and Mean Sea Level (global geoidal) heights. + +GLOBE is global terrain model of 30 arcsecond resolution that covers entire Earth. It consists of large tiles that have WGS84 horizontal coordinates and Mean Sea Level (global geoidal) heights. + +Let's create combined tiled global terrain model. + +1. Download Globe files from [Get Data Tiles-Global Land One-km Base Elevation Project | NCEI](https://www.ngdc.noaa.gov/mgg/topo/gltiles.html) as [All Tiles in One .zip file](https://www.ngdc.noaa.gov/mgg/topo/DATATILES/elev/all10g.zip) + Unpack files to `all10` directory +2. Download header files for Globe files from [Topography and Digital Terrain Data | NCEI](https://www.ngdc.noaa.gov/mgg/topo/elev/esri/hdr/) - to the same `all10` directory +3. SRTM data - finding source data to download is not that easy (albeit probably possible). Assume we have them in `srtm_geoidal_hgt` directory. +4. Ascribe coordinate system to GLOBE files in `all10` and convert them to .tif. Result in `globe_geoidal_tif`: +``` +mkdir globe_geoidal_tif +for fn in all10/???? ; do gdal_translate -a_srs '+proj=longlat +datum=WGS84' $fn globe_geoidal_tif/${fn##*/}.tif ; done +``` +Takes ~1 minute. YMMV + +5. Convert heights of files in `globe_geoidal_tif` directory to ellipsoidal. Result in `globe_wgs84_tif`: +``` +to_wgs84.py --src_geoid GEOID_DIR/wrd_ge/wrd_ge_prd_egm2008.gtx --out_dir globe_wgs84_tif 'globe_geoidal_tif/*.tif' +``` +Takes ~1 minute on 8 CPUs. YMMV + +6. Convert heights of SRTM in `srtm_geoidal_hgt` directory to WGS84. Result in `wgs84_tif` directory: +``` +to_wgs84.py --src_geoid GEOID_DIR/wrd_ge/wrd_ge_prd_egm2008.gtx --out_ext .tif --out_dir wgs84_tif 'srtm_geoidal_hgt/*.hgt' +``` +Takes ~40 minutes on 8 CPUs. YMMV + +7. Add GLOBE tiles to the north of 60N to `wgs84_tif` directory: +``` +tiler.py --bottom 60 --margin 1 \ + --tile_pattern 'wgs84_tif/{LAT_HEM}{lat_d:02}{LON_HEM}{lon_l:03}_globe.tif' \ + 'globe_wgs84_tif/*.tif' +``` +Takes ~8 minutes on 8 CPUs. YMMV + +8. Add GLOBE tiles to the south of 567S to `wgs84_tif` directory: +``` +tiler.py --top=-56 --margin 1 \ + --tile_pattern 'wgs84_tif/{LAT_HEM}{lat_d:02}{LON_HEM}{lon_l:03}_globe.tif' \ + 'globe_wgs84_tif/*.tif' +``` +Takes ~9 minutes on 8 CPUs. YMMV + +9. Convert TIFF files in `wgs84_tif` directory to PNG in `wgs84_png`: +``` +to_png.py --data_type UInt16 --out_dir wgs84_png 'wgs84_tif/*.tif' +``` +Takes ~45 minutes on 8 CPUs. YMMV. _The conversion to PNG is in progress. Skip this step for now._ + + +### High resolution terrain data (aka LiDAR) + +Assume LiDAR files (multilevel set of 2-band TIFF files, indexed by .csv files) is in `proc_lidar` directory. Let's convert it to tiles. + +_**Note:** the first two steps are a work in progress. For the time being skip to step three._ + +1. Convert LiDARS to set of per-agglomeration TIFF files with geoidal heights. Result in `lidar_geoidal_tif` directory: +``` +lidar_merge.py --format_param BIGTIFF=YES --format_param COMPRESS=ZSTD proc_lidar lidar_geoidal_tif +``` + Here ZSTD compression was used to save disk space, albeit it makes conversion process slower. + Takes 2.5 hours on 8 CPUs. YMMV + +2. Tile lidars in `lidar_geoidal_tif`. Result is in `tiled_lidar_geoidal_tif` directory: +``` +tiler.py --format_param BIGTIFF=YES --format_param COMPRESS=ZSTD --margin 2 \ + --tile_pattern 'tiled_lidar_geoidal_tif/{lat_hem}{lat_u:02}{lon_hem}{lon_l:03}.tif' \ + 'lidar_geoidal_tif/*.tif' +``` + Takes 5 hours on 8 CPUs. YMMV + +3. Convert geoidal heights of tiles in `tiled_lidar_geoidal_tif` to ellipsoidal. Result in `tiled_lidar_wgs84_tif` directory: +``` +to_wgs84.py --format_param BIGTIFF=YES --format_param COMPRESS=ZSTD --remove_src \ + --src_geoid 'GEOID_DIR/usa_ge/usa_ge_prd_g2018?0.gtx' \ + --src_geoid 'GEOID_DIR/usa_ge/usa_ge_prd_g2012b?0.gtx' \ + --src_geoid GEOID_DIR/can_ge/can_ge_prd_HT2_2010v70.gtx \ + --out_dir tiled_lidar_wgs84_tif \ + 'tiled_lidar_geoidal_tif/*.tif' +``` +Takes ***22 hours on 16 CPUs (probably ~45 hours on 8CPUs)***. YMMV + +4. Convert TIF files in `tiled_lidar_wgs84_tif` directory to PNG files in `tiled_lidar_wgs84_png` directory: +`to_png.py --out_dir tiled_lidar_wgs84_png 'tiled_lidar_wgs84_tif/*'` +Takes ~8 hours on 8 CPUs. YMMV. _The conversion to PNG is in progress. Skip this step for now._ + + +### Canada CDSM surface model + +CDSM is a surface (rooftop/treetop) model that covers canada up to 61N latitude.It can be downloaded from [Canadian Digital Surface Model 2000](https://open.canada.ca/data/en/dataset/768570f8-5761-498a-bd6a-315eb6cc023d) page as [Cloud Optimized GeoTIFF of the CDSM](https://datacube-prod-data-public.s3.ca-central-1.amazonaws.com/store/elevation/cdem-cdsm/cdsm/cdsm-canada-dem.tif) + +1. Converting `cdsm-canada-dem.tif` to WGS84 horizontal coordinate system (leaving heights geoidal) to `cdsm-canada-dem_wgs84_geoidal.tif` +``` +to_wgs84.py --pixels_per_degree 3600 --round_boundaries_to_degree \ + --format_param BIGTIFF=YES --format_param COMPRESS=PACKBITS \ + cdsm-canada-dem.tif cdsm-canada-dem_wgs84_geoidal.tif +``` +Takes ~3 hours. YMMV + +2. Tiling `dsm-canada-dem_wgs84_geoidal.tif` to `tiled_wgs84_geoidal_tif/can_rt_nw.tif` +``` +tiler.py --margin 1 --format_param COMPRESS=PACKBITS \ + --tile_pattern 'tiled_wgs84_geoidal_tif/{lat_hem}{lat_u:02}{lon_hem}{lon_l:03}.tif' \ + dsm-canada-dem_wgs84_geoidal.tif +``` +Lot of tiles contain nothing but NoData - they are dropped. +Takes ~1 hour on 8 CPUs. YMMV + +3. Converting heights of files in `tiled_wgs84_geoidal_tif` to ellipsoidal, result in `tiled_wgs84_tif`. Geoids are under `GEOID_DIR`, using Canada HTv2 geoid +``` +to_wgs84.py --src_geoid GEOID_DIR/can_ge/can_ge_prd_HT2_2010v70.gtx \ + --extend_geoid_coverage 0.001 \ + --out_dir tiled_wgs84_tif 'tiled_wgs84_geoidal_tif/*.tif' +``` +Takes ~10 minutes on 8 CPU. YMMV + +4. Converting tiff tiles in `tiled_wgs84_tif` to png, result in `tiled_wgs84_png` +`to_png.py --out_dir tiled_wgs84_png 'tiled_wgs84_tif/*.tif'` +Takes ~3 minutes on 8 CPU. YMMV. _The conversion to PNG is in progress. Skip this step for now._ + +### Land usage files + +The biggest issue with land usage files is to catch 'em all. Different institutions that manage land categorization for an area have different category definitions. The AFC engine requires tiles defined in the NLCD format, and the mapping to NLCD must be contained in the `nlcd_wgs84.yaml` file. + +#### NLCD land usage files + +1. Downloading sources. + - CONUS 2019. From [Data | Multi-Resolution Land Characteristics (MLRC) Consortium. Pick 'Land Cover' and 'CONUS'](https://www.mrlc.gov/data?f%5B0%5D=category%3ALand%20Cover&f%5B1%5D=region%3Aconus) download [NLCD 2019 Land Cover (CONUS)](https://s3-us-west-2.amazonaws.com/mrlc/nlcd_2019_land_cover_l48_20210604.zip) + Unpack to `nlcd_sources` folder + - Alaska 2016. From [Data | Multi-Resolution Land Characteristics (MLRC) Consortium. Pick 'Land Cover' and 'Alaska'](https://www.mrlc.gov/data?f%5B0%5D=category%3ALand%20Cover&f%5B1%5D=region%3Aalaska) download [NLCD 2016 Land Cover (ALASKA)](https://s3-us-west-2.amazonaws.com/mrlc/NLCD_2016_Land_Cover_AK_20200724.zip) + Unpack to `nlcd_sources` folder +2. Converting NLCD sources (that use map projection coordinate system) in `nlcd_sources` to TIFF files with WGS84 coordinate system. Result in `nlcd_wgs84_tif` + - CONUS 2019 +``` +nlcd_wgs84.py --pixels_per_degree 3600 \ + --format_param BIGTIFF=YES --format_param COMPRESS=PACKBITS \ + nlcd_sources/nlcd_2019_land_cover_l48_20210604.img \ + nlcd_wgs84_tif/nlcd_2019_land_cover_l48_20210604.tif +``` +Takes ~50 minutes. YMMV + - Alaska 2016. Crop at 53N to avoid intersection with CONUS land coverage (which might be an issue when used without tiling): +``` +nlcd_wgs84.py --pixels_per_degree 3600 --bottom 53 \ + --format_param BIGTIFF=YES --format_param COMPRESS=PACKBITS \ + nlcd_sources/NLCD_2016_Land_Cover_AK_20200724.img \ + nlcd_wgs84_tif/NLCD_2016_Land_Cover_AK_20200724.tif +``` +Takes ~9 minutes. YMMV + +3. Tiling files in `nlcd_wgs84_tif`, result in `tiled_nlcd_wgs84_tif`: +``` +tiler.py --format_param COMPRESS=PACKBITS --remove_value 0 \ + --tile_pattern 'tiled_nlcd_wgs84_tif/{lat_hem}{lat_u:02}{lon_hem}{lon_l:03}.tif' \ + 'nlcd_wgs84_tif/*.tif' +``` +Takes ~13 minutes on 8 CPUs. YMMV + +4. Converting tiles in `tiled_nlcd_wgs84_tif` directory to PNG. Result in `tiled_nlcd_wgs84_png` directory: +``` +to_png.py --out_dir tiled_nlcd_wgs84_png 'tiled_nlcd_wgs84_tif/*.tif' +``` +Takes ~3 minutes on 8 CPUs. YMMV. _The conversion to PNG is in progress. Skip this step for now._ + +#### NOOA land usage files + +1. Downloading sources: + - Guam 2016. From [C-CAP Land Cover files for Guam](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/guam/) download [guam_2016_ccap_hr_land_cover20180328.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/guam/guam_2016_ccap_hr_land_cover20180328.img) to `noaa_sources` directory + - Hawaii 2005-2011. From [C-CAP Land Cover files for Hawaii](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/hi/) download to `noaa_sources` directory: + - [hi_hawaii_2005_ccap_hr_land_cover.ige](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/hi/hi_hawaii_2005_ccap_hr_land_cover.ige) + - [hi_hawaii_2010_ccap_hr_land_cover20150120.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/hi/hi_hawaii_2010_ccap_hr_land_cover20150120.img) + - [hi_kahoolawe_2005_ccap_hr_land_cover.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/hi/hi_kahoolawe_2005_ccap_hr_land_cover.img) + - [hi_kauai_2010_ccap_hr_land_cover20140929.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/hi/hi_kauai_2010_ccap_hr_land_cover20140929.img) + - [hi_lanai_2011_ccap_hr_land_cover_20141204.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/hi/hi_lanai_2011_ccap_hr_land_cover_20141204.img) + - [hi_maui_2010_ccap_hr_land_cover_20150213.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/hi/hi_maui_2010_ccap_hr_land_cover_20150213.img) + - [hi_molokai_2010_ccap_hr_land_cover20150102.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/hi/hi_molokai_2010_ccap_hr_land_cover20150102.img) + - [hi_niihau_2010_ccap_hr_land_cover20140930.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/hi/hi_niihau_2010_ccap_hr_land_cover20140930.img) + - [hi_oahu_2011_ccap_hr_land_cover20140619.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/hi/hi_oahu_2011_ccap_hr_land_cover20140619.img) + - Puerto Rico 2010. From [C-CAP Land Cover files for Puerto Rico](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/pr/) download to `noaa_sources` directory + - [pr_2010_ccap_hr_land_cover20170214.ige](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/pr/pr_2010_ccap_hr_land_cover20170214.ige) + - [pr_2010_ccap_hr_land_cover20170214.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/pr/pr_2010_ccap_hr_land_cover20170214.img) + - US Virgin Islands 2012. From [C-CAP Land Cover files for US Virgin Islands](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/usvi/) download to `noaa_sources` directory: + - [usvi_stcroix_2012_ccap_hr_land_cover20150910.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/usvi/usvi_stcroix_2012_ccap_hr_land_cover20150910.img) + - [usvi_stjohn_2012_ccap_hr_land_cover20140915.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/usvi/usvi_stjohn_2012_ccap_hr_land_cover20140915.img) + - [usvi_stthomas_2012_ccap_hr_land_cover20150914.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/usvi/usvi_stthomas_2012_ccap_hr_land_cover20150914.img) + - American Samoa 2009-2010. From [C-CAP Land Cover files for American Samoa](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/as/) download to `noaa_sources` directory: + - [as_east_manua_2010_ccap_hr_land_cover.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/as/as_east_manua_2010_ccap_hr_land_cover.img) + - [as_rose_atoll_2009_ccap_hr_land_cover.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/as/as_rose_atoll_2009_ccap_hr_land_cover.img) + - [as_swains_island_2010_ccap_hr_land_cover.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/as/as_swains_island_2010_ccap_hr_land_cover.img) + - [as_tutuila_2010_ccap_hr_land_cover.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/as/as_tutuila_2010_ccap_hr_land_cover.img) + - [https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/as/as_west_manua_2010_ccap_hr_land_cover.img](https://chs.coast.noaa.gov/htdata/raster1/landcover/bulkdownload/hires/as/as_west_manua_2010_ccap_hr_land_cover.img) +2. Converting NOAA sources (that use map projection coordinate system) in `noaa_sources` to TIFF files with WGS84 coordinate system. Result in `noaa_wgs84_tif`: +``` +for path_fn_ext in noaa_sources/*.img ; do fn_ext=${path_fn_ext##*/} ; fn=${fn_ext%.*} ; \ + nlcd_wgs84.py --encoding noaa --pixels_per_degree 3600 \ + --format_param BIGTIFF=YES --format_param COMPRESS=PACKBITS \ + ${path_fn_ext} noaa_wgs84_tif/${fn}.tif ; done +``` +Takes ~12 minutes on 8 CPUs. YMMV + +3. Tiling files in `noaa_wgs84_tif`, result in `tiled_noaa_wgs84_tif`: +``` +tiler.py --format_param COMPRESS=PACKBITS --remove_value 0 \ + --tile_pattern 'tiled_noaa_wgs84_tif/{lat_hem}{lat_u:02}{lon_hem}{lon_l:03}.tif' \ + 'noaa_wgs84_tif/*.tif' +``` +Takes ~10 seconds on 8 CPUs. YMMV + +4. Converting tiles in `tiled_noaa_wgs84_tif` directory to PNG. Result in `tiled_noaa_wgs84_png` directory: +`to_png.py --out_dir tiled_noaa_wgs84_png 'tiled_noaa_wgs84_tif/*.tif'` +Takes ~2 seconds on 8 CPUs. YMMV. _The conversion to PNG is in progress. Skip this step for now._ + +### Canada land usage files +1. Downloading source. From [2015 Land Cover of Canada - Open Government Portal](https://www.mrlc.gov/data?f%5B0%5D=category%3ALand%20Cover&f%5B1%5D=region%3Aconus) download [https://ftp.maps.canada.ca/pub/nrcan_rncan/Land-cover_Couverture-du-sol/canada-landcover_canada-couverture-du-sol/CanadaLandcover2015.zip) + Unpack to `canada_lc_sources` folder +2. Converting Canada land cover sources (that use map projection coordinate system) in `canada_lc_sources` to TIFF files with WGS84 coordinate system. Result in `canada_lc_wgs84_tif`: +``` +nlcd_wgs84.py --encoding noaa --pixels_per_degree 3600 \ + --format_param BIGTIFF=YES --format_param COMPRESS=PACKBITS \ + canada_lc_sources/CAN_LC_2015_CAL.tif \ + canada_lc_wgs84_tif/CAN_LC_2015_CAL.tif +``` +Takes ***12.5 hours***. Of them 20 minutes was spent on 8 CPUs and the rest - on single CPU. YMMV + +3. Tiling Canada land cover file in `canada_lc_wgs84_tif`, result in `tiled_canada_lc_wgs84_tif`: +``` +tiler.py --format_param COMPRESS=PACKBITS --remove_value 0 \ + --tile_pattern 'tiled_canada_lc_wgs84_tif/{lat_hem}{lat_u:02}{lon_hem}{lon_l:03}.tif' \ + canada_lc_wgs84_tif/CAN_LC_2015_CAL.tif +``` +Takes ~50 minutes on 8 CPUs. YMMV + +4. Converting tiles in `tiled_canada_lc_wgs84_tif` directory to PNG. Result in `tiled_canada_lc_wgs84_png` directory: +``` +to_png.py --out_dir tiled_canada_lc_wgs84_png 'tiled_canada_lc_wgs84_tif/*.tif' +``` +Takes ~4 minutes om 8 CPU. YMMV. _The conversion to PNG is in progress. Skip this step for now._ + +### Corine land cover files +The Corine land cover is the database from the Copernicus program of the EU space program. It provides land classification over the EU. + +1. Downloading source. From [the CLC 2018](https://land.copernicus.eu/pan-european/corine-land-cover/clc2018) page, download the GeoTIFF version of the data (login required). Unzip to `corine_lc_sources` folder +2. Converting Corine land cover sources (that use map projection coordinate system) in `corine_lc_sources` to TIFF files in WGS84 coordinate system and with the land cover mapping in the `.yaml` file applied to put in the same format as the NLCD data: +``` +nlcd_wgs84.py --encoding corine --pixels_per_degree 3600 \ + --format_param COMPRESS=PACKBITS corine_lc_sources/DATA/U2018_CLC2018_V2020_20u1.tif \ + corine_lc_sources/DATA/U2018_CLC2018_V2020_20u1_resampled.tif +``` + +3. Tiling the Corine land cover (if necessary) to a directory `tiled_corine_lc_wgs84_tif`: +``` +tiler.py --format_param COMPRESS=PACKBITS --remove_value 0 \ + --tile_pattern 'tiled_corine_lc_wgs84_tif/{lat_hem}{lat_u:02}{lon_hem}{lon_l:03}.tif' \ + corine_lc_sources/Data/U2018_CLC2018_V2020_20u1_resampled.tif +``` + +## *proc_gdal* - creating LiDAR files +This directory contains undigested, but very important files that convert LiDAR files from source format (as downloaded from https://rockyweb.usgs.gov/vdelivery/Datasets/Staged/Elevation/Non_Standard_Contributed/NGA_US_Cities/ ) to a format compatible with the AFC Engine (two-band geodetic raster files and their index .csv files) diff --git a/tools/geo_converters/USVIxform.xlsx b/tools/geo_converters/USVIxform.xlsx new file mode 100644 index 0000000..6b2711c Binary files /dev/null and b/tools/geo_converters/USVIxform.xlsx differ diff --git a/tools/geo_converters/dir_md5.py b/tools/geo_converters/dir_md5.py new file mode 100755 index 0000000..5762ab8 --- /dev/null +++ b/tools/geo_converters/dir_md5.py @@ -0,0 +1,746 @@ +#!/usr/bin/env python3 +# Computes MD5 hash over files in directory + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=unused-wildcard-import, invalid-name, too-few-public-methods +# pylint: disable=too-many-statements, too-many-locals, too-many-arguments +# pylint: disable=wildcard-import +import argparse +import datetime +import fnmatch +import hashlib +import json +try: + import jsonschema +except ImportError: + pass +import multiprocessing +import os +import queue +import re +import signal +import sys +import threading +from typing import Any, Dict, List, NamedTuple, Optional, Tuple, Union + +from geoutils import * + +# For printing statistics +GIGA = 1024 * 1024 * 1024 + +# Name of schema file for version information JSON files +SCHEMA_FILE_NAME = "g8l_info_schema.json" + +# Root of geospatial data definitions in JSON file +FILESETS_KEY = "data_releases" + +# Structure of dataset key +FILESET_KEY_PATTERN = r"^\w{3}_\w{2}_\w{3}$" + +# Key of filemask list witin dataset +FILESET_MASKS_KEY = "files" + +# Key of MD5 list witin dataset +FILESET_MD5_KEY = "md5" + +_EPILOG = """ This script benefits from `jsonschema` Python module installed, +but may leave without it. Hence it may easily be used outside of container. +Some examples: +- List subcommands: + $ dir_md5.py +- Print help on `compute` subcommand: + $ dir_md5.py help compute +- List LiDAR files (in proc_lidar_2019 directory) to be included into MD5 + computation: + $ dir_md5.py list --mask '*.csv' --mask '*.tif' --recursive proc_lidar_2019 +- Compute MD5 over SRTM files in srtm3arcsecondv003 directory: + $ dir_md5.py compute --mask '*.hgt' srtm3arcsecondv003 +- Update MD5 for USA geoids, per by usa_ge_prd/usa_ge_prd_version_info.json + $ dir_md5.py update --json usa_ge_prd/usa_ge_prd_version_info.json +""" + + +class FileInfo: + """ Information about file to hash + + Public attributes: + full_path -- Full file path (to use for file reading) + rel_path -- Root-relative path to use as sort key if computation is + recursive + basename -- Basename to use as sort key if computation is nonrecursive + size -- File size in bytes + """ + + def __init__(self, filename: str, root: str, size: int) -> None: + """ Constructor + + Arguments: + filename -- File name + root -- Root relative to which relative path should be computed + size -- File size in bytes + """ + self.full_path = os.path.abspath(filename) + self.rel_path = os.path.relpath(filename, root) + self.basename = os.path.basename(filename) + self.size = size + + def sort_key(self, with_path: bool) -> Union[str, Tuple[str, str]]: + """ Sort key + + Arguments: + with_path -- True to return relative path as sort key, False to return + basename + Returns sort key + """ + return os.path.split(self.rel_path) if with_path else self.basename + + +class ChunkInfo: + """ Information about file chunk + + Public attributes: + seq -- Chunk sequence number in overall chunk sequence + fileinfo -- Information about file to read chunk from + offset -- Chunk offset + length -- Chunk length + data -- Chunk content (filled in worker, initially None) + """ + + def __init__(self, seq: int, fileinfo: FileInfo, offset: int, + length: int) -> None: + """ Constructor + + Arguments: + seq -- Chunk sequence number in overall chunk sequence + fileinfo -- Information about file to read chunk from + offset -- Chunk offset + length -- Chunk length + """ + self.seq = seq + self.fileinfo = fileinfo + self.offset = offset + self.length = length + self.data: Optional[bytes] = None + + +class Md5Computer: + """ Threaded MD5 computer + + Private attributes: + _num_threads -- Number of threads + _chunk_size -- Size of chunk in bytes + _max_in_flight -- Maximum number of retrieved but not processed chunks + _progress -- Print progress information + """ + + def __init__(self, num_threads: Optional[int], chunk_size: int, + max_in_flight: int, progress: bool, stats: bool) -> None: + """ Constructor + + Arguments: + num_threads -- Number of threads (None for 2*CPUs) + chunk_size -- Size of chunk in bytes + max_in_flight -- Maximum number of retrieved but not processed chunks + progress -- Print progress information + stats -- Print performance statistics + """ + self._num_threads: int = \ + num_threads or (2 * multiprocessing.cpu_count()) + self._chunk_size = chunk_size + self._max_in_flight = max_in_flight + self._progress = progress + self._stats = stats + + def calculate(self, fileinfos: List[FileInfo]) -> str: + """ Calculate MD5 + + Arguments: + fileinfos -- List of FileInfo objects on files to use + Return MD5 in string representation + """ + start_time = datetime.datetime.now() + md5 = hashlib.md5() + # Worker threads that read chunks' data + threads: List[threading.Thread] = [] + try: + # Queue to worker threads, contains unfilled chunks, Nones to stop + chunk_task_queue: queue.Queue[Optional[ChunkInfo]] = queue.Queue() + # Queue from worker threads, contains filled chunks, Nones on error + chunk_result_queue: queue.Queue[Optional[ChunkInfo]] = \ + queue.Queue() + # Filled chunks, indexed by sequence number + chunk_map: Dict[int, ChunkInfo] = {} + # Sequence number of next chunk to be put to task queue + next_chunk_seq = 0 + # Sequence number of next chunk to MD5 + next_md5_seq = 0 + # Creating worker threads + original_sigint_handler = \ + signal.signal(signal.SIGINT, signal.SIG_IGN) + for _ in range(self._num_threads): + threads.append( + threading.Thread( + target=self._chunk_worker, + kwargs={"task_queue": chunk_task_queue, + "result_queue": chunk_result_queue})) + threads[-1].start() + signal.signal(signal.SIGINT, original_sigint_handler) + + task_queue_lens = 0 + task_queue_samples = 0 + last_name = "" + + # List of all nonfilled chunks + chunks = self._get_chunks(fileinfos) + # While not all chunks were MD%-ed + while next_md5_seq != len(chunks): + # MD5 chunks from map that are in order + while next_md5_seq in chunk_map: + data = chunk_map[next_md5_seq].data + assert data is not None + md5.update(data) + chunk_map[next_md5_seq].data = None + del chunk_map[next_md5_seq] + next_md5_seq += 1 + + task_queue_lens += chunk_task_queue.qsize() + task_queue_samples += 1 + + # Give workers some chunks to fill + while (next_chunk_seq != len(chunks)) and \ + ((next_chunk_seq - next_md5_seq) < + self._max_in_flight): + chunk = chunks[next_chunk_seq] + + new_name = chunk.fileinfo.rel_path + if self._progress and (last_name != new_name): + print(new_name + + (max(0, len(last_name) - len(new_name))) * " ", + end="\r", flush=True) + last_name = new_name + + chunk_task_queue.put(chunk) + next_chunk_seq += 1 + # Retrieve filled chunks to chunk map + while (not chunk_result_queue.empty()) or \ + (next_md5_seq != len(chunks)) and \ + (next_md5_seq not in chunk_map): + chunk1: Optional[ChunkInfo] = chunk_result_queue.get() + error_if(chunk1 is None, + "Undefined file reading problem") + assert chunk1 is not None + error_if( + chunk1.data is None, + f"Error reading {chunk1.length} bytes at offset " + f"{chunk1.offset} from '{chunk.fileinfo.full_path}'") + chunk_map[chunk1.seq] = chunk1 + finally: + # Close all threads + for _ in range(len(threads)): + chunk_task_queue.put(None) + for thread in threads: + thread.join() + + if self._progress: + print(len(last_name) * " ", end="\r") + + if self._stats: + seconds = (datetime.datetime.now() - start_time).total_seconds() + total_size = sum(fi.size for fi in fileinfos) + cpu_bound = \ + (task_queue_lens / (task_queue_samples or 1)) < \ + (self._max_in_flight / 2) + print(f"Duration: {int(seconds) // 3600}h " + f"{(int(seconds) // 60) % 60}m {seconds % 60:.2f}s") + print(f"Data length: {total_size / GIGA:.3f} GB") + print(f"Performance: {total_size / seconds / GIGA:.2f} GB/s") + print( + f"Operation was {'CPU(MD5)' if cpu_bound else 'IO'}-bound") + + return "".join(f"{b:02X}" for b in md5.digest()) + + def _get_chunks(self, fileinfos: List[FileInfo]) -> List[ChunkInfo]: + """ Create list of chunks """ + ret: List[ChunkInfo] = [] + for fi in fileinfos: + offset = 0 + while offset < fi.size: + length = min(self._chunk_size, fi.size - offset) + ret.append( + ChunkInfo(seq=len(ret), fileinfo=fi, offset=offset, + length=length)) + offset += length + return ret + + def _chunk_worker(self, task_queue: queue.Queue, + result_queue: queue.Queue) -> None: + """ Worker thread function that fills chunks + + Arguments: + task_queue -- Queue with chunks to fill. None to exit + result_queue -- Queue with filled chunks + """ + while True: + chunk: Optional[ChunkInfo] = None + try: + chunk = task_queue.get() + if chunk is None: + return + with open(chunk.fileinfo.full_path, mode="rb") as f: + f.seek(chunk.offset) + chunk.data = f.read(chunk.length) + except Exception: + pass + finally: + result_queue.put(chunk) + + +class VersionJson: + """ Handler of version information JSON + + Private attributes: + _filename -- JSON file name + _json_dict -- JSON file content as dictionary + + Public attributes: + directory -- Directory where JSON file is located + """ + # Information about fileset, stored in JSON + FilesetInfo = \ + NamedTuple( + "FilesetInfo", [ + # Fileset type (region_filetype) + ("fileset_type", str), + # List of masks + ("masks", List[str])]) + + def __init__(self, filename: str) -> None: + """ Constructor + + Arguments: + filename -- JSON file name + """ + self._filename = filename + error_if(not os.path.isfile(self._filename), + f"File '{self._filename}' not found") + try: + with open(self._filename, mode="rb") as f: + self._json_dict = json.load(f) + except json.JSONDecodeError as ex: + error(f"Invalid JSON syntax in '{self._filename}': {ex}") + self.directory = os.path.dirname(self._filename) or "." + + schema_filename = \ + os.path.join(os.path.dirname(__file__) or ".", SCHEMA_FILE_NAME) + if os.path.isfile(schema_filename): + if "jsonschema" in sys.modules: + try: + with open(schema_filename, mode="rb") as f: + json_schema_dict = json.load(f) + try: + jsonschema.validate(instance=self._json_dict, + schema=json_schema_dict) + except jsonschema.ValidationError as ex: + warning(f"File '{self._filename}' doesn't match " + f"schema: {ex}") + except json.JSONDecodeError as ex: + warning(f"Invalid JSON syntax in '{schema_filename}': " + f"{ex}, hence correctness of '{self._filename}' " + f"schema not checked") + else: + warning(f"'jsonschema' module not installed in Python, hence " + f"correctness of '{self._filename}' schema not " + f"checked") + else: + warning(f"Version info schema file ({schema_filename}) was not " + f"found, hence correctness of '{self._filename}' schema " + f"not checked") + error_if(not (isinstance(self._json_dict, dict) and + (FILESETS_KEY in self._json_dict) and + isinstance(self._json_dict[FILESETS_KEY], dict) and + all(re.match(FILESET_KEY_PATTERN, k) + for k in self._json_dict[FILESETS_KEY])), + f"Unsupported structure of '{self._filename}'") + + def get_filesets(self) -> List["VersionJson.FilesetInfo"]: + """ Returns information about filesets contained in the file """ + ret: List["VersionJson.FilesetInfo"] = [] + for fs_type, fs_dict in self._json_dict[FILESETS_KEY].items(): + error_if(not (isinstance(fs_dict, dict) and + (FILESET_MASKS_KEY in fs_dict) and + isinstance(fs_dict[FILESET_MASKS_KEY], list) and + all(isinstance(v, str) + for v in fs_dict[FILESET_MASKS_KEY])), + f"'{fs_type}' fileset descriptor in " + f"'{FILESET_KEY_PATTERN}' of '{self._filename}' has " + f"invalid structure") + ret.append( + self.__class__.FilesetInfo( + fileset_type=fs_type, masks=fs_dict[FILESET_MASKS_KEY])) + return ret + + def get_md5(self, fs_type: str) -> Optional[str]: + """ Returns MD5 stored for given fileset type. None if there is none + """ + return self._json_dict[FILESETS_KEY][fs_type].get(FILESET_MD5_KEY) + + def update_md5(self, fs_type: str, md5_str: str) -> None: + """ Updates MD5 for given fileset type """ + self._json_dict[FILESETS_KEY][fs_type][FILESET_MD5_KEY] = md5_str + try: + with open(self._filename, mode="w", encoding="utf-8") as f: + json.dump(self._json_dict, f, indent=2) + except OSError as ex: + error(f"Error writing '{self._filename}': {ex}") + + +def get_files(file_dir_name: str, masks: Optional[List[str]], + recursive: bool) -> List[FileInfo]: + """ Creates list of FileInfo object from given file/directory name + + Arguments: + file_or_dir -- Name of file or directory + masks -- None or list of fnmatch-compatible wildcard masks + recursive -- True to recurse into subdirectories + Returns list of FileInfo objects + """ + ret: List[FileInfo] = [] + + def process_file(filename: str, root: str) -> None: + """ Adds file to resulting list if it is eligible + + Arguments: + filename -- File name + root -- Root directory to use for FileInfo creation + """ + if masks: + for mask in masks: + if fnmatch.fnmatch(os.path.basename(filename), mask): + break + else: + return + ret.append(FileInfo(filename=filename, root=root, + size=os.path.getsize(filename))) + if os.path.isfile(file_dir_name): + process_file(file_dir_name, os.path.dirname(file_dir_name)) + elif os.path.isdir(file_dir_name): + if recursive: + for dirpath, _, basenames in os.walk(file_dir_name): + for basename in basenames: + process_file(os.path.join(dirpath, basename), + file_dir_name) + else: + for basename in os.listdir(file_dir_name): + full_name = os.path.join(file_dir_name, basename) + if os.path.isfile(full_name) and \ + (not os.path.islink(full_name)): + process_file(full_name, file_dir_name) + else: + error(f"'{file_dir_name}' is neither file nor directory name") + return ret + + +def get_filelists( + json_filename: Optional[str], json_required: bool = False, + files_and_dirs: Optional[List[str]] = None, recursive: bool = False, + masks: Optional[List[str]] = None) \ + -> Tuple[Optional[VersionJson], + List[Tuple[Optional[str], List[FileInfo]]]]: + """ Returns filelists, indexed by fileset types + + Arguments: + json_filename -- Optional name of version info JSAON file + json_required -- True if version info JSAON file must be specified + files_and_dirs -- List of files and directories for which to compute MD5. + If empty and JSOIN file not specified - all files in the\ + current directory + recursive -- True to recurse into subdirectories + masks -- fnmatch masks of files to include into MD5 computation + """ + error_if(json_required and (json_filename is None), + "Version info JSON file (--json switch) not provided") + error_if( + (json_filename is not None) and files_and_dirs, + "Files/directories should not be provided if --json is provided") + ret: List[Tuple[Optional[str], List[FileInfo]]] = [] + vj: Optional[VersionJson] = None + if json_filename is not None: + vj = VersionJson(json_filename) + for fs in vj.get_filesets(): + ret.append( + (fs.fileset_type, get_files(vj.directory, masks=fs.masks, + recursive=False))) + recursive = False + else: + fileinfos: List[FileInfo] = [] + for file_or_dir in files_and_dirs or ["."]: + fileinfos += get_files(file_dir_name=file_or_dir, masks=masks, + recursive=recursive) + ret.append((None, fileinfos)) + if (len(fileinfos) == 1) and \ + (os.path.splitext(fileinfos[0].basename)[1] == ".json"): + warning("Didn't you forget to provide '--json' switch? Without it " + "MD5 will be computed only over JSON file itself") + + def sort_key(fi: FileInfo) -> Union[str, Tuple[str, str]]: + return fi.sort_key(with_path=recursive) + + for _, fileinfos in ret: + fileinfos.sort(key=sort_key) + for i in range(len(fileinfos) - 1): + error_if( + sort_key(fileinfos[i]) == sort_key(fileinfos[i + 1]), + f"Files '{fileinfos[i].full_path}' and " + f"'{fileinfos[i + 1].full_path}' have same sort name of " + f"'sort_key(fileinfos[i])', which makes MD5 undefined (as it " + f"is file order dependent)") + return (vj, ret) + + +def do_list(args: Any) -> None: + """Execute "list" command. + + Arguments: + args -- Parsed command line arguments + """ + _, filelists = \ + get_filelists( + json_filename=args.json, files_and_dirs=args.FILES_AND_DIRS, + recursive=args.recursive, masks=args.mask) + for fs_type, fileinfos in filelists: + if len(filelists) != 1: + print(f"Fileset: {fs_type}") + for fileinfo in fileinfos: + print(fileinfo.rel_path if (args.recursive and not args.json) + else fileinfo.basename) + if args.stats: + print("") + total_size = sum(fi.size for fi in fileinfos) + print(f"Data length: {total_size / GIGA:.3f} GB") + if len(filelists) != 1: + print("") + + +def do_compute(args: Any) -> None: + """Execute "compute" command. + + Arguments: + args -- Parsed command line arguments + """ + if args.nice: + nice() + _, filelists = \ + get_filelists( + json_filename=args.json, files_and_dirs=args.FILES_AND_DIRS, + recursive=args.recursive, masks=args.mask) + for fs_type, fileinfos in filelists: + if len(filelists) != 1: + print(f"Fileset: {fs_type}") + md5_computer = Md5Computer(num_threads=threads_arg(args.threads), + chunk_size=args.chunk, + max_in_flight=args.max_in_flight, + progress=args.progress, stats=args.stats) + print(md5_computer.calculate(fileinfos)) + if len(filelists) != 1: + print("") + + +def do_verify(args: Any) -> None: + """Execute "verify" command. + + Arguments: + args -- Parsed command line arguments + """ + if args.nice: + nice() + version_json, filelists = \ + get_filelists(json_filename=args.json, json_required=True) + assert version_json is not None + mismatches: List[str] = [] + for fs_type, fileinfos in filelists: + assert fs_type is not None + if len(filelists) != 1: + print(f"Fileset: {fs_type}") + md5_computer = Md5Computer(num_threads=threads_arg(args.threads), + chunk_size=args.chunk, + max_in_flight=args.max_in_flight, + progress=args.progress, stats=args.stats) + computed_md5 = md5_computer.calculate(fileinfos) + json_md5 = version_json.get_md5(fs_type) + if computed_md5 != json_md5: + print(f"MD5 mismatch. Computed: {computed_md5}, in '{args.json}': " + f"{json_md5}'") + print("") + mismatches.append(fs_type) + error_if(mismatches, f"MD5 mismatches found in: {', '.join(mismatches)}") + + +def do_update(args: Any) -> None: + """Execute "update" command. + + Arguments: + args -- Parsed command line arguments + """ + if args.nice: + nice() + version_json, filelists = \ + get_filelists(json_filename=args.json, json_required=True) + assert version_json is not None + for fs_type, fileinfos in filelists: + assert fs_type is not None + if len(filelists) != 1: + print(f"Fileset: {fs_type}") + md5_computer = Md5Computer(num_threads=threads_arg(args.threads), + chunk_size=args.chunk, + max_in_flight=args.max_in_flight, + progress=args.progress, stats=args.stats) + computed_md5 = md5_computer.calculate(fileinfos) + json_md5 = version_json.get_md5(fs_type) + if computed_md5 != json_md5: + version_json.update_md5(fs_type, computed_md5) + + +def do_help(args: Any) -> None: + """Execute "help" command. + + Arguments: + args -- Parsed command line arguments (also contains 'argument_parser' and + 'subparsers' fields) + """ + if args.subcommand is None: + args.argument_parser.print_help() + else: + args.subparsers.choices[args.subcommand].print_help() + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + # Switches for explicitly specified sets of files + switches_explicit = argparse.ArgumentParser(add_help=False) + switches_explicit.add_argument( + "--recursive", action="store_true", + help="Process files in subdirectories") + switches_explicit.add_argument( + "--mask", metavar="WILDCARD", action="append", + help="Only use given filename pattern (should not include directory). " + "Wildcard format is fnmatch compatible (?, *, [...]). This switch may " + "be specified several times. By default all files are used. Don't " + "forget to quote it on Linux") + switches_explicit.add_argument( + "FILES_AND_DIRS", nargs="*", + help="Files and directories. Default is current directory. Yet if no " + "arguments specified - help will be printed, so, say, '.' or '*' " + "might be used") + + # Switches for MD5 computation + switches_computation = argparse.ArgumentParser(add_help=False) + switches_computation.add_argument( + "--progress", action="store_true", + help="Print progress information") + switches_computation.add_argument( + "--threads", metavar="COUNT_OR_PERCENT%", + help="Number of threads to use. If positive - number of threads, if " + "negative - number of CPU cores NOT to use, if followed by `%%` - " + "percent of CPU cores. Default is total number of CPU cores") + switches_computation.add_argument( + "--nice", action="store_true", + help="Lower priority of this process and its subprocesses") + switches_computation.add_argument( + "--chunk", type=int, default=16 * 1024 * 1024, help=argparse.SUPPRESS) + switches_computation.add_argument( + "--max_in_flight", type=int, default=50, help=argparse.SUPPRESS) + + # Switches for statistics + switches_stats = argparse.ArgumentParser(add_help=False) + switches_stats.add_argument( + "--stats", action="store_true", help="Print statistics") + + # Switches for version info JSON + switches_optional_json = argparse.ArgumentParser(add_help=False) + switches_optional_json.add_argument( + "--json", metavar="VERSION_INFO_JSON", + help="Name of ...version_info.json file that describes fileset(s) to " + "compute MD5 for") + + # Switches for version info JSON + switches_required_json = argparse.ArgumentParser(add_help=False) + switches_required_json.add_argument( + "--json", metavar="VERSION_INFO_JSON", required=True, + help="Name of ...version_info.json file that describes fileset(s) to " + "compute MD5 for. This parameter is mandatory") + + argument_parser = argparse.ArgumentParser( + description="Computes MD5 hash over files in directory", + formatter_class=argparse.RawDescriptionHelpFormatter, epilog=_EPILOG) + subparsers = argument_parser.add_subparsers(dest="subcommand", + metavar="SUBCOMMAND") + + # Subparser for "list" subcommand + parser_list = subparsers.add_parser( + "list", + parents=[switches_explicit, switches_optional_json, switches_stats], + help="Print list of files (no MD5 computed) - sorted in the same " + "order as MD5 would have been be computed. Sorting is first by " + "directory (if --recursive specified) then by filename") + parser_list.set_defaults(func=do_list) + + # Subparser for "compute" subcommand + parser_compute = subparsers.add_parser( + "compute", + parents=[switches_explicit, switches_optional_json, + switches_computation, switches_stats], + help="Compute MD5") + parser_compute.set_defaults(func=do_compute) + + # Subparser for "verify" subcommand + parser_verify = subparsers.add_parser( + "verify", + parents=[switches_required_json, switches_computation, switches_stats], + help="Computes MD5 and checks if it matches one in " + "...version_info.json file") + parser_verify.set_defaults(func=do_verify) + + # Subparser for "update" subcommand + parser_update = subparsers.add_parser( + "update", + parents=[switches_required_json, switches_computation, switches_stats], + help="Computes MD5 and updates ...version_info.json file with it") + parser_update.set_defaults(func=do_update) + + # Subparser for 'help' command + parser_help = subparsers.add_parser( + "help", add_help=False, + help="Prints help on given subcommand") + parser_help.add_argument( + "subcommand", metavar="SUBCOMMAND", nargs="?", + choices=subparsers.choices, + help="Name of subcommand to print help about (use " + + "\"%(prog)s --help\" to get list of all subcommands)") + parser_help.set_defaults(func=do_help, subparsers=subparsers, + argument_parser=argument_parser) + parser_help.set_defaults(supports_unknown_args=False) + + if not argv: + argument_parser.print_help() + sys.exit(1) + args = argument_parser.parse_args(argv) + + setup_logging() + + args.func(args) + + +if __name__ == "__main__": + try: + main(sys.argv[1:]) + except KeyboardInterrupt: + sys.exit(1) diff --git a/tools/geo_converters/g8l_info_schema.json b/tools/geo_converters/g8l_info_schema.json new file mode 100644 index 0000000..1235809 --- /dev/null +++ b/tools/geo_converters/g8l_info_schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://afc.broadcom.com/g8l_info_schema.json", + "description": "AFC Geospatial data release(s) descriptor", + "type": "object", + "properties": { + "data_releases": { + "description": "Geospatial data releases contained in current directory or its subdirectories, indexed by 'region_type' designators", + "type": "object", + "property_names": { + "type": "string", + "pattern": "^[:word:]{3}_[:word:]{2}$" + }, + "additionalProperties": { + "description": "Files of single type related to single region", + "type": "object", + "properties": { + "md5": { + "description": "MD5 of pertinent files", + "anyOf": [ + { "type": "string", "maxLength": 0 }, + { "type": "string", "pattern": "^[0-9a-fA-F]{32}$" } + ] + }, + "files": { + "description": "List of fbmatch-compatible filename patterns that constitute set of geospatial files. May include subdirectories", + "type": "array", + "minItems": 1, + "items": { "type": "string" } + }, + "version": { + "description": "Data version", + "type": "string" + }, + "origin": { + "description": "Brief description of what is the source data", + "type": "string" + }, + "origin_date": { + "description": "Download date in YYYY-MM-DD format", + "type": "string", + "format": "date" + }, + "geoid": { + "description": "Geoid name used for data transformation", + "type": "string" + }, + "translation": { + "description": "Description of source to final data transformation e.g. in a form of gdal command sequence. String with \\n separators and/or array of strings", + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "description": { + "description": "Gory details on data provenance and intended use. String with \\n separators and/or array of strings", + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + } + } + } + } + } +} + diff --git a/tools/geo_converters/geoutils.py b/tools/geo_converters/geoutils.py new file mode 100755 index 0000000..3ff15c7 --- /dev/null +++ b/tools/geo_converters/geoutils.py @@ -0,0 +1,1061 @@ +# Utility stuff for geospatial scripts + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=invalid-name, wrong-import-order +# pylint: disable=too-many-instance-attributes, too-few-public-methods +# pylint: disable=too-many-arguments, unnecessary-pass, too-many-locals +# pylint: disable=too-many-statements, too-many-branches, too-many-lines +import __main__ +from collections.abc import Callable, Iterable, Iterator, Sequence +import datetime +import glob +import inspect +import math +import logging +import multiprocessing +import os +import re +import shlex +import signal +import subprocess +import sys +import tempfile +from typing import Any, Dict, Generic, List, NamedTuple, Optional, Tuple, \ + TypeVar, Union + +try: + import psutil + HAS_PSUTIL = True +except ImportError: + HAS_PSUTIL = False + + +# Default scale for Int16/Int32 -> PNG UInt16 +DEFAULT_INT_PNG_SCALE = [-1000., 0., 0., 1000.] + +# Default scale for Int16/Int32 -> PNG UInt16 +DEFAULT_FLOAT_PNG_SCALE = [-1000., 0., 0., 5000.] + +# Default UInt16 PNG NoData value +DEFAULT_PNG_NO_DATA = 65535 + +# PNG driver name +PNG_FORMAT = "PNG" + +# PNG file extension +PNG_EXT = ".png" + + +def dp(*args, **kwargs) -> None: # pylint: disable=invalid-name + """Print debug message + + Arguments: + args -- Format and positional arguments. If latter present - formatted + with % + kwargs -- Keyword arguments. If present formatted with format() + """ + msg = args[0] if args else "" + if len(args) > 1: + msg = msg % args[1:] + if args and kwargs: + msg = msg.format(**kwargs) + cur_frame = inspect.currentframe() + assert (cur_frame is not None) and (cur_frame.f_back is not None) + frameinfo = inspect.getframeinfo(cur_frame.f_back) + print(f"DP {frameinfo.function}()@{frameinfo.lineno}: {msg}") + + +def setup_logging() -> None: + """ Set up logging """ + # Set up logging + console_handler = logging.StreamHandler() + console_handler.setFormatter( + logging.Formatter( + f"{os.path.basename(__main__.__file__)}. " + f"%(levelname)s: %(message)s")) + logging.getLogger().addHandler(console_handler) + logging.getLogger().setLevel(logging.INFO) + + +def error(msg: str) -> None: + """ Prints given msg as error message and exit abnormally """ + logging.error(msg) + sys.exit(1) + + +def error_if(cond: Any, msg: str) -> None: + """ If condition evaluates to true prints given msg as error message and + exits abnormally """ + if cond: + error(msg) + + +def warning(warnmsg: str) -> None: + """ Print given warning message """ + logging.warning(warnmsg) + + +def execute(args: List[str], env: Optional[Dict[str, str]] = None, + return_output: bool = False, fail_on_error: bool = True, + return_error: bool = False, disable_output: bool = False, + ignore_exit_code: bool = False) \ + -> Union[bool, Optional[str]]: + """ Execute command + + Arguments: + args -- List of command line parts + env -- Optional environment dictionary + return_output -- True to return output and not print it, False to print + output and not return it + fail_on_error -- True to fail on error, false to return None/False + return_error -- Return stderr on failure, None on success + disable_output -- True to disable all output + ignore_exit_code -- Assume success even though exit code is nonzero + Returns True/stdout on success, False/None on error + """ + if not (return_output or disable_output): + print(" ".join(shlex.quote(a) for a in args)) + try: + p = subprocess.run( + args, text=True, env=env, check=False, + stdout=subprocess.PIPE if (return_output or disable_output) + else None, + stderr=subprocess.PIPE if (return_output and (not fail_on_error)) + or return_error or disable_output + else None) + if (not p.returncode) or ignore_exit_code: + if return_output: + return p.stdout + if return_error: + return None + return True + if fail_on_error: + if p.stderr is not None: + print(p.stderr) + sys.exit(p.returncode) + errmsg = p.stderr + except OSError as ex: + if fail_on_error: + error(f"{ex}") + errmsg = str(ex) + if return_output: + return None + if return_error: + return errmsg + return False + + +def gdal_env() -> Optional[Dict[str, str]]: + """ Returns environment required to run GDAL utilities. None if not + needed """ + if os.name != "nt": + return None + if "PROJ_LIB" in os.environ: + return None + gw = execute(["where", "gdalwarp"], return_output=True, + fail_on_error=False) + error_if(gw is None, "'gdalwarp' not found") + assert isinstance(gw, str) + gw = gw.strip().split("\n")[0] + ret = dict(os.environ) + for path_to_proj in ("projlib", r"..\share\proj"): + proj_path = os.path.abspath(os.path.join(os.path.dirname(gw.strip()), + path_to_proj)) + if os.path.isfile(os.path.join(proj_path, "proj.db")): + ret["PROJ_LIB"] = proj_path + break + else: + warning("Projection library directory not found. GDAL utilities may " + "refuse to work properly") + return ret + + +class GdalInfo: + """ gdalinfo information from file + + Public attributes: + raw -- Raw 'gdalinfo' output. None on gdalinfo fail + top -- Top latitude in north-positive degrees or None + bottom -- Bottom latitude in north-positive degrees or None + left -- Left longitude in east-positive degrees or None + right -- Right longitude in north-positive degrees or None + pixel_size_lat -- Pixel size in latitudinal direction in degrees or None + pixel_size_lon -- Pixel size in longitudinal direction in degrees or None + is_geodetic -- True for geodetic coordinate system, False for map + projection coordinate system, None if unknown + data_types -- List of per-band data type names + proj_string -- Proj-4 coordinate system definition or None + valid_percent -- Maximum valid (not 'no data') percent among bands or None + min_value -- Minimum value among all bands or None + max_value -- Maximum value among all bands or None + """ + + def __init__(self, filename: str, + options: Optional[Union[str, List[str]]] = None, + fail_on_error: bool = True, remove_aux: bool = False) -> None: + """ Constructor + filename -- Name of file to inspect + options -- None or additional option or list of additional + options, list of additional gdalinfo options + fail_on_error -- True to fail on error + remove_aux -- True to remove created .aux.xml file + """ + self.raw: Optional[str] = None + self.top: Optional[float] = None + self.bottom: Optional[float] = None + self.left: Optional[float] = None + self.right: Optional[float] = None + self.pixel_size_lat: Optional[float] = None + self.pixel_size_lon: Optional[float] = None + self.is_geodetic: Optional[bool] = None + self.proj_string: Optional[str] = None + self.valid_percent: Optional[float] = None + self.min_value: Optional[float] = None + self.max_value: Optional[float] = None + self.data_types: List[str] = [] + if isinstance(options, str): + options = [options] + ret = execute(["gdalinfo"] + (options or []) + [filename], + env=gdal_env(), fail_on_error=fail_on_error, + return_output=True, disable_output=not fail_on_error) + if remove_aux and os.path.isfile(filename + ".aux.xml"): + try: + os.unlink(filename + ".aux.xml") + except OSError: + pass + if ret is None: + return + assert isinstance(ret, str) + self.raw = ret + r: Dict[str, Dict[str, float]] = {} + for prefix, lat_kind, lon_kind in [("Upper Left", "max", "min"), + ("Lower Left", "min", "min"), + ("Upper Right", "max", "max"), + ("Lower Right", "min", "max")]: + m = re.search( + prefix + r".+?\(\s*(?P\d+)d\s*(?P\d+)'" + r"\s*(?P\d+\.\d+)\"(?P[EW])," + r"\s*(?P\d+)d\s*(?P\d+)'\s*" + r"(?P\d+\.\d+)\"(?P[NS])\s*\)", self.raw) + if m is None: + continue + lat = \ + (float(m.group("lat_deg")) + + float(m.group("lat_min")) / 60 + + float(m.group("lat_sec")) / 3600) * \ + (1 if m.group("lat_hem") == "N" else -1) + lon = \ + (float(m.group("lon_deg")) + + float(m.group("lon_min")) / 60 + + float(m.group("lon_sec")) / 3600) * \ + (1 if m.group("lon_hem") == "E" else -1) + for key, value, kind in [("lat", lat, lat_kind), + ("lon", lon, lon_kind)]: + r.setdefault(key, {}) + r[key][kind] = \ + (min if kind == "min" else max)(r[key][kind], value) \ + if kind in r[key] else value + for attr, latlon, minmax in \ + [("top", "lat", "max"), ("bottom", "lat", "min"), + ("left", "lon", "min"), ("right", "lon", "max")]: + setattr(self, attr, r.get(latlon, {}).get(minmax)) + + if "CS[ellipsoidal" in self.raw: + self.is_geodetic = True + elif "CS[Cartesian" in self.raw: + self.is_geodetic = False + + m = re.search(r"Pixel Size\s*=\s*\(\s*([0-9.-]+),\s*([0-9.-]+)\)", + self.raw) + if m is not None: + self.pixel_size_lat = abs(float(m.group(2))) + self.pixel_size_lon = abs(float(m.group(1))) + + m = re.search(r"PROJ\.4 string is:\s*'(.+?)'", self.raw) + if m is not None: + self.proj_string = m.group(1) + + for row, attr, use_max in \ + [("STATISTICS_VALID_PERCENT", "valid_percent", True), + ("STATISTICS_MINIMUM", "min_value", False), + ("STATISTICS_MAXIMUM", "max_value", True)]: + for vps in re.findall(r"(?<=%s=)([0-9.eE]+)" % row, self.raw): + try: + read_value = float(vps) + current_value = getattr(self, attr) + if (current_value is None) or \ + (use_max and (read_value > current_value)) or \ + ((not use_max) and (read_value < current_value)): + setattr(self, attr, read_value) + except ValueError: + pass + self.data_types = re.findall(r"Band\s+\d+\s+.*Type=(\w+)", self.raw) + + def __bool__(self): + return self.raw is not None + + +# Source data type for WindowedProcessPool processor +WppSrc = TypeVar("WppSrc") +# Result data type for WindowedProcessPool processor +WppResult = TypeVar("WppResult") + +# Data type for task queue +ProcQueueTask = NamedTuple("ProcQueueTask", [("idx", int), ("data", Any)]) + +# Data type for results queue +ProcQueueResult = NamedTuple("ProcQueueResult", [("idx", int), ("data", Any)]) + + +class WindowedProcessPool(Iterable[WppResult], Generic[WppSrc, WppResult]): + """ Kind of multiprocessing pool that yields results in the same order as + they were produced and maintain a limited number of data items in flight + (when already processed but not yet consumed). + Yields data via iteration + Must be used as context manager (with 'with') + + Private attributes: + _producer -- Iterable that produces source data items + _processor -- Function that produces result from the source data + _max_in_flight -- Maximum number of produced but not yet consumed data + items + _task_queue -- Queue with source data for processing. Contains + ProcQueueTask objects or None (to signal worker to + stop) + _results_queue -- Queue with processing results. Contains ProcQueueResult + objects + _result_dict -- Dictionary that stores received processed results, + indexed by their sequential indices + _next_src_idx -- Sequential index of next produced data item + _next_result_idx -- Sequential index of next result to be consumed + _processes -- List of worker processes + _running -- True if worker processes running (not yet terminated) + """ + + def __init__(self, producer: Iterable[WppSrc], + processor: Callable[[WppSrc], WppResult], + max_in_flight: int, threads: Optional[int] = None) -> None: + """ Constructor + + Arguments: + producer -- Iterable that produces source data items + processor -- Function that produces result from the source data + max_in_flight -- Maximum number of produced but not yet consumed data + items + threads -- None or number of threads to use + """ + self._producer = producer + self._processor = processor + self._max_in_flight = max_in_flight + self._task_queue: multiprocessing.Queue[Optional[ProcQueueTask]] = \ + multiprocessing.Queue() + self._results_queue: multiprocessing.Queue[ProcQueueResult] = \ + multiprocessing.Queue() + self._result_dict: Dict[int, WppResult] = {} + self._next_src_idx = 0 + self._next_result_idx = 0 + original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) + self._processes: List[multiprocessing.Process] = [] + for _ in range(threads or multiprocessing.cpu_count()): + self._processes.append( + multiprocessing.Process( + target=self.__class__._processor_wrapper, + name="WindowedProcessPoolWorker", + kwargs={"task_queue": self._task_queue, + "results_queue": self._results_queue, + "processor": self._processor})) + self._processes[-1].start() + signal.signal(signal.SIGINT, original_sigint_handler) + self._running = True + + def __enter__(self) -> "WindowedProcessPool": + """ Context entry """ + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """ Context exit """ + self._terminate() + + def __iter__(self) -> Iterator[WppResult]: + """ Iteration over processed results """ + prod_next = self._producer.__iter__().__next__ + exhausted = False + while (not exhausted) or (self._next_src_idx > self._next_result_idx): + # Filling task queue + while (not exhausted) and \ + ((self._next_src_idx - self._next_result_idx) < + self._max_in_flight): + try: + src: WppSrc = prod_next() + self._task_queue.put( + ProcQueueTask(idx=self._next_src_idx, data=src)) + self._next_src_idx += 1 + except StopIteration: + exhausted = True + # Draining result queue + while (self._next_result_idx not in self._result_dict) or \ + (not self._results_queue.empty()): + queue_result: ProcQueueResult = \ + self._results_queue.get(block=True) + self._result_dict[queue_result.idx] = queue_result.data + # Popping next processed line + result: WppResult = self._result_dict[self._next_result_idx] + del self._result_dict[self._next_result_idx] + self._next_result_idx += 1 + yield result + self._terminate() + + def _terminate(self) -> None: + """ Terminate worker processes """ + if not self._running: + return + self._running = False + for _ in range(len(self._processes)): + self._task_queue.put(None) + for proc in self._processes: + proc.join() + + @classmethod + def _processor_wrapper(cls, task_queue: multiprocessing.Queue, + results_queue: multiprocessing.Queue, + processor: Callable[[WppSrc], WppResult]) -> None: + """ Worker process + + Arguments: + task_queue -- Queue for data to process (ProcQueueTask objects or + Nones to stop) + results_queue -- Queue for processing results + processor -- Processing function + """ + while True: + task: Optional[ProcQueueTask] = task_queue.get(block=True) + if task is None: + return + results_queue.put( + ProcQueueResult(idx=task.idx, data=processor(task.data))) + + +class Durator: + """ Prints operation duration. + This class intended to be used as context manager (with 'with') + + Private attributes: + _heading -- End message heading + _start_time -- Operation start time + + """ + + def __init__(self, heading: str) -> None: + """ Constructor + + Arguments: + heading -- End message heading + """ + self._start_time = datetime.datetime.now() + self._heading = heading + + def __enter__(self) -> None: + """ Context entry """ + pass + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """ Context exit """ + if exc_val is not None: + return + duration = datetime.datetime.now() - self._start_time + print(f"{self._heading} {self.__class__.duration_to_hms(duration)}") + + @classmethod + def duration_to_hms(cls, duration: datetime.timedelta) -> str: + """ Converts duration to printable representation """ + total_seconds = int(duration.total_seconds()) + seconds = total_seconds % 60 + total_seconds //= 60 + minutes = total_seconds % 60 + total_seconds //= 60 + hours = total_seconds + ret = "" + if hours: + ret += f" {hours}h" + if hours or minutes: + ret += f" {minutes}m" + ret += f" {seconds}s" + return ret[1:] + + +def warp_resamplings() -> List[str]: + """ List of supported resampling methods """ + warp_help = \ + execute(["gdalwarp", "--help"], env=gdal_env(), return_output=True, + ignore_exit_code=True) + assert isinstance(warp_help, str) + m = re.search(r"Available resampling methods:\s*((\n\s+\S+.*)+)", + warp_help) + error_if(not m, "Can't fetch list of available resampling methods") + assert m is not None + methods = m.group(1) + for remove in (r"\s", r"\(.*?\)", r"\."): + methods = re.sub(remove, "", methods) + return methods.split(",") + + +def translate_datatypes() -> List[str]: + """ List of data types, supported by gdal_translate """ + trans_help = \ + execute(["gdal_translate", "--help"], env=gdal_env(), + return_output=True, ignore_exit_code=True) + assert isinstance(trans_help, str) + m = re.search(r"-ot\s.*?{((.|\n)+?)\}", trans_help) + error_if(not m, "Can't fetch list of available data types") + assert m is not None + return re.split(r"\W+", m.group(1)) + + +def warp(src: str, dst: str, resampling: str, + top: Optional[float] = None, + bottom: Optional[float] = None, + left: Optional[float] = None, + right: Optional[float] = None, + pixel_size_lat: Optional[float] = None, + pixel_size_lon: Optional[float] = None, + src_geoid: Optional[str] = None, + dst_geoid: Optional[str] = None, + out_format: Optional[str] = None, + format_params: Optional[List[str]] = None, + data_type: Optional[str] = None, + center_lon_180: bool = False, + overwrite: bool = False, + quiet: bool = False) -> Tuple[bool, Optional[str]]: + """ Perform conversion with gdalwarp + + Arguments: + src -- Source file name + dst -- Resulting file name + resampling -- Resampling method (value for gdalwarp's -r) + top -- Optional maximum latitude (for cropping) + bottom -- Optional minimum latitude (for cropping) + left -- Optional minimum longitude (for cropping) + right -- Optional maximum longitude (for cropping) + pixel_size_lat -- Optional pixel size in latitudinal direction + pixel_size_lon -- Optional pixel size in longitudinal direction + src_geoid -- Optional source geoid file name + dst_geoid -- Optional destination geoid file name + out_format -- Optional output format (for cases when it can't be + discerned from extension) + format_params -- Optional list of format parameters + data_type -- Optional name of data type in resulting file + center_lon_180 -- True to center result around 180 longitude + overwrite -- True to overwrite resulting file + quiet -- True to print nothing but return messages as part of + return tuple and also to return negative success status + on failure. False to print everything that needs to be + and fail on failure + Returns (success, msg) tuple. 'msg' is None if quiet is False + """ + exec_args: List[str] = ["gdalwarp", "-r", resampling] + + if src_geoid: + gi = GdalInfo(src, options="-proj4", fail_on_error=not quiet) + if not gi: + return (False, f"{src}: gdalinfo inspection failed") + if gi.proj_string is None: + err_msg = f"{src}: can't retrieve projection information" + error_if(not quiet, err_msg) + return (False, err_msg) + exec_args += ["-s_srs", f"{gi.proj_string} +geoidgrids={src_geoid}"] + + exec_args += ["-t_srs", + f"+proj=longlat +datum=WGS84" + f"{(' +geoidgrids=' + dst_geoid) if dst_geoid else ''}"] + + error_if(not ((top is None) == (bottom is None) == (left is None) == + (right is None)), + "Target boundaries defined partially. They should either be " + "defined fully or not at all") + if left is not None: + assert right is not None + while right <= left: + right += 360 + while (right - left) > 360: + right -= 360 + exec_args += ["-te", str(left), str(bottom), + str(right), str(top)] + + error_if(not ((pixel_size_lat is None) == (pixel_size_lon is None)), + "Pixel sizes defined partially. They should either be defined " + "fully or not at all") + if pixel_size_lat is not None: + exec_args += ["-tr", str(pixel_size_lon), str(pixel_size_lat)] + + if out_format: + exec_args += ["-of", out_format] + for fp in (format_params or []): + exec_args += ["-co", fp] + if data_type: + exec_args += ["-ot", data_type] + if center_lon_180: + exec_args += ["--config", "CENTER_LONG", "180"] + if overwrite: + exec_args += ["-overwrite"] + exec_args += [src, dst] + + ret = execute(exec_args, env=gdal_env(), disable_output=quiet, + return_error=quiet) + if quiet: + assert not isinstance(ret, bool) + return \ + (ret is None, + " ".join(shlex.quote(a) for a in exec_args) + + ("" if ret is None else ("\n" + ret))) + return (True, None) + + +def threads_arg(arg: Optional[str]) -> int: + """ Returns thread count for given threads parameter """ + total = multiprocessing.cpu_count() + if arg is None: + return total + try: + if arg.endswith("%"): + ret = int(float(arg[:-1]) * total / 100) + else: + ret = int(arg) + if ret < 0: + ret = total + ret + return max(1, min(total, ret)) + except ValueError as ex: + error(f"Invalid thread count format '{arg}': {ex}") + return 0 # Will never happen, pacifying linters + + +class Boundaries: + """ Map boundaries + + Public attributes: + is_geodetic -- True for geodetic (lat/lon) coordinate system, + False for map projection, None if not known + top -- Top latitude in degrees + bottom -- Bottom latitude in degrees + left -- Left longitude in degrees + right -- Right longitude in degrees + edges_overridden -- True if edges overridden - with command line + parameters, map->geodetic conversion or adjustment + pixel_size_lat -- Pixel size in latitudinal direction + pixel_size_lon -- Pixel size in longitudinal direction + pixel_size_overridden -- True if pixel size overridden - with command line + parameters, map->geodetic conversion or adjustment + cross_180 -- Crosses 180 longitude. None if not known + """ + + def __init__(self, filename: Optional[str] = None, + top: Optional[float] = None, bottom: Optional[float] = None, + left: Optional[float] = None, right: Optional[float] = None, + pixel_size: Optional[float] = None, + pixels_per_degree: Optional[float] = None, + round_boundaries_to_degree: bool = False, + round_all_boundaries_to_degree: bool = False, + round_pixels_to_degree: bool = False) -> None: + """ Constructor + + Arguments: + filename -- Optional GDAL file name from which to + gdalinfo boundaries + top -- Optional top boundary (north-positive + degrees) + bottom -- Optional bottom boundary + (north-positive degrees) + left -- Optional left boundary (east-positive + degrees) + right -- Optional right boundary + (east-positive degrees) + pixel_size -- Optional pixel size in degrees + pixels_per_degree -- Optional number of pixels per degree + round_boundaries_to_degree -- Round not explicitly specified + boundaries to next whole degree in + outward direction + round_all_boundaries_to_degree -- Round all boundaries to next whole + degree in outward direction + round_pixels_to_degree -- Round not explicitly specified pixel + sizes to whole number of pixels per + degree + """ + self.top = top + self.bottom = bottom + self.left = left + self.right = right + self.pixel_size_lat: Optional[float] + self.pixel_size_lon: Optional[float] + self.cross_180: Optional[bool] = None + if pixel_size is not None: + self.pixel_size_lat = self.pixel_size_lon = pixel_size + elif pixels_per_degree is not None: + self.pixel_size_lat = self.pixel_size_lon = 1 / pixels_per_degree + else: + self.pixel_size_lat = self.pixel_size_lon = None + self.edges_overridden = \ + any(v is not None for v in + (self.top, self.bottom, self.left, self.right)) + self.pixel_size_overridden = \ + any(v is not None for v in + (self.pixel_size_lat, self.pixel_size_lon)) + if not all(v is not None for v in + (self.top, self.bottom, self.left, self.right, + self.pixel_size_lat, self.pixel_size_lon)) and \ + (filename is not None): + gi = GdalInfo(filename) + resample = not gi.is_geodetic + if resample: + temp_file: Optional[str] = None + try: + error_if((gi.right is None) or (gi.left is None), + f"Can't determine geodetic boundaries of " + f"'{filename}' from its 'gdalinfo' information") + assert (gi.right is not None) and (gi.left is not None) + h, temp_file = tempfile.mkstemp(suffix=".tif") + os.close(h) + warp(src=filename, dst=temp_file, resampling="near", + pixel_size_lat=0.01, pixel_size_lon=0.01, + center_lon_180=gi.right < gi.left, overwrite=True, + quiet=True) + gi = GdalInfo(temp_file) + self.edges_overridden = True + finally: + if temp_file: + os.unlink(temp_file) + for attr in ["top", "bottom", "left", "right", + "pixel_size_lat", "pixel_size_lon"]: + if getattr(self, attr) is None: + setattr(self, attr, getattr(gi, attr)) + if round_boundaries_to_degree or round_all_boundaries_to_degree: + for attr, arg, ceil_floor in \ + [("top", top, math.ceil), ("bottom", bottom, math.floor), + ("left", left, math.floor), ("right", right, math.ceil)]: + if (arg is not None) and (not round_all_boundaries_to_degree): + continue + v = getattr(self, attr) + if v is None: + continue + self.edges_overridden = True + setattr(self, attr, ceil_floor(v)) + if round_pixels_to_degree and \ + (pixel_size is None) and (pixels_per_degree is None): + for attr in ("pixel_size_lat", "pixel_size_lon"): + v = getattr(self, attr) + if v is None: + continue + self.pixel_size_overridden = True + setattr(self, attr, 1 / round(1 / v)) + if (self.left is not None) and (self.right is not None): + left, right = self.left, self.right + while left > 180: + left -= 360 + right -= 360 + while left <= -180: + left += 360 + right += 360 + assert -180 < left <= 180 + self.cross_180 = right >= 180 + + def __eq__(self, other: Any) -> bool: + """ Equality comparison """ + return isinstance(other, self.__class__) and \ + (self._values_tuple() == other._values_tuple()) + + def __hash__(self) -> int: + """ Hash """ + return hash(self._values_tuple()) + + def contains(self, other: "Boundaries", extension: float = 0) -> bool: + """ True if given boundaries lie inside this boundaries. Artificially + extends self by given amount """ + assert (self.top is not None) and (self.bottom is not None) + assert (self.left is not None) and (self.right is not None) + assert (other.top is not None) and (other.bottom is not None) + assert (other.left is not None) and (other.right is not None) + if (other.top > (self.top + extension)) or \ + (other.bottom < (self.bottom - extension)): + return False + + self_left, self_right, other_left, other_right, \ + self_circular, other_circular = \ + self.__class__.align_longitude_ranges( + self.left, self.right, other.left, other.right) + if not self_circular: + self_left -= extension + self_right += extension + return self_circular or \ + ((not other_circular) and + ((self_left <= other_left) and (self_right >= other_right))) + + def intersects(self, other: "Boundaries") -> bool: + """ True if given boundaries intersect with this boundaries """ + assert (self.top is not None) and (self.bottom is not None) + assert (self.left is not None) and (self.right is not None) + assert (other.top is not None) and (other.bottom is not None) + assert (other.left is not None) and (other.right is not None) + if (other.bottom > self.top) or (other.top < self.bottom): + return False + + self_left, self_right, other_left, other_right, \ + self_circular, other_circular = \ + self.__class__.align_longitude_ranges( + self.left, self.right, other.left, other.right) + return self_circular or other_circular or \ + ((other_left <= self_right) and (other_right >= self_left)) + + def combine(self, other: "Boundaries", + round_boundaries_to_degree: bool = False) -> "Boundaries": + """ Returns boundaries that contains both this and other boundaries + (optionally rounding boundaries to next outward degree. Pixel sizes are + not set + """ + assert (self.top is not None) and (self.bottom is not None) + assert (self.left is not None) and (self.right is not None) + assert (other.top is not None) and (other.bottom is not None) + assert (other.left is not None) and (other.right is not None) + + self_left, self_right, other_left, other_right, \ + self_circular, other_circular = \ + self.__class__.align_longitude_ranges( + self.left, self.right, other.left, other.right) + + return Boundaries( + top=max(self.top, other.top), + bottom=min(self.bottom, other.bottom), + left=self_left if self_circular else + (other_left if other_circular else min(self_left, other_left)), + right=self_right if self_circular else + (other_right if other_circular else max(self_right, other_right)), + round_all_boundaries_to_degree=round_boundaries_to_degree and + (not (self_circular or other_circular))) + + def crop(self, other: "Boundaries", + round_boundaries_to_degree: bool = False) \ + -> Optional["Boundaries"]: + """ Returns self, cropped by other boundaries """ + assert (self.top is not None) and (self.bottom is not None) + assert (self.left is not None) and (self.right is not None) + ret_top = self.top if other.top is None else min(self.top, other.top) + ret_bottom = self.bottom if other.bottom is None \ + else max(self.bottom, other.bottom) + if self.top < self.bottom: + return None + + assert (other.left is None) == (other.right is None) + if other.left is not None: + assert other.right is not None + self_left, self_right, other_left, other_right, \ + self_circular, other_circular = \ + self.__class__.align_longitude_ranges( + self.left, self.right, other.left, other.right) + ret_left = other_left if self_circular else \ + (self_left if other_circular else max(self_left, other_left)) + ret_right = other_right if self_circular else \ + (self_right if other_circular + else min(self_right, other_right)) + if ret_left > ret_right: + return None + else: + ret_left = self.left + ret_right = self.right + return Boundaries( + top=ret_top, bottom=ret_bottom, left=ret_left, right=ret_right, + round_all_boundaries_to_degree=round_boundaries_to_degree) + + def _values_tuple(self) -> Tuple[Optional[Union[float, bool]], ...]: + """ Tuple containing all fields """ + return (self.top, self.bottom, self.left, self.right, + self.pixel_size_lat, self.pixel_size_lon, + self.edges_overridden, self.pixel_size_overridden) + + def __str__(self) -> str: + """ String representation (for development purposes) """ + assert (self.top is not None) and (self.bottom is not None) + assert (self.left is not None) and (self.right is not None) + return (f"[{abs(self.bottom)}{'N' if self.bottom >= 0 else 'S'} - " + f"{abs(self.top)}{'N' if self.top >= 0 else 'S'}] X " + f"[{abs(self.left)}{'E' if self.left >= 0 else 'W'} - " + f"{abs(self.right)}{'E' if self.right >= 0 else 'W'}]") + + @classmethod + def align_longitude_ranges(cls, left1: float, right1: float, left2: float, + right2: float) \ + -> Tuple[float, float, float, float, bool, bool]: + """ For given pair of longitude ranges return another pair that are + properly ordered and lie in same 360 degree range, also returns if + source or destination are circular + """ + circular1 = \ + (left1 != right1) and (abs(math.fmod(right1 - left1, 360)) < 1e-6) + circular2 = \ + (left2 != right2) and (abs(math.fmod(right2 - left2, 360)) < 1e-6) + while right1 <= left1: + right1 += 360 + while (right1 - left1) > 360: + right1 -= 360 + + while right2 <= left2: + right2 += 360 + while (right2 - left2) > 360: + right2 -= 360 + + if not (circular1 or circular2): + while (right1 - left2) >= 360: + left1 -= 360 + right1 -= 360 + while (left1 - right2) <= -360: + left1 += 360 + right1 += 360 + return (left1, right1, left2, right2, circular1, circular2) + + +class Geoids: + """ Collection of geoid files + + Private attributes: + _geoids -- List of (boundary, file) tuples for geoid files + _extension -- Amount of geoid coverage extension when coverage is tested + """ + + def __init__(self, geoids: Optional[Union[str, List[str]]], + extension: float = 0) -> None: + """ Constructor + + Arguments: + geoids -- None or name of geoid files or list of names of geoid + files. Geoid file names may include wildcard symbols. List + items expected to follow in order of preference decrease + extension -- Artificially extend geoid boundaries by this amount when + checking coverage - to accommodate for margins + """ + self._extension = extension + if isinstance(geoids, str): + geoids = [geoids] + elif geoids is None: + geoids = [] + self._geoids: List[Tuple[Boundaries, str]] = [] + for geoid_str in geoids: + files = glob.glob(geoid_str) + if (not files) and (not os.path.dirname(geoid_str)): + files = glob.glob(os.path.join(os.path.dirname(__file__), + geoid_str)) + error_if(not files, + f"No geoid files matching '{geoid_str}' found") + for filename in files: + self._geoids.append((Boundaries(filename), filename)) + + def __bool__(self): + """ True if collection is not empty """ + return bool(self._geoids) + + def geoid_for(self, boundaries: Boundaries, + fail_if_not_found: bool = True) -> Optional[str]: + """ Finds geoid containing given boundaries + + Arguments: + boundaries -- Boundaries to look geoid for. First fit + (ostensibly - preferable) geoid is expected to be + found + fail_if_not_found -- True to fail if nothing found, False to return + None + Returns name of geoid file, None if not found + """ + for gb, gn in self._geoids: + if gb.contains(boundaries, extension=self._extension): + return gn + error_if(fail_if_not_found, f"Geoid for {boundaries} not found") + return None + + +def get_scale(arg_scale: Optional[List[float]], src_data_types: Sequence[str], + dst_format: Optional[str], dst_data_type: Optional[str], + dst_ext: Optional[str]) -> Optional[List[float]]: + """ Apply default to scale parameter if destination is PNG and has + different data type + + Arguments: + arg_scale -- Optional --scale parameter value + src_data_types -- Collection of data types in source files + dst_format -- Optional GDAL file type format for destination file + dst_data_type -- Optional resulting data type + dst_ext -- Optional extension of destination file + Returns either what was passed with --scale or proper PNG-related default + """ + if (arg_scale is not None) or (not src_data_types) or \ + (not _is_png(gdal_format=dst_format, file_ext=dst_ext)) or \ + all(v in ("Byte", "UInt16") for v in src_data_types) or \ + (dst_data_type not in (None, "UInt16")): + return arg_scale + if any("Float" for v in src_data_types): + return DEFAULT_FLOAT_PNG_SCALE + return DEFAULT_INT_PNG_SCALE + + +def get_no_data(arg_no_data: Optional[str], src_data_types: Sequence[str], + dst_format: Optional[str], dst_data_type: Optional[str], + dst_ext: Optional[str]) -> Optional[str]: + """ Apply default to NoData parameter if destination is PNG and has + different data type + + Arguments: + arg_no_data -- Optional --no_data parameter value + src_data_types -- Collection of data types in source files + dst_format -- Optional GDAL file type format for destination file + dst_data_type -- Optional resulting data type + dst_ext -- Optional extension of destination file + Returns either what was passed with --no_data or proper PNG-related default + """ + if (arg_no_data is not None) or (not src_data_types) or \ + (not _is_png(gdal_format=dst_format, file_ext=dst_ext)) or \ + all(v in ("Byte", "UInt16") for v in src_data_types) or\ + (dst_data_type not in (None, "UInt16")): + return arg_no_data + return str(DEFAULT_PNG_NO_DATA) + + +def _is_png(gdal_format: Optional[str], file_ext: Optional[str]) -> bool: + """ Checksd if given file information corresponds to PNG file + + Argument: + gdal_format -- Optional value of --format switch + file_ext -- File extenmsion + True if at least ond of given arguments corresponds to PNG + """ + return (gdal_format == PNG_FORMAT) or (file_ext == PNG_EXT) + + +def get_resampling( + arg_resampling: Optional[str], src_data_types: Sequence[str], + dst_data_type: Optional[str]) -> str: + """ Provides resulting resampling method + + Arguments: + arg_resampling -- Optional resampling algorithm from command line + src_data_types -- Collection of data types in source files + dst_data_type -- Optional resulting data type + """ + if arg_resampling: + return arg_resampling + error_if(not (src_data_types or dst_data_type), + "--resampling can't be derived and must be specified explicitly") + return \ + "near" if "Byte" in list(src_data_types) + [dst_data_type] else "cubic" + + +def nice() -> None: + """ Lower priority of current process """ + if os.name == "nt": + if HAS_PSUTIL: + psutil.Process().nice(psutil.BELOW_NORMAL_PRIORITY_CLASS) + else: + warning("Process can't be niced because 'psutil' Python module is " + "not installed") + elif os.name == "posix": + os.nice(1) + else: + warning("No idea how to nice in this environment") diff --git a/tools/geo_converters/lidar_merge.py b/tools/geo_converters/lidar_merge.py new file mode 100755 index 0000000..b3598f6 --- /dev/null +++ b/tools/geo_converters/lidar_merge.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 +# Merges LiDAR files into one-file-per-agglomeration (gdal_merge.py phase) + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=wildcard-import, unused-wildcard-import, too-many-arguments +# pylint: disable=too-many-locals, invalid-name, too-many-return-statements +# pylint: disable=too-many-branches, too-many-statements + +import argparse +import csv +import datetime +import enum +import multiprocessing.pool +import os +import shlex +import sys +from typing import List, Optional, Set, Tuple + +from geoutils import * + +# Pattern for .csv name that corresponds to locality name +CSV_PATTERN = "%s_info.csv" + +# Default resampling to use in gdalbuildvrt +DEFAULT_RESAMPLING = "cubic" + +# Conversion result status +ConvStatus = enum.Enum("ConvStatus", ["Success", "Exists", "Error"]) + +_EPILOG = """This script expects that GDAL utilities are installed and in PATH. +Also it expects that as part of this installation proj/proj.db is somehow +installed as well (if it is, but gdal_translate still complains - set PROJ_LIB +environment variable to point to this proj directory). +Some usage examples: + +- Merge lidars in proj_lidar_2019, result in MERGED_LIDARS directrory, + use 8 CPUs on lowered (nice) priority, + use ZSTD compression in resulted files: + $ lidar_merge.py --threads 8 --nice --proj_param BIGTIFF=YES \\ + --proj_param COMPRESS=ZSTD proj_lidar_2019 MERGED_LIDARS + On 8 CPUs this takes ~5 hours. +""" + + +class ConvResult(NamedTuple): + """ Conversion result """ + + # Name of converted file + locality: str + + # Conversion status + status: ConvStatus + + # Conversion duration + duration: datetime.timedelta + + # Optional error message + msg: Optional[str] = None + + # gdal_merge command line used + command_line: Optional[str] = None + + +def merge_worker( + src_dir: str, dst_dir: str, locality: str, out_ext: Optional[str], + out_format: Optional[str], resampling: str, format_params: List[str], + overwrite: bool, verbose: bool) -> ConvResult: + """ Merge worker + + Arguments: + src_dir -- Source directory + dst_dir -- Destination directory + locality -- Locality (subdirectory with .tif files) + out_ext -- Optional output file extension + out_format -- Optional output format (GDAL driver name) + resampling -- Resampling for gdalvrt + format_params -- Optional output format parameters + overwrite -- True to overwrite existing files + verbose -- Print gdal_merge output directly and verbosely + Return Conversion result + """ + temp_filename_vrt = os.path.join(dst_dir, locality + ".vrt") + temp_filename_xml: Optional[str] = None + temp_filename: Optional[str] = None + try: + start_time = datetime.datetime.now() + warn_msg: str = "" + srcs: List[str] = [] + src_extensions: Set[str] = set() + src_pixel_sizes: Set[Tuple[Optional[float], Optional[float]]] = set() + csv_file_nmame = os.path.join(src_dir, CSV_PATTERN % locality) + with open(csv_file_nmame, newline='', encoding="utf-8") as csv_f: + for row in csv.DictReader(csv_f): + if "FILE" not in row: + return \ + ConvResult( + locality=locality, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg=f"Invalid '{csv_file_nmame}' file structure") + src_filename = os.path.join(src_dir, locality, row["FILE"]) + if not os.path.isfile(src_filename): + return \ + ConvResult( + locality=locality, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg=f"Source file '{src_filename}' of locality " + f"'{locality}' not found") + gi = GdalInfo(src_filename, fail_on_error=False) + if not gi: + return \ + ConvResult( + locality=locality, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg=f"Unable to inspect '{src_filename}' of " + f"locality '{locality}' with gdalinfo") + src_extensions.add(os.path.splitext(src_filename)[1]) + src_pixel_sizes.add((gi.pixel_size_lat, gi.pixel_size_lon)) + srcs.append(src_filename) + if not srcs: + return \ + ConvResult( + locality=locality, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg=f"Locality '{locality}' have no source files") + srcs.reverse() + if len(src_pixel_sizes) != 1: + if warn_msg: + warn_msg += "\n" + warn_msg += f"Locality '{locality}' has different pixel sizes " \ + f"in different source files. Pixel sizes of the last file " \ + f"({srcs[0]}) will be used" + if out_ext is None: + if len(src_extensions) != 1: + return \ + ConvResult( + locality=locality, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg=f"Source files for locality '{locality}' have " + f"have different extensions. Extension for output " + f"file must be explicitly specified") + out_ext = list(src_extensions)[0] + dst_filename = os.path.join(dst_dir, locality) + out_ext + temp_filename = \ + os.path.join(dst_dir, locality) + ".incomplete" + out_ext + temp_filename_xml = temp_filename + ".aux.xml" + if os.path.isfile(dst_filename): + if not overwrite: + return ConvResult( + locality=locality, + status=ConvStatus.Exists, + duration=datetime.datetime.now() - + start_time) + + # Building VRT for filelist + vrt_args = ["gdalbuildvrt", "-ignore_srcmaskband"] + if resampling: + vrt_args += ["-r", resampling] + vrt_args.append(temp_filename_vrt) + vrt_args += srcs + command_line = " ".join(shlex.quote(arg) for arg in vrt_args) + exec_result = execute(vrt_args, env=gdal_env(), + disable_output=not verbose, + return_error=not verbose, fail_on_error=verbose) + if verbose: + assert exec_result is True + elif exec_result is not None: + assert isinstance(exec_result, str) + return ConvResult(locality=locality, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg=exec_result, command_line=command_line) + + # Translating VRT to single file + trans_args = ["gdal_translate", "-strict"] + if out_format: + trans_args += ["-of", out_format] + for fp in format_params: + trans_args += ["-co", fp] + trans_args += [temp_filename_vrt, temp_filename] + command_line = " ".join(shlex.quote(arg) for arg in trans_args) + exec_result = execute(trans_args, env=gdal_env(), + disable_output=not verbose, + return_error=not verbose, fail_on_error=verbose) + if verbose: + assert exec_result is True + elif exec_result is not None: + assert isinstance(exec_result, str) + return ConvResult(locality=locality, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg=exec_result, command_line=command_line) + if os.path.isfile(dst_filename): + if verbose: + print(f"Removing '{dst_filename}'") + os.unlink(dst_filename) + if verbose: + print(f"Renaming '{temp_filename}' to '{dst_filename}'") + os.rename(temp_filename, dst_filename) + return ConvResult(locality=locality, status=ConvStatus.Success, + duration=datetime.datetime.now() - start_time, + msg=warn_msg, command_line=command_line) + except (Exception, KeyboardInterrupt, SystemExit) as ex: + return ConvResult(locality=locality, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg=repr(ex)) + finally: + for filename in (temp_filename, temp_filename_xml, temp_filename_vrt): + try: + if filename and os.path.isfile(filename): + os.unlink(filename) + except OSError: + pass + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + argument_parser = argparse.ArgumentParser( + description="Merges LiDAR files into one-file-per-agglomeration", + formatter_class=argparse.RawDescriptionHelpFormatter, epilog=_EPILOG) + argument_parser.add_argument( + "--overwrite", action="store_true", + help="Overwrite existing files. By default they are skipped (to " + "achieve easy resumption of the process)") + argument_parser.add_argument( + "--format", metavar="GDAL_DRIVER_NAME", + help="File format expressed as GDAL driver name (see " + "https://gdal.org/drivers/raster/index.html ). By default derived " + "from target file extension") + argument_parser.add_argument( + "--format_param", metavar="NAME=VALUE", action="append", + default=[], + help="Format option. May be specified several times") + argument_parser.add_argument( + "--threads", metavar="COUNT_OR_PERCENT%", + help="Number of threads to use. If positive - number of threads, if " + "negative - number of CPU cores NOT to use, if followed by `%%` - " + "percent of CPU cores. Default is total number of CPU cores") + argument_parser.add_argument( + "--nice", action="store_true", + help="Lower priority of this process and its subprocesses") + argument_parser.add_argument( + "--locality", metavar="LOCALITY_NAME", action="append", + help="Do the merge only for given locality (name of subdirectory with " + "LiDAR files, initial part of .csv file name). This parameter may be " + "specified more than once. Default is to do the merge for all " + "localities") + argument_parser.add_argument( + "--resampling", metavar="METHOD", default=DEFAULT_RESAMPLING, + choices=warp_resamplings(), + help=f"Resampling method to use. See " + f"https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r for " + f"explanations. Default is '{DEFAULT_RESAMPLING}'") + argument_parser.add_argument( + "--out_ext", metavar=".EXT", + help="Extension for output files. Default is to keep original " + "extension") + argument_parser.add_argument( + "--verbose", action="store_true", + help="Process localities sequentially, print verbose gdal_translate " + "output in real time, fail on first error. For debug purposes") + argument_parser.add_argument( + "SRC_DIR", + help="Source directory - root of all LiDAR files, where .csv files " + "are") + argument_parser.add_argument( + "DST_DIR", help="Target directory") + + if not argv: + argument_parser.print_help() + sys.exit(1) + args = argument_parser.parse_args(argv) + + setup_logging() + + if args.nice: + nice() + + error_if(not os.path.isdir(args.SRC_DIR), + f"Source directory '{args.SRC_DIR}' not found") + + start_time = datetime.datetime.now() + + if not os.path.isdir(args.DST_DIR): + os.makedirs(args.DST_DIR) + localities: List[str] = [] + if args.locality: + for locality in args.locality: + dir_name = os.path.join(args.SRC_DIR, locality) + csv_name = os.path.join(args.SRC_DIR, CSV_PATTERN % locality) + error_if( + not (os.path.isdir(dir_name) and os.path.isfile(csv_name)), + f"Source data for locality '{locality}' not found in " + f"'{args.SRC_DIR}'") + localities.append(locality) + else: + for locality in os.listdir(args.SRC_DIR): + dir_name = os.path.join(args.SRC_DIR, locality) + csv_name = os.path.join(args.SRC_DIR, CSV_PATTERN % locality) + if os.path.isdir(dir_name) and os.path.isfile(csv_name): + localities.append(locality) + error_if(not localities, + f"No LiDAR localities found in '{args.SRC_DIR}'") + + completed_count = [0] # List to facilitate closure in completer() + total_count = len(localities) + skipped_localities: List[str] = [] + failed_localities: List[str] = [] + + def completer(cr: ConvResult) -> None: + """ Processes completion of a single file """ + completed_count[0] += 1 + msg = f"{completed_count[0]} of {total_count} " \ + f"({completed_count[0] * 100 // total_count}%) {cr.locality}: " + if (not args.verbose) and (cr.command_line is not None): + msg += "\n" + cr.command_line + "\n" + if cr.status == ConvStatus.Exists: + msg += "Destination file exists. Skipped" + skipped_localities.append(cr.locality) + elif cr.status == ConvStatus.Error: + error_if(args.verbose, cr.msg) + if cr.msg is not None: + msg += "\n" + cr.msg + failed_localities.append(cr.locality) + else: + assert cr.status == ConvStatus.Success + if cr.msg is not None: + msg += "\n" + cr.msg + msg += f"Converted in {Durator.duration_to_hms(cr.duration)}" + print(msg) + + common_kwargs = { + "src_dir": args.SRC_DIR, + "dst_dir": args.DST_DIR, + "out_ext": args.out_ext, + "out_format": args.format, + "resampling": args.resampling, + "format_params": args.format_param, + "overwrite": args.overwrite, + "verbose": args.verbose} + try: + if args.verbose: + for locality in localities: + completer(merge_worker(locality=locality, **common_kwargs)) + else: + original_sigint_handler = \ + signal.signal(signal.SIGINT, signal.SIG_IGN) + with multiprocessing.pool.ThreadPool( + processes=threads_arg(args.threads)) as pool: + signal.signal(signal.SIGINT, original_sigint_handler) + for locality in localities: + kwds = common_kwargs.copy() + kwds["locality"] = locality + pool.apply_async(merge_worker, kwds=kwds, + callback=completer) + pool.close() + pool.join() + if skipped_localities: + print(f"{len(skipped_localities)} previously processed localities " + "skipped") + if failed_localities: + print("Following localities were not converted due to errors:") + for locality in sorted(failed_localities): + print(f" {locality}") + print( + f"Total duration: " + f"{Durator.duration_to_hms(datetime.datetime.now() - start_time)}") + except KeyboardInterrupt: + sys.exit(1) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tools/geo_converters/make_population_db.py b/tools/geo_converters/make_population_db.py new file mode 100644 index 0000000..85603a8 --- /dev/null +++ b/tools/geo_converters/make_population_db.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +""" Preparing SQLite DB with population density data """ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# pylint: disable=wildcard-import, too-many-locals, invalid-name +# pylint: disable=too-many-branches, too-many-statements, wrong-import-order + +import argparse +import datetime +import math +import os +from osgeo import gdal +import sqlite3 +import struct +import sys +import tempfile +from typing import Dict, List, Optional + +from geoutils import error, error_if, setup_logging, warp + +# Default resolution (tile size) of population database in seconds +DEFAULT_RESOLUTION_SEC = 60 + +# Population database table name +TABLE_NAME = "population_density" +# Name of field with cumulative population density (ultimately - normed to 1) +CUMULATIVE_DENSITY_FIELD = "cumulative_density" + +# Names of tile boundary fields +MIN_LAT_FIELD = "min_lat" +MAX_LAT_FIELD = "max_lat" +MIN_LON_FIELD = "min_lon" +MAX_LON_FIELD = "max_lon" + +# Database bulk write saize (in number of records) +BULK_WRITE_THRESHOLD = 1000 + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + argument_parser = argparse.ArgumentParser( + description="Make Population density DB for als_load_tool.py") + argument_parser.add_argument( + "--resolution", metavar="SECONDS", default=DEFAULT_RESOLUTION_SEC, + type=float, + help=f"Resolution (tile size) of resulting database in seconds. " + f"Default is {DEFAULT_RESOLUTION_SEC}") + argument_parser.add_argument( + "--overwrite", action="store_true", + help="Overwrite target database if it is already exists") + argument_parser.add_argument( + "--center_lon_180", action="store_true", + help="Better be set for countries crossing 180 longitude (e.g for " + "USA with Alaska)") + argument_parser.add_argument( + "FROM", help="Source GDAL-compatible population density file") + argument_parser.add_argument( + "TO", help="Resulting sqlite3 file") + + if not argv: + argument_parser.print_help() + sys.exit(1) + args = argument_parser.parse_args(argv) + + setup_logging() + + error_if(not os.path.isfile(args.FROM), + f"Source file '{args.FROM}' not found") + if os.path.isfile(args.TO): + error_if(not args.overwrite, + f"Database file '{args.TO}' already exists. Specify " + f"'--overwrite' to overwrite it") + try: + os.unlink(args.TO) + except OSError as ex: + error(f"Error deleting '{args.TO}': {repr(ex)}") + else: + target_dir = os.path.dirname(args.TO) + if target_dir and (not os.path.isdir(target_dir)): + try: + os.makedirs(target_dir) + except OSError as ex: + error(f"Error creating directory '{target_dir}' for target " + f"database: {repr(ex)}'") + + gdal_dataset: Optional[gdal.Dataset] = None + gdal_band: Optional[gdal.Band] = None + scaled_file: Optional[str] = None + db_conn: Optional[sqlite3.Connection] = None + success = False + try: + fd, scaled_file = \ + tempfile.mkstemp( + prefix=os.path.basename(os.path.splitext(__file__)[0]), + suffix=".tif") + os.close(fd) + warp(src=args.FROM, dst=scaled_file, resampling="sum", + pixel_size_lat=args.resolution / 3600, + pixel_size_lon=args.resolution / 3600, + format_params=["BIGTIFF=YES", "COMPRESS=PACKBITS"], + data_type="Float64", center_lon_180=args.center_lon_180, + overwrite=True, quiet=False) + db_conn = sqlite3.connect(args.TO) + db_cur = db_conn.cursor() + db_cur.execute( + f"CREATE TABLE {TABLE_NAME}({CUMULATIVE_DENSITY_FIELD} real, " + f"{MIN_LAT_FIELD} real, {MAX_LAT_FIELD} real, " + f"{MIN_LON_FIELD} real, {MAX_LON_FIELD} real)") + db_cur.execute( + f"CREATE INDEX {CUMULATIVE_DENSITY_FIELD}_idx ON {TABLE_NAME}" + f"({CUMULATIVE_DENSITY_FIELD} ASC)") + gdal_dataset = gdal.Open(scaled_file, gdal.GA_ReadOnly) + line_len = gdal_dataset.RasterXSize + num_lines = gdal_dataset.RasterYSize + lon0, lon_res, _, lat0, _, lat_res = gdal_dataset.GetGeoTransform() + unpack_format = "d" * line_len + + gdal_band = gdal_dataset.GetRasterBand(1) + nodata = gdal_band.GetNoDataValue() + + total_population: float = 0 + bulk: List[Dict[str, float]] = [] + start_time = datetime.datetime.now() + print("Fetching density data from file") + for lat_idx in range(num_lines): + print(f"Line {lat_idx} of {num_lines} " + f"({lat_idx * 100 / num_lines:.1f}%)", + end="\r", flush=True) + raster_bytes = gdal_band.ReadRaster(xoff=0, yoff=lat_idx, + xsize=line_len, ysize=1) + raster_values = struct.unpack(unpack_format, raster_bytes) + for lon_idx in range(line_len): + v = raster_values[lon_idx] + if math.isnan(v) or (v in (0, nodata)): + continue + total_population += v + lat1 = lat0 + lat_idx * lat_res + lat2 = lat0 + (lat_idx + 1) * lat_res + lon1 = lon0 + lon_idx * lon_res + lon2 = lon0 + (lon_idx + 1) * lon_res + min_lon = min(lon1, lon2) + max_lon = max(lon1, lon2) + while min_lon < -180: + min_lon += 360 + max_lon += 360 + while max_lon > 180: + min_lon -= 360 + max_lon -= 360 + bulk.append({CUMULATIVE_DENSITY_FIELD: total_population, + MIN_LAT_FIELD: min(lat1, lat2), + MAX_LAT_FIELD: max(lat1, lat2), + MIN_LON_FIELD: min_lon, + MAX_LON_FIELD: max_lon}) + if len(bulk) < BULK_WRITE_THRESHOLD: + continue + db_cur.executemany( + f"INSERT INTO {TABLE_NAME} VALUES(" + f":{CUMULATIVE_DENSITY_FIELD}, " + f":{MIN_LAT_FIELD}, :{MAX_LAT_FIELD}," + f":{MIN_LON_FIELD}, :{MAX_LON_FIELD})", + bulk) + db_conn.commit() + bulk = [] + if bulk: + db_cur.executemany( + f"INSERT INTO {TABLE_NAME} VALUES(" + f":{CUMULATIVE_DENSITY_FIELD}, " + f":{MIN_LAT_FIELD}, :{MAX_LAT_FIELD}," + f":{MIN_LON_FIELD}, :{MAX_LON_FIELD})", + bulk) + db_conn.commit() + bulk = [] + print(f"\nPopulation total: {total_population}") + print("Normalizing density") + db_cur.execute(f"UPDATE {TABLE_NAME} SET {CUMULATIVE_DENSITY_FIELD} = " + f"{CUMULATIVE_DENSITY_FIELD} / {total_population}") + db_conn.commit() + success = True + except KeyboardInterrupt: + pass + finally: + gdal_band = None + gdal_dataset = None + if scaled_file is not None: + try: + os.unlink(scaled_file) + except OSError: + pass + if db_conn is not None: + db_conn.close() + if not success: + try: + os.unlink(args.TO) + except OSError: + pass + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tools/geo_converters/nlcd_wgs84.py b/tools/geo_converters/nlcd_wgs84.py new file mode 100755 index 0000000..4c2546f --- /dev/null +++ b/tools/geo_converters/nlcd_wgs84.py @@ -0,0 +1,493 @@ +#!/usr/bin/env python3 +# Converts land cover file to NLCD/WGS84 format + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=wildcard-import, invalid-name, too-few-public-methods +# pylint: disable=too-many-locals, unused-wildcard-import + +import argparse +from collections.abc import Iterable +import os +import struct +import sys +import tempfile +from typing import Dict, List, NamedTuple, Set, Tuple +import yaml + +from geoutils import * + +try: + from osgeo import gdal + HAS_GDAL = True +except ImportError: + HAS_GDAL = False + +# Parameter YAML file name +PARAM_FILE_NAME = os.path.splitext(__file__)[0] + ".yaml" + +# Length of color table in translated file +TARGET_COLOR_TABLE_LENGTH = 256 + +DEFAULT_RESAMPLING = "near" + + +_EPILOG = """This script expects that GDAL utilities are installed and in PATH. +Also it expects that as part of this installation proj/proj.db is somehow +installed as well (if it is, but gdalwarp still complains - set PROJ_LIB +environment variable to point to this proj directory). + +Usage examples: + - Convert CONUS NLCD file nlcd_2019_land_cover_l48_20210604.img that is in map + projection (Albers conic) to nlcd_2019_land_cover_l48_20210604_resampled.tif + that uses WGS84 (lat/lon) coordinates. Data resolution set to 1 second (3600 + pixels per degree) as it is comparable to 30 m pixel size in the source + data, TIFF data compression set to PACKBITS (shrinks file 9 times, no + performance penalty): + $ ./nlcd_wgs84.py --pixels_per_degree 3600 \\ + --format_param COMPRESS=PACKBITS \\ + nlcd_2019_land_cover_l48_20210604.img \\ + nlcd_2019_land_cover_l48_20210604_resampled.tif + - Same for Alaska NLCD: + $ ./nlcd_wgs84.py --pixels_per_degree 3600 \\ + --format_param COMPRESS=PACKBITS \\ + NLCD_2016_Land_Cover_AK_20200724.img \\ + NLCD_2016_Land_Cover_AK_20200724_resampled.tif + - Puerto Rico NLCD from NOAA source (NOAA uses different land usage codes): + $ /nlcd_wgs84.py --pixels_per_degree 3600 --encoding noaa \\ + --format_param COMPRESS=PACKBITS \\ + pr_2010_ccap_hr_land_cover20170214.img \\ + pr_2010_ccap_hr_land_cover20170214_resampled.tif +Note that --threads only affect recoding part of this script (that is +relatively fast). Coordinate system change (second phase) uses single CPU and +takes a long time (e.g. 12 hours for Canada) +""" + + +class GdalProgressor: + """ GDAL-style context printer + + Private attributes: + _total_lines -- Total number of raster lines + _last_fraction -- Recently printed 1/40-th fraction + """ + + def __init__(self, filename: str, total_lines: int) -> None: + """ Constructor + + Arguments: + filename -- Name of file being processed + total_lines -- Total number of raster lines + """ + self._total_lines = total_lines + self._last_fraction: int = -1 + print(f"Processing:{filename}: ", end="", flush=True) + + def progress(self, line: int) -> None: + """ Print progress + + Arguments: + line -- 0-based raster line number + """ + if line == (self._total_lines - 1): + print("100 - done.") + return + new_fraction = line * 40 // self._total_lines + if new_fraction == self._last_fraction: + return + self._last_fraction = new_fraction + print("." if new_fraction % 4 else (new_fraction // 4 * 10), end="", + flush=True) + + +class YamlParams: + """ Parameters stored in accompanying YAML file (mostly encoding-related) + + Public attributes: + default_code -- Default target code + default_color -- Default map color + target_codes -- By-code dictionary of TargetCodeInfo objects + encodings -- By name dictionary of by-code dictionaries of SrcCodeInfo + objects + """ + # Information about target (NLCD as of time of this writing) code + TargetCodeInfo = \ + NamedTuple("TargetCodeInfo", + [ + # Land usage description + ("description", str), + # Color to use on generated map + ("color", List[int])]) + # Information about source code + SrcCodeInfo = \ + NamedTuple("SrcCodeInfo", + [ + # Land usage description + ("description", str), + # Correspondent target code + ("target_code", int)]) + + def __init__(self): + """ Constructor. Reads all from YAML file """ + error_if(not os.path.isfile(PARAM_FILE_NAME), + f"Parameter file '{PARAM_FILE_NAME}' not found") + if hasattr(yaml, "CLoader"): + loader = yaml.CLoader + elif hasattr(yaml, "FullLoader"): + loader = yaml.FullLoader + else: + loader = None + with open(PARAM_FILE_NAME, encoding="utf-8") as f: + yaml_content = f.read() + # pylint: disable=no-value-for-parameter + yaml_dict = yaml.load(yaml_content, loader) if loader \ + else yaml.load(yaml_content) + try: + self.default_code: int = yaml_dict["target"]["default_code"] + self.default_color: List[int] = \ + yaml_dict["target"]["default_color"] + self.target_codes: Dict[int, "YamlParams.TargetCodeInfo"] = {} + for code, tgt_code_info in yaml_dict["target"]["codes"].items(): + error_if( + code in self.target_codes, + f"Target encoding code {code} specified more than once") + self.target_codes[code] = \ + self.TargetCodeInfo( + description=tgt_code_info["description"], + color=tgt_code_info["color"]) + self.encodings: Dict[str, Dict[int, "YamlParams.SrcCodeInfo"]] = {} + for name, enc_info in yaml_dict["encodings"].items(): + self.encodings[name] = {} + for code, src_code_info in enc_info["codes"].items(): + error_if( + code in self.encodings[name], + f"Encoding '{name}' has information for code {code} " + f"specified more than once") + error_if( + src_code_info["target_code"] not in self.target_codes, + f"Encoding '{name}' code {code} refers target code " + f"{src_code_info['target_code']} not defined in " + f"target encoding") + self.encodings[name][code] = \ + self.SrcCodeInfo( + description=src_code_info["description"], + target_code=src_code_info["target_code"]) + except LookupError as ex: + error(f"Invalid '{os.path.basename(PARAM_FILE_NAME)}' structure: " + f"{ex}") + + def get_color(self, code: int) -> Tuple[int, ...]: + """ Returns color for given code """ + return tuple(self.target_codes[code].color if code in self.target_codes + else self.default_color) + + def get_translation_dict(self, name: str) -> Dict[int, int]: + """ Returns translation dictionary for given encoding """ + error_if(name not in self.encodings, + f"Encoding '{name}' not found") + return {k: i.target_code for k, i in self.encodings[name].items()} + + +class TranslationContexts: + """ Collection of translation contexts. + + Private attributes: + self._contexts -- Contexts, ordered by row length and data type + """ + # Translation context: preallocated buffers and constants used in + # translation + TranslationContext = \ + NamedTuple("TranslationContext", + [ + # Format of input data for estruct.unpack() + ("input_format", str), + # Format of output data for struct.pack() + ("output_format", str), + # Buffer for output data + ("buffer", List[int])]) + + # Translation of GDAL raster data types to struct data types + _DATA_SIZES: Dict[int, str] = \ + {gdal.GDT_Byte: "B", + gdal.GDT_Int8: "b", + gdal.GDT_Int16: "h", + gdal.GDT_Int32: "i", + gdal.GDT_UInt16: "H", + gdal.GDT_UInt32: "I"} if HAS_GDAL else {} + + def __init__(self) -> None: + """ Constructor """ + self._contexts: \ + Dict[Tuple[int, int], + "TranslationContexts.TranslationContext"] = {} + + def get_context(self, input_size: int, data_type_code: int) \ + -> "TranslationContexts.TranslationContext": + """ Context for given data length and type + + Arguments: + input_size -- Input data size in bytes + data_type_code -- GDAL input data type code + Returns Context for given size and data type + """ + key = (input_size, data_type_code) + ret = self._contexts.get(key) + if ret is None: + struct_data_type = self._DATA_SIZES.get(data_type_code) + error_if(struct_data_type is None, + f"Unsupported data type code of {data_type_code} in " + f"source file") + assert struct_data_type is not None + length = input_size // struct.calcsize(struct_data_type) + ret = \ + self.TranslationContext( + input_format=length * struct_data_type, + output_format=length * "B", buffer=length * [0]) + self._contexts[key] = ret + return ret + + +class Translation: + """ Pixel row translator to NLCD encoding. + This class intended to be used as context manager (with 'with`) + + Private attributes: + _translation_dict -- Dictionary from source to target (NLCD as of time + of this writing) encoding + _default_code -- Code to use when source code missing in + translation dictionary + _data_type_code -- GDAL pixel data type code + _translation_contexts -- Translation context factory + """ + + def __init__(self, translation_dict: Dict[int, int], default_code: int, + data_type_code: int) -> None: + """ Constructor + + Arguments: + translation _dict -- Translation dictionary to target encoding + default_code -- Default code in target encoding + data_type_code -- GDAL pixel data type code + """ + self._translation_dict = translation_dict + self._default_code = default_code + self._data_type_code = data_type_code + self._translation_contexts = TranslationContexts() + + def translate(self, input_bytes: bytes) -> Tuple[bytes, Set[int]]: + """ Translate pixel row + + Arguments: + input_bytes -- Input raster line byte string + Returns tuple with translated raster line and set of codes that were + not in translation dictionary + """ + tc = \ + self._translation_contexts.get_context( + input_size=len(input_bytes), + data_type_code=self._data_type_code) + input_values = struct.unpack(tc.input_format, input_bytes) + unknown_codes: Set[int] = set() + end_idx = len(input_values) + idx = 0 + while idx < end_idx: + try: + while idx < end_idx: + tc.buffer[idx] = self._translation_dict[input_values[idx]] + idx += 1 + except KeyError: + unknown_codes.add(input_values[idx]) + self._translation_dict[input_values[idx]] = self._default_code + return (struct.pack(tc.output_format, *tc.buffer), unknown_codes) + + +def translate(src: str, dst: str, yaml_params: YamlParams, + translation_name: str, threads: int) -> None: + """ Translate pixel codes to NLCD encoding + + Arguments: + src -- Source file name + dst -- Result file name (GeoTiff, no special parameters) + yaml_params -- Parameters from accompanying YAML file + translation_name -- Name of translation + threads -- Number of threads to use + """ + ds_src = gdal.Open(src, gdal.GA_ReadOnly) + band_src = ds_src.GetRasterBand(1) + driver_gtiff = gdal.GetDriverByName('GTiff') + ds_dst = \ + driver_gtiff.Create(dst, xsize=band_src.XSize, ysize=band_src.YSize, + eType=gdal.GDT_Byte) + error_if(ds_dst is None, f"Can't create '{dst}'") + ds_dst.SetProjection(ds_src.GetProjection()) + ds_dst.SetGeoTransform(ds_src.GetGeoTransform()) + band_dst = ds_dst.GetRasterBand(1) + color_table_dst = gdal.ColorTable() + for idx in range(TARGET_COLOR_TABLE_LENGTH): + color_table_dst.SetColorEntry(idx, yaml_params.get_color(idx)) + band_dst.SetRasterColorTable(color_table_dst) + band_dst.SetRasterColorInterpretation(gdal.GCI_PaletteIndex) + band_dst.SetNoDataValue(yaml_params.default_code) + translation = \ + Translation( + translation_dict=yaml_params.get_translation_dict( + translation_name), + default_code=yaml_params.default_code, + data_type_code=band_src.DataType) + progressor = GdalProgressor(filename=src, total_lines=band_src.YSize) + + def scan_line_producer() -> Iterable[bytes]: + """ Produces scan lines from the source file """ + for y in range(band_src.YSize): + yield band_src.ReadRaster(xoff=0, yoff=y, + xsize=band_src.XSize, ysize=1) + + unknown_codes: Set[int] = set() + with WindowedProcessPool(producer=scan_line_producer(), + processor=translation.translate, + max_in_flight=100, threads=threads) as wpp: + y = 0 + for scanline_dst, uc in wpp: + band_dst.WriteRaster(xoff=0, yoff=y, xsize=band_src.XSize, ysize=1, + buf_string=scanline_dst) + progressor.progress(y) + y += 1 + unknown_codes |= uc + ds_dst.FlushCache() + ds_dst = None + if unknown_codes: + warning( + f"Source file contains following unknown land usage codes: " + f"{', '.join(str(c) for c in sorted(unknown_codes))}. They were " + f"translated to {yaml_params.default_code}. Consider adding these " + f"codes to '{os.path.basename(PARAM_FILE_NAME)}'") + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + yaml_params = YamlParams() + argument_parser = argparse.ArgumentParser( + description="Converts land cover file to NLCD/WGS84 format", + formatter_class=argparse.RawDescriptionHelpFormatter, epilog=_EPILOG) + argument_parser.add_argument( + "--pixel_size", metavar="DEGREES", type=float, + help="Resulting file resolution in pixel size (expressed in degrees). " + "Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--pixels_per_degree", metavar="NUMBER", type=float, + help="Resulting file resolution in pixels per degree. Default is to " + "use one from 'gdalinfo'") + argument_parser.add_argument( + "--top", metavar="MAX_LATITUDE", type=float, + help="Maximum latitude. Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--bottom", metavar="MIN_LATITUDE", type=float, + help="Minimum latitude. Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--left", metavar="MIN_LONGITUDE", type=float, + help="Minimum longitude. Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--right", metavar="MAX_LONGITUDE", type=float, + help="Maximum longitude. Default is to use one from 'gdalinfo'") + encoding_names = sorted(yaml_params.encodings.keys()) + argument_parser.add_argument( + "--encoding", choices=encoding_names, + help=f"Translate land cover codes from given encoding to NLCD " + f"encoding. Supported encodings are: {', '.join(encoding_names)}") + argument_parser.add_argument( + "--format", metavar="GDAL_DRIVER_NAME", + help="File format expressed as GDAL driver name (see " + "https://gdal.org/drivers/raster/index.html ). By default derived " + "from target file extension") + argument_parser.add_argument( + "--format_param", metavar="NAME=VALUE", action="append", + default=[], + help="Format option (e.g. COMPRESS=PACKBITS or BIGTIFF=IF_NEEDED). " + "May be specified several times") + resampling_methods = warp_resamplings() + argument_parser.add_argument( + "--resampling", metavar="METHOD", default=DEFAULT_RESAMPLING, + choices=resampling_methods, + help=f"Resampling method. Possible values: {resampling_methods}. " + f"Default is `{DEFAULT_RESAMPLING}'") + argument_parser.add_argument( + "--threads", metavar="COUNT_OR_PERCENT%", + help="Number of threads to use. If positive - number of threads, if " + "negative - number of CPU cores NOT to use, if followed by `%%` - " + "percent of CPU cores. Default is total number of CPU cores") + argument_parser.add_argument( + "--nice", action="store_true", + help="Lower priority of this process and its subprocesses") + argument_parser.add_argument( + "--overwrite", action="store_true", + help="Overwrite target file if it exists") + argument_parser.add_argument("SRC", help="Source file name") + argument_parser.add_argument("DST", help="Destination file name") + + if not argv: + argument_parser.print_help() + sys.exit(1) + args = argument_parser.parse_args(argv) + + setup_logging() + + if args.nice: + nice() + + error_if(not os.path.isfile(args.SRC), + f"Can't find source file '{args.SRC}'") + error_if(os.path.isfile(args.DST) and (not args.overwrite), + f"'{args.DST}' already exists. Specify --overwrite to overwrite") + error_if( + (args.encoding is not None) and (not HAS_GDAL), + "Python GDAL support was not installed. Try to 'pip install gdal' or " + "run containerized version of this script") + os.makedirs( + os.path.dirname(os.path.abspath( + os.path.expanduser(os.path.expandvars(args.DST)))), + exist_ok=True) + boundaries = \ + Boundaries( + args.SRC, pixel_size=args.pixel_size, + pixels_per_degree=args.pixels_per_degree, + top=args.top, bottom=args.bottom, left=args.left, right=args.right, + round_boundaries_to_degree=True, round_pixels_to_degree=True) + try: + temp: Optional[str] = None + src = args.SRC + with Durator("Total duration:"): + if args.encoding is not None: + with Durator("This stage took"): + print("TRANSLATING PIXELS TO NLCD ENCODING") + h, temp = \ + tempfile.mkstemp(suffix=os.path.splitext(args.DST)[1], + dir=os.path.dirname(args.DST) or ".") + os.close(h) + translate(src=src, dst=temp, yaml_params=yaml_params, + translation_name=args.encoding, + threads=threads_arg(args.threads)) + src = temp + with Durator("This stage took"): + print("TRANSLATING TO WGS84") + warp(src=src, dst=args.DST, resampling=args.resampling, + top=boundaries.top, bottom=boundaries.bottom, + left=boundaries.left, right=boundaries.right, + pixel_size_lat=boundaries.pixel_size_lat, + pixel_size_lon=boundaries.pixel_size_lon, + out_format=args.format, format_params=args.format_param, + overwrite=args.overwrite) + finally: + if temp: + os.unlink(temp) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tools/geo_converters/nlcd_wgs84.yaml b/tools/geo_converters/nlcd_wgs84.yaml new file mode 100644 index 0000000..ea9b81c --- /dev/null +++ b/tools/geo_converters/nlcd_wgs84.yaml @@ -0,0 +1,375 @@ +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# This is a parameter file for nlcd_wgs84.py that specifies land usage code +# translation parameters + +--- +# Target encoding. For now - NLCD +target: + codes: + 0: + description: "Unclassified" + color: [0, 0, 0, 0] + 11: + description: "Open Water" + color: [70, 107, 159, 255] + 12: + description: "Perennial Ice/Snow" + color: [209, 222, 248, 255] + 21: + description: "Developed, Open Space (e.g large-lot single-family housing units, parks, golf courses, and planted vegetation). Impervious surfaces < 20%" + color: [222, 197, 197, 255] + 22: + description: "Developed, Low Intensity (e.g single-family housing units). Impervious surfaces 20%-49%" + color: [217, 146, 130, 255] + 23: + description: "Developed, Medium Intensity (e.g single-family housing units). Impervious surfaces 50%-79%" + color: [235, 0, 0, 255] + 24: + description: "Developed, High Intensity - areas where people reside or work in high numbers (e.g. apartment complexes, row houses and commercial/industrial). Impervious surfaces 80%-100%" + color: [171, 0, 0, 255] + 31: + description: "Barren Land (Rock/Sand/Clay). Vegetation < 15% of total cover" + color: [179, 172, 159, 255] + 41: + description: "Deciduous Forest (dominated by trees generally > 5m tall, > 20% of total vegetation cover). > 75% shed foliage simultaneously" + color: [104, 171, 95, 255] + 42: + description: "Evergreen Forest (dominated by trees generally > 5m tall, > 20% of total vegetation cover). > 75% maintain leaves all year. Canopy always with green foliage" + color: [28, 95, 44, 255] + 43: + description: "Mixed Forest (dominated by trees generally > 5m tall, > 20% of total vegetation cover). Neither deciduous nor evergreen > 75% of total tree cover" + color: [181, 197, 143, 255] + 51: + description: "Dwarf Scrub (Alaska only, dominated by shrubs < 20 centimeters tall with shrub canopy typically greater than 20% of total vegetation. E.g. grasses, sedges, herbs, and non-vascular vegetation" + color: [172, 146, 57, 255] + 52: + description: "Shrub/Scrub (dominated by shrubs < 5m tall, canopy > 20% of total vegetation. E.g. shrubs, young trees, trees stunted from environmental conditions)" + color: [204, 184, 121, 255] + 71: + description: "Grassland/Herbaceous (grammanoid or herbaceous vegetation are > 80% of total vegetation). No tilling, but may be grazing" + color: [223, 223, 194, 255] + 72: + description: "Sedge/Herbaceous (Alaska only, sedges and forbs are > 80% of total vegetation). Grasses or grass like plants, includes sedge and sedge tussock tundra" + color: [209, 209, 130, 255] + 73: + description: "Lichens (Alaska only, fruticose or foliose lichens > 80% of total vegetation)" + color: [164, 204, 81, 255] + 74: + description: "Moss (Alaska only, mosses > 80% of total vegetation)" + color: [130, 184, 158, 255] + 81: + description: "Pasture/Hay (grasses, legumes, planted for livestock grazing or the production of seed or hay crops, perennial cycle). Pasture/hay vegetation > 20% of total vegetation" + color: [220, 217, 57, 255] + 82: + description: "Cultivated Crops. Production of annual crops, such as corn, soybeans, vegetables, tobacco, and cotton, and also perennial woody crops such as orchards and vineyards, actively tilled lands. Crop vegetation > 20% of total vegetation" + color: [171, 108, 40, 255] + 90: + description: "Woody Wetlands. Forest or shrub > 20%of vegetative cover. Periodically saturated with or covered with water" + color: [184, 217, 235, 255] + 95: + description: "Emergent Herbaceous Wetlands. Perennial herbaceous vegetation > 80% of vegetative cover. Periodically saturated with or covered with water" + color: [108, 159, 184, 255] + default_code: 0 + default_color: [0, 0, 0, 255] + +# Source encoding +encodings: + noaa: + codes: + 0: + description: "Background" + target_code: 0 + 1: + description: "Unclassified. This class contains no data due to cloud conditions or data voids" + target_code: 0 + 2: + description: "Impervious Surfaces. Buildings, parking lots and roads of asphalt/concrete or other constructed surfaces which do not allow infiltration from precipitation" + target_code: 24 + 3: + description: "Medium Intensity Developed" + target_code: 23 + 4: + description: "Low Intensity Developed" + target_code: 22 + 5: + description: "Open Spaces Developed. Some constructed materials, but mostly lawn grasses. Impervious < 20% of total cover. E.g. large-lot single-family housing units, parks, golf courses, and planted vegetation" + target_code: 21 + 6: + description: "Cultivated Land. Cropland) and woody cultivated lands" + target_code: 82 + 7: + description: "Pasture/Hay. Grasses, legumes or mixtures planted for livestock grazing, production of sees or hay crops" + target_code: 81 + 8: + description: "Grassland. Dominated by natural grasses and non-grasses (forbs) that are not fertilized, cut, tilled, or planted regularly" + target_code: 71 + 9: + description: "Deciduous Forest. Woody vegetation un-branched 0.6 to 1m, height > 5m" + target_code: 41 + 10: + description: "Evergreen Forest. Areas > 67% of the trees remain green. Coniferous and broad-leaved evergreens (> 5m)" + target_code: 42 + 11: + description: "Mixed Forest. Forested areas with both evergreen and deciduous trees (> 5m) and neither predominate" + target_code: 43 + 12: + description: "Scrub/Shrub. Dominated by woody vegetation < 5m in height. True shrubs, young trees, and trees or shrubs that are small or stunted because of environmental conditions" + target_code: 52 + 13: + description: "Palustrine Forested Wetland. Wetlands non-tidal or tidal with ocean-derived salinity < 0.05% dominated by woody vegetation > 5m in height" + target_code: 90 + 14: + description: "Palustrine Scrub/Shrub Wetland. Wetlands non-tidal or tidal with ocean-derived salinity < 0.05% dominated by woody vegetation < 5m in height" + target_code: 90 + 15: + description: "Palustrine Emergent Wetland. Wetlands non-tidal or tidal with ocean-derived salinity < 0.05% dominated by trees, shrubs, persistent emergents, emergent mosses, or lichens" + target_code: 95 + 16: + description: "Estuarine Forest Wetland. Tidal with ocean-derived salinity > 0.05% dominated by woody vegetation > 5m in height" + target_code: 90 + 17: + description: "Estuarine Scrub/Shrub Wetland. Tidal with ocean-derived salinity > 0.05% dominated by woody vegetation < 5m in height" + target_code: 90 + 18: + description: "Estuarine Emergent. Erect, rooted, herbaceous hydrophytes (excluding mosses and lichens) that are present for most of the growing season in most years. Perennial plants usually dominate these wetlands. Except sub-tidal and irregularly exposed water regimes" + target_code: 95 + 19: + description: "Unconsolidated Shore. Substrates lacking vegetation except for pioneering plants. E.g. beaches, bars, and flats" + target_code: 31 + 20: + description: "Bare Land. Bare soil, rock, sand, silt, gravel. Little or no vegetation" + target_code: 31 + 21: + description: "Water. Includes all areas with < 30% cover of vegetation" + target_code: 11 + 22: + description: "Palustrine Aquatic Bed. Wetlands and deepwater habitats dominated by plants that grow on or below the surface of the water" + target_code: 11 + 23: + description: "Estuarine Aquatic Bed. Widespread and diverse Algal Beds in the Marine and Estuarine Systems on sediment substrates. Both the sub-tidal and inter-tidal and may grow to depths of 30m. Includes kelp forests" + target_code: 11 + 24: + description: "Tundra. Treeless cover beyond the latitudinal limit of the boreal forest and above boreal forest in high mountains" + target_code: 52 + 25: + description: "Snow/Ice. Snow and ice that persist for greater portions of the year" + target_code: 12 + 26: + description: "Dwarf Scrub - Alaska specific" + target_code: 51 + 27: + description: "Sedge/Herbaceous - Alaska specific" + target_code: 72 + 28: + description: "Moss - Alaska specific" + target_code: 74 + + canada: + codes: + 0: + description: "Background" + target_code: 0 + 1: + description: "Temperate or sub-polar needleleaf forest" + target_code: 42 + 2: + description: "Sub-polar taiga needleleaf forest" + target_code: 42 + 3: + description: "Tropical or sub-tropical broadleaf evergreen forest" + target_code: 42 + 4: + description: "Tropical or sub-tropical broadleaf deciduous forest" + target_code: 41 + 5: + description: "Temperate or sub-polar broadleaf deciduous forest" + target_code: 41 + 6: + description: "Mixed forest" + target_code: 43 + 7: + description: "Tropical or sub-tropical shrubland" + target_code: 52 + 8: + description: "Temperate or sub-polar shrubland" + target_code: 52 + 9: + description: "Tropical or sub-tropical grassland" + target_code: 71 + 10: + description: "Temperate or sub-polar grassland" + target_code: 71 + 11: + description: "Sub-polar or polar shrubland-lichen-moss" + target_code: 52 + 12: + description: "Sub-polar or polar grassland-lichen-moss" + target_code: 71 + 13: + description: "Sub-polar or polar barren-lichen-moss" + target_code: 31 + 14: + description: "Wetland" + target_code: 95 + 15: + description: "Cropland" + target_code: 82 + 16: + description: "Barren lands" + target_code: 31 + 17: + description: "Urban" + # Should be 24. Mapping 24 to 22. + target_code: 22 + 18: + description: "Water" + target_code: 11 + 19: + description: "Snow and Ice" + target_code: 12 + corine: + codes: + 1: + description: "Continuous urban fabric" + target_code: 24 + 2: + description: "Discontinuous urban fabric" + target_code: 23 + 3: + description: "Industrial or commercial units" + target_code: 22 + 4: + description: "Road and rail networks and associated land" + target_code: 22 + 5: + description: "Port areas" + target_code: 22 + 6: + description: "Airports" + target_code: 22 + 7: + description: "Mineral extraction sites" + target_code: 21 + 8: + description: "Dump sites" + target_code: 21 + 9: + description: "Construction sites" + target_code: 21 + 10: + description: "Green urban areas" + target_code: 21 + 11: + description: "Sport and leisure facilities" + target_code: 22 + 12: + description: "Non-irrigated arable land" + target_code: 82 + 13: + description: "Permanently irrigated land" + target_code: 82 + 14: + description: "Rice fields" + target_code: 82 + 15: + description: "Vineyards" + target_code: 82 + 16: + description: "Fruit trees and berry plantations" + target_code: 82 + 17: + description: "Olive groves" + target_code: 82 + 18: + description: "Pastures" + target_code: 81 + 19: + description: "Annual crops associated with permanent crops" + target_code: 82 + 20: + description: "Complex cultivation patterns" + target_code: 82 + 21: + description: "Land principally occupied by agriculture with significant areas of natural vegetation" + target_code: 82 + 22: + description: "Agro-forestry areas" + target_code: 41 + 23: + description: "Broad-leaved forest" + target_code: 41 + 24: + description: "Coniferous forest" + target_code: 42 + 25: + description: "Mixed forest" + target_code: 43 + 26: + description: "Natural grasslands" + target_code: 71 + 27: + description: "Moors and heathland" + target_code: 52 + 28: + description: "Sclerophyllous vegetation" + target_code: 52 + 29: + description: "Transitional woodland-shrub" + target_code: 52 + 30: + description: "Beaches - dunes - sands" + target_code: 31 + 31: + description: "Bare rocks" + target_code: 31 + 32: + description: "Sparsely vegetated areas" + target_code: 31 + 33: + description: "Burnt areas" + target_code: 31 + 34: + description: "Glaciers and perpetual snow" + target_code: 12 + 35: + description: "Inland marshes" + target_code: 95 + 36: + description: "Peat bogs" + target_code: 95 + 37: + description: "Salt marshes" + target_code: 95 + 38: + description: "Salines" + target_code: 95 + 39: + description: "Intertidal flats" + target_code: 31 + 40: + description: "Water courses" + target_code: 11 + 41: + description: "Water bodies" + target_code: 11 + 42: + description: "Coastal lagoons" + target_code: 11 + 43: + description: "Estuaries" + target_code: 11 + 44: + description: "Sea and ocean" + target_code: 11 + 48: + description: "No data" + target_code: 0 + -128: + description: "No data" + target_code: 0 + diff --git a/tools/geo_converters/proc_gdal/.qmake.stash b/tools/geo_converters/proc_gdal/.qmake.stash new file mode 100644 index 0000000..62f5a9b --- /dev/null +++ b/tools/geo_converters/proc_gdal/.qmake.stash @@ -0,0 +1,22 @@ +QMAKE_CXX.QT_COMPILER_STDCXX = 201402L +QMAKE_CXX.QMAKE_GCC_MAJOR_VERSION = 9 +QMAKE_CXX.QMAKE_GCC_MINOR_VERSION = 3 +QMAKE_CXX.QMAKE_GCC_PATCH_VERSION = 1 +QMAKE_CXX.COMPILER_MACROS = \ + QT_COMPILER_STDCXX \ + QMAKE_GCC_MAJOR_VERSION \ + QMAKE_GCC_MINOR_VERSION \ + QMAKE_GCC_PATCH_VERSION +QMAKE_CXX.INCDIRS = \ + /usr/include/c++/9 \ + /usr/include/c++/9/x86_64-redhat-linux \ + /usr/include/c++/9/backward \ + /usr/lib/gcc/x86_64-redhat-linux/9/include \ + /usr/local/include \ + /usr/include +QMAKE_CXX.LIBDIRS = \ + /usr/lib/gcc/x86_64-redhat-linux/9 \ + /usr/lib64 \ + /lib64 \ + /usr/lib \ + /lib diff --git a/tools/geo_converters/proc_gdal/GdalDataModel.cpp b/tools/geo_converters/proc_gdal/GdalDataModel.cpp new file mode 100644 index 0000000..4374bce --- /dev/null +++ b/tools/geo_converters/proc_gdal/GdalDataModel.cpp @@ -0,0 +1,343 @@ +#include + +#include "GdalDataModel.h" + +/******************************************************************************************/ +/* CONSTRUCTOR GdalDataModel::GdalDataModel */ +/******************************************************************************************/ +GdalDataModel::GdalDataModel(const std::string &dataSourcePath, const std::string &heightFieldName) +{ + // Registers necessary drivers + OGRRegisterAll(); + + // _ptrDataSource = OGRSFDriverRegistrar::Open(dataSourcePath.c_str(), FALSE); + + _ptrDataSource = (GDALDataset *) GDALOpenEx(dataSourcePath.c_str(), GDAL_OF_VECTOR, NULL, NULL, NULL ); + + // Check if data source was successfully opened + if( _ptrDataSource == nullptr ) + throw std::runtime_error("Failed to open OGR data source at " + dataSourcePath); + + // Check if data source contains exactly one 1 layer + if (_ptrDataSource->GetLayerCount() != 1) { + throw std::runtime_error("GdalDataModel::GdalDataModel(): There may be undefined behavior if data source contains more than 1 layer"); + } + + _ptrLayer = _ptrDataSource->GetLayer(0); // Data source has a 1:1 relationship with layer + + // Extract spatial reference datum from current layer + testSrcSpatialRef = new OGRSpatialReference(); + testSrcSpatialRef->SetWellKnownGeogCS("WGS84"); + + // Create new spatial reference to transform current layer's coordinates into our preferred coordinate system + // GDAL maintains ownership of returned pointer, so we should not manage its memory + testDestSpatialRef = _ptrLayer->GetSpatialRef(); + + // Prepare for coordinate transform + testCoordTransform = OGRCreateCoordinateTransformation(testSrcSpatialRef, testDestSpatialRef); + invCoordTransform = OGRCreateCoordinateTransformation(testDestSpatialRef, testSrcSpatialRef); + + if (!heightFieldName.empty()) { + std::unique_ptr ptrOFeature; + ptrOFeature = std::unique_ptr(_ptrLayer->GetNextFeature()); + heightFieldIdx = ptrOFeature->GetFieldIndex(heightFieldName.c_str()); + + if (heightFieldIdx == -1) { + throw std::runtime_error(std::string("GdalDataModel::GdalDataModel(): ERROR: data contains no height field \"") + heightFieldName + "\""); + } + } else { + heightFieldIdx = -1; + } +}; +/******************************************************************************************/ + +/******************************************************************************************/ +/* DESTRUCTOR GdalDataModel::GdalDataModel */ +/******************************************************************************************/ +GdalDataModel::~GdalDataModel() +{ + // Memory de-allocation + delete testSrcSpatialRef; + OGRCoordinateTransformation::DestroyCT(testCoordTransform); + OGRCoordinateTransformation::DestroyCT(invCoordTransform); +}; +/******************************************************************************************/ + +/***********************************/ +/* getMaxBuildingHeightAtPoint */ +/***********************************/ +// Looks at 2.5D data (buildings with multiple heights/polygons) and returns the highest possible height for a given test point (ensures accuracy) +double GdalDataModel::getMaxBuildingHeightAtPoint(double latDeg, double lonDeg) const +{ + double maxHeight_m = std::numeric_limits::quiet_NaN(); // Instantiate as NaN in case no building data exists at input lat/lon + + const auto buildingMap = getBuildingsAtPoint(latDeg, lonDeg); + + if (!buildingMap.empty()) { + const auto maxBuildingPair_m = std::max_element + ( + buildingMap.begin(), buildingMap.end(), + [](const std::pair& p1, const std::pair& p2) + { + return p1.second < p2.second; + } + ); + + maxHeight_m = maxBuildingPair_m->second; + } + + return maxHeight_m; +} + +/***********************************/ +/* getBuildingsAtPoint */ +/***********************************/ +// Return a map of OGRGeometry FIDs and the heights associated with each of those OGRGeometries +std::map GdalDataModel::getBuildingsAtPoint(double lat, double lon) const +{ // If in a building return a polygon, if not then return NULL + // Ensure loop begins with first feature + _ptrLayer->ResetReading(); + + // Extract spatial reference datum from current layer + OGRSpatialReference *tmpSrcSpatialRef = new OGRSpatialReference(); + tmpSrcSpatialRef->SetWellKnownGeogCS("WGS84"); + // Create new spatial reference to transform current layer's coordinates into our preferred coordinate system + // GDAL maintains ownership of returned pointer, so we should not manage its memory + OGRSpatialReference* tmpDestSpatialRef = _ptrLayer->GetSpatialRef(); + + // Prepare for coordinate transform + OGRCoordinateTransformation *tmpCoordTransform = OGRCreateCoordinateTransformation(tmpSrcSpatialRef, tmpDestSpatialRef); + + int transformSuccess; + // Perform transform into WGS-84 lat/lon coordinate system + int allTransformSuccess = tmpCoordTransform->TransformEx(1, &lon, &lat, nullptr, &transformSuccess); + + // Store input point projected into data source's spatial reference + GdalHelpers::GeomUniquePtr testPoint(GdalHelpers::createGeometry()); // Create OGRPoint on heap + testPoint->setX(lon); testPoint->setY(lat); // lat and lon have been transformed into the layer's spatial reference + + // Filter for features/geometries which geographically intersect with the test point + if (testPoint.get()->IsValid()) { + _ptrLayer->SetSpatialFilter(testPoint.get()); // FIXME: Does this shrink layer to only include features which intersected with a previous test point? + } else { + throw std::runtime_error("GdalDataModel::getBuildingsAtPoint(): OGRPoint testPoint returned invalid."); + } + + std::unique_ptr ptrOFeature; + // Iterate over features defined in current layer + std::map returnMap; + while ((ptrOFeature = std::unique_ptr(_ptrLayer->GetNextFeature())) != NULL) { + // GDAL maintains ownership of returned pointer, so we should not manage its memory + OGRGeometry *ptrOGeometry = ptrOFeature->GetGeometryRef(); + + // Check whether the current geometry is a polygon + if (ptrOGeometry != NULL && ptrOGeometry->getGeometryType() == OGRwkbGeometryType::wkbPolygon) { + if (heightFieldIdx != -1) { + returnMap.insert(std::make_pair(static_cast(ptrOFeature->GetFID()),ptrOFeature->GetFieldAsDouble(heightFieldIdx))); + } else { + returnMap.insert(std::make_pair(static_cast(ptrOFeature->GetFID()),0.0)); + } + } else { + std::cout << "GdalDataModel::getBuildingsAtPoint(): Can't find polygon geometries in current feature"; + } + } + + // Memory de-allocation + delete tmpSrcSpatialRef; + OGRCoordinateTransformation::DestroyCT(tmpCoordTransform); + + // Un-do effects of spatial filter + // _ptrLayer = _ptrDataSource->GetLayer(0); + + return returnMap; +} + +void GdalDataModel::printDebugInfo() const +{ + double minLon, maxLon; + double minLat, maxLat; + bool minMaxFlag = true; + int numPolygon = 0; + std::unique_ptr ptrOFeature; + // Iterate over features defined in current layer + std::map returnMap; + _ptrLayer->ResetReading(); + + OGREnvelope oExt; + if (_ptrLayer->GetExtent(&oExt, TRUE) == OGRERR_NONE) { + printf("Extent: (%f, %f) - (%f, %f)\n", + oExt.MinX, oExt.MinY, oExt.MaxX, oExt.MaxY); + } + + _ptrLayer->ResetReading(); + while ((ptrOFeature = std::unique_ptr(_ptrLayer->GetNextFeature())) != NULL) { + // GDAL maintains ownership of returned pointer, so we should not manage its memory + OGRGeometry *ptrOGeometry = ptrOFeature->GetGeometryRef(); + + std::cout << std::setprecision(25); + // Check whether the current geometry is a polygon + if (ptrOGeometry != NULL && ptrOGeometry->getGeometryType() == OGRwkbGeometryType::wkbPolygon) { + double height = (heightFieldIdx != -1 ? ptrOFeature->GetFieldAsDouble(heightFieldIdx) : 0.0); + returnMap.insert(std::make_pair(static_cast(ptrOFeature->GetFID()),height)); + + OGRPolygon *poly = (OGRPolygon *) ptrOGeometry; +#if 1 + std::cout << "POLYGON: " << numPolygon << std::endl; + std::cout << "FEATURE ID: " << ptrOFeature->GetFID() << std::endl; + char *wkt_tmp = (char *) NULL; + poly->exportToWkt(&wkt_tmp); + std::cout << wkt_tmp << std::endl; + std::cout << "POLYGON_HEIGHT = " << height << std::endl << std::endl; + + std::cout << "NUM_FIELD: " << ptrOFeature->GetFieldCount() << std::endl; + for(int fieldIdx=0; fieldIdxGetFieldCount(); ++fieldIdx) { + std::cout << " FIELD_" << fieldIdx << ": " << ptrOFeature->GetFieldDefnRef(fieldIdx)->GetNameRef() << " = " << ptrOFeature->GetFieldAsDouble(fieldIdx) << std::endl; + } +#endif + + OGRLinearRing *pRing = poly->getExteriorRing(); + // std::cout << "NUM_POINTS = " << pRing->getNumPoints() << std::endl << std::endl; + for(int ptIdx=0; ptIdxgetNumPoints(); ptIdx++) { + double lon = pRing->getX(ptIdx); + double lat = pRing->getY(ptIdx); + + int transformSuccess; + // Perform transform into WGS-84 lat/lon coordinate system + int allTransformSuccess = invCoordTransform->TransformEx(1, &lon, &lat, nullptr, &transformSuccess); + +#if 1 + std::cout << " POINT " << ptIdx << " : " << pRing->getX(ptIdx) << " " << pRing->getY(ptIdx) << " " << lon << " " << lat << std::endl; +#endif + + if (minMaxFlag) { + minLon = lon; + maxLon = lon; + minLat = lat; + maxLat = lat; + minMaxFlag = false; + } + if (lon < minLon) { + minLon = lon; + } else if (lon > maxLon) { + maxLon = lon; + } + if (lat < minLat) { + minLat = lat; + } else if (lat > maxLat) { + maxLat = lat; + } + } + + OGREnvelope env; + poly->getEnvelope(&env); + std::cout << " MIN_X " << env.MinX << std::endl; + std::cout << " MAX_X " << env.MaxX << std::endl; + std::cout << " MIN_Y " << env.MinY << std::endl; + std::cout << " MAX_Y " << env.MaxY << std::endl; +#if 1 + free(wkt_tmp); + std::cout << std::endl; +#endif + } else { + std::cout << "GdalDataModel::getBuildingsAtPoint(): Can't find polygon geometries in current feature"; + } + numPolygon++; + } + + std::cout << "NUM_POLYGON = " << numPolygon << std::endl; + std::cout << "MIN_LON = " << minLon << std::endl; + std::cout << "MAX_LON = " << maxLon << std::endl; + std::cout << "MIN_LAT = " << minLat << std::endl; + std::cout << "MAX_LAT = " << maxLat << std::endl; + std::cout << std::endl; + + double rlanLon = minLon*0.75 + maxLon*0.25; + double rlanLat = minLat*0.75 + maxLat*0.25; + double fsLon = minLon*0.25 + maxLon*0.75; + double fsLat = minLat*0.25 + maxLat*0.75; + +// fsLon = -73.98427; +// fsLat = 40.74801; + + double rlanX = rlanLon; + double rlanY = rlanLat; + double fsX = fsLon; + double fsY = fsLat; + + int transformSuccess; + testCoordTransform->TransformEx(1, &rlanX, &rlanY, nullptr, &transformSuccess); + testCoordTransform->TransformEx(1, &fsX, &fsY, nullptr, &transformSuccess); + + std::cout << "RLAN " << " : " << rlanX << " " << rlanY << " " << rlanLon << " " << rlanLat << std::endl; + std::cout << "FS " << " : " << fsX << " " << fsY << " " << fsLon << " " << fsLat << std::endl; + + OGRPoint rlanPoint; + OGRPoint fsPoint; + + rlanPoint.setX(rlanX); + rlanPoint.setY(rlanY); + fsPoint.setX(fsX); + fsPoint.setY(fsY); + + GdalHelpers::GeomUniquePtr signalPath(GdalHelpers::createGeometry()); + signalPath->addPoint(rlanX, rlanY); + signalPath->addPoint(fsX, fsY); + + std::vector idList; + + _ptrLayer->SetSpatialFilter(signalPath.get()); // Filter entire building database for only polygons which intersect with signal path into constrained layer + + numPolygon = 0; + _ptrLayer->ResetReading(); + while ((ptrOFeature = std::unique_ptr(_ptrLayer->GetNextFeature())) != NULL) { + // GDAL maintains ownership of returned pointer, so we should not manage its memory + OGRGeometry *ptrOGeometry = ptrOFeature->GetGeometryRef(); + + std::cout << std::setprecision(25); + // Check whether the current geometry is a polygon + if (ptrOGeometry != NULL && ptrOGeometry->getGeometryType() == OGRwkbGeometryType::wkbPolygon) { + OGRPolygon *poly = (OGRPolygon *) ptrOGeometry; + std::cout << "POLYGON: " << numPolygon << std::endl; + + std::cout << "FEATURE ID: " << ptrOFeature->GetFID() << std::endl; + + OGRLinearRing *pRing = poly->getExteriorRing(); + std::cout << "NUM_POINTS = " << pRing->getNumPoints() << std::endl << std::endl; + for(int ptIdx=0; ptIdxgetNumPoints(); ptIdx++) { + double lon = pRing->getX(ptIdx); + double lat = pRing->getY(ptIdx); + + // Perform transform into WGS-84 lat/lon coordinate system + int allTransformSuccess = invCoordTransform->TransformEx(1, &lon, &lat, nullptr, &transformSuccess); + + std::cout << " POINT " << ptIdx << " : " << pRing->getX(ptIdx) << " " << pRing->getY(ptIdx) << " " << lon << " " << lat << std::endl; + } + + double flag = true; + if (ptrOGeometry->Contains(&rlanPoint)) { + std::cout << "CONTAINS RLAN" << std::endl; + flag = false; + } + if (ptrOGeometry->Contains(&fsPoint)) { + std::cout << "CONTAINS FS" << std::endl; + flag = false; + } + + if (flag) { + idList.push_back(ptrOFeature->GetFID()); + } + + std::cout << std::endl; + } else { + std::cout << "GdalDataModel::getBuildingsAtPoint(): Can't find polygon geometries in current feature"; + } + + numPolygon++; + } + + std::cout << "NUM_POLYGON_IN_PATH = " << numPolygon << std::endl; + + std::cout << "NUM_POLYGON_NOT_CONTAIN_ENDPTS = " << idList.size() << std::endl; + +} + diff --git a/tools/geo_converters/proc_gdal/GdalDataModel.h b/tools/geo_converters/proc_gdal/GdalDataModel.h new file mode 100644 index 0000000..1982802 --- /dev/null +++ b/tools/geo_converters/proc_gdal/GdalDataModel.h @@ -0,0 +1,44 @@ +#ifndef INCLUDE_GDALDATAMODEL_H +#define INCLUDE_GDALDATAMODEL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include "GdalHelpers.h" + +// Uses OGR from GDAL v1.11 to read in .shp files +#include "ogrsf_frmts.h" + +class GdalDataModel +{ +public: + GdalDataModel(const std::string &dataSourcePath, const std::string &heightFieldName); + ~GdalDataModel(); + + // The return object is owned by this instance of the class + GDALDataset *getDataSource() { return _ptrDataSource; } + // The return object is owned by this instance of the class + OGRLayer *getLayer() { return _ptrLayer; } + + OGRSpatialReference* testSrcSpatialRef; + OGRSpatialReference* testDestSpatialRef; + OGRCoordinateTransformation *testCoordTransform; + OGRCoordinateTransformation *invCoordTransform; + + double getMaxBuildingHeightAtPoint(double latDeg, double lonDeg) const; + std::map getBuildingsAtPoint(double lat, double lon) const; + + void printDebugInfo() const; + + int heightFieldIdx; +private: + GDALDataset *_ptrDataSource; + OGRLayer *_ptrLayer; +}; + +#endif // INCLUDE_GDALDATAMODEL_H diff --git a/tools/geo_converters/proc_gdal/GdalHelpers.cpp b/tools/geo_converters/proc_gdal/GdalHelpers.cpp new file mode 100644 index 0000000..3e96969 --- /dev/null +++ b/tools/geo_converters/proc_gdal/GdalHelpers.cpp @@ -0,0 +1,172 @@ +// Copyright (C) 2017 RKF Engineering Solutions, LLC +#include "GdalHelpers.h" +#include "MultiGeometryIterable.h" +#include +#include +#include +#include +#include + +template<> +OGRPoint * GdalHelpers::createGeometry(){ + return static_cast(OGRGeometryFactory::createGeometry(wkbPoint)); +} + +template<> +OGRMultiPoint * GdalHelpers::createGeometry(){ + return static_cast(OGRGeometryFactory::createGeometry(wkbMultiPoint)); +} + +template<> +OGRLineString * GdalHelpers::createGeometry(){ + return static_cast(OGRGeometryFactory::createGeometry(wkbLineString)); +} + +template<> +OGRMultiLineString * GdalHelpers::createGeometry(){ + return static_cast(OGRGeometryFactory::createGeometry(wkbMultiLineString)); +} + +template<> +OGRLinearRing * GdalHelpers::createGeometry(){ + return static_cast(OGRGeometryFactory::createGeometry(wkbLinearRing)); +} + +template<> +OGRPolygon * GdalHelpers::createGeometry(){ + return static_cast(OGRGeometryFactory::createGeometry(wkbPolygon)); +} + +template<> +OGRMultiPolygon * GdalHelpers::createGeometry(){ + return static_cast(OGRGeometryFactory::createGeometry(wkbMultiPolygon)); +} + +template<> +OGRGeometryCollection * GdalHelpers::createGeometry(){ + return static_cast(OGRGeometryFactory::createGeometry(wkbGeometryCollection)); +} + +void GdalHelpers::OgrFreer::operator()(void *ptr) const{ + if(ptr){ + OGRFree(ptr); + } +} + +void GdalHelpers::GeometryDeleter::operator()(OGRGeometry *ptr) const{ + OGRGeometryFactory::destroyGeometry(ptr); +} + +void GdalHelpers::FeatureDeleter::operator()(OGRFeature *obj){ + OGRFeature::DestroyFeature(obj); +} + +void GdalHelpers::SrsDeleter::operator()(OGRSpatialReference *obj){ + OGRSpatialReference::DestroySpatialReference (obj); +} + +void GdalHelpers::coalesce(OGRGeometryCollection *target, OGRGeometryCollection *source){ + for(OGRGeometry *item : MultiGeometryIterableMutable(*source)){ + target->addGeometryDirectly(item); + } + while(!source->IsEmpty()){ + source->removeGeometry(0, FALSE); + } +} + + +std::string GdalHelpers::exportWkb(const OGRGeometry &geom){ + std::ostringstream errStr; + const size_t wkbSize = geom.WkbSize(); + std::unique_ptr wkbData(new unsigned char[wkbSize]); + const int status = geom.exportToWkb( + wkbXDR, + wkbData.get() + ); + if(status != OGRERR_NONE){ + errStr << "Failed to export WKB: code " << status; + throw std::runtime_error(errStr.str()); + } + return std::string(reinterpret_cast(wkbData.get()), wkbSize); +} + +std::string GdalHelpers::exportWkt(const OGRGeometry *geom){ + if(!geom){ + return std::string(); + } + + char *wktData; + const int status = geom->exportToWkt(&wktData); + if(status != OGRERR_NONE){ + std::ostringstream errStr; + errStr << "Failed to export WKT: code " << status; + throw std::runtime_error(errStr.str()); + } + std::unique_ptr wrap(wktData); + + return std::string(wktData); +} + +OGRGeometry * GdalHelpers::importWkt(const std::string &data){ + if(data.empty()){ + return nullptr; + } + + OGRGeometry *geom = nullptr; + std::string edit(data); + char *front = const_cast(edit.data()); + const int status = OGRGeometryFactory::createFromWkt(&front, nullptr, &geom); + if(status != OGRERR_NONE){ + delete geom; + std::ostringstream errStr; + errStr << "Failed to import WKT: code " << status; + throw std::runtime_error(errStr.str()); + } + return geom; +} + +std::string GdalHelpers::exportWkt(const OGRSpatialReference *srs){ + if(!srs){ + return std::string(); + } + + char *wktData; + const int status = srs->exportToWkt(&wktData); + if(status != OGRERR_NONE){ + std::ostringstream errStr; + errStr << "Failed to export WKT: code " << status; + throw std::runtime_error(errStr.str()); + } + std::unique_ptr wrap(wktData); + + return std::string(wktData); + +} + +std::string GdalHelpers::exportProj4(const OGRSpatialReference *srs){ + if(!srs){ + return std::string(); + } + + char *projData; + const int status = srs->exportToProj4(&projData); + std::unique_ptr wrap(projData); + if(status != OGRERR_NONE){ + std::ostringstream errStr; + errStr << "Failed to export Proj.4: code " << status; + throw std::runtime_error(errStr.str()); + } + + return std::string(projData); +} + +OGRSpatialReference * GdalHelpers::importWellKnownGcs(const std::string &name) { + std::unique_ptr srs(new OGRSpatialReference()); + const auto status = srs->SetWellKnownGeogCS(name.data()); + if (status != OGRERR_NONE) { + std::ostringstream errStr; + errStr << "Failed to import well-known GCS: code " << status; + throw std::runtime_error(errStr.str()); + } + return srs.release(); +} diff --git a/tools/geo_converters/proc_gdal/GdalHelpers.h b/tools/geo_converters/proc_gdal/GdalHelpers.h new file mode 100644 index 0000000..083a046 --- /dev/null +++ b/tools/geo_converters/proc_gdal/GdalHelpers.h @@ -0,0 +1,138 @@ +// Copyright (C) 2017 RKF Engineering Solutions, LLC +#ifndef SRC_KSCEGEOMETRY_GDALHELPERS_H_ +#define SRC_KSCEGEOMETRY_GDALHELPERS_H_ + +#include +#include + +class OGRGeometry; +class OGRPoint; +class OGRMultiPoint; +class OGRLineString; +class OGRMultiLineString; +class OGRLinearRing; +class OGRPolygon; +class OGRMultiPolygon; +class OGRGeometryCollection; +class OGRFeature; +class OGRSpatialReference; + +namespace GdalHelpers{ + +/** A helper function to construct new OGRGeometry-derived objects in the + * proper dynamic library memory space (specifically for windows DLLs). + * + * @tparam Typ A class derived from OGRGeometry. + * @return A new default instance of the desired class. + */ +template +Typ * createGeometry() = delete; + +template<> +OGRPoint * createGeometry(); + +template<> +OGRMultiPoint * createGeometry(); + +template<> +OGRLineString * createGeometry(); + +template<> +OGRMultiLineString * createGeometry(); + +template<> +OGRLinearRing * createGeometry(); + +template<> +OGRPolygon * createGeometry(); + +template<> +OGRMultiPolygon * createGeometry(); + +template<> +OGRGeometryCollection * createGeometry(); + +/** Delete OGR data with OGRFree() in a DLL-safe way. + */ +class OgrFreer{ +public: + /// Interface for std::unique_ptr deleter + void operator()(void *ptr) const; +}; + +/** Delete OGRGeometry objects in a DLL-safe way. +*/ +class GeometryDeleter{ +public: + /// Interface for std::unique_ptr deleter + void operator()(OGRGeometry *ptr) const; +}; + +/// Convenience name for unique-pointer class +template +using GeomUniquePtr = std::unique_ptr; + +/** Delete OGRFeature objects in a DLL-safe way. +*/ +class FeatureDeleter{ +public: + /// Interface for std::unique_ptr deleter + void operator()(OGRFeature *obj); +}; + +/** Delete OGRSpatialReference objects in a DLL-safe way. +*/ +class SrsDeleter{ +public: + /// Interface for std::unique_ptr deleter + void operator()(OGRSpatialReference *obj); +}; + +/** Move contained geometries from one collection to another. + * @param target The target into which all geometries are moved. + * @param source The source from which geometries are taken. + * @post The @c source is empty. + */ +void coalesce(OGRGeometryCollection *target, OGRGeometryCollection *source); + +/** Export geometry to a Well Known Binary representation. + * + * @param geom The geometry object to export. + * @return The WKB byte string for the geometry. + * The data is in network byte order (big-endian). + */ +std::string exportWkb(const OGRGeometry &geom); + +/** Export geometry to Well Known Text representation. + * A null geometry has an empty array. + * @param geom The object to export, which may be null. + * Ownership is kept by the caller. + * @return The WKT string for the geometry. + */ +std::string exportWkt(const OGRGeometry *geom); + +/** Import geometry from Well Known Text representation. + * An empty array will result in a null geometry. + * @param data The WKT string for the geometry. + * @return The new created geometry. + * Ownership is taken by the caller. + */ +OGRGeometry * importWkt(const std::string &data); + +/** Export SRS to Well Known Text representation. + * @param srs Pointer to the object to export, which may be null. + * @return The WKT string for the SRS. + */ +std::string exportWkt(const OGRSpatialReference *srs); + +/** Export SRS to Proj.4 representation. + * @param srs Pointer to the object to export, which may be null. + * @return The proj.4 string for the SRS. + */ +std::string exportProj4(const OGRSpatialReference *srs); + +OGRSpatialReference * importWellKnownGcs(const std::string &name); + +} + +#endif /* SRC_KSCEGEOMETRY_GDALHELPERS_H_ */ diff --git a/tools/geo_converters/proc_gdal/MultiGeometryIterable.h b/tools/geo_converters/proc_gdal/MultiGeometryIterable.h new file mode 100644 index 0000000..5c8275f --- /dev/null +++ b/tools/geo_converters/proc_gdal/MultiGeometryIterable.h @@ -0,0 +1,113 @@ +// Copyright (C) 2017 RKF Engineering Solutions, LLC +#ifndef MULTI_GEOMETRY_ITERABLE_H +#define MULTI_GEOMETRY_ITERABLE_H + +#include +#include +#include + +/** Wrap an OGRMultiPoint object and allow STL-style iteration. + */ +template +class MultiGeometryIterable{ +public: + /** Create a new iterable wrapper. + * + * @param geom The multi-geometry to iterate over. + */ + MultiGeometryIterable(Container &geom) + : _geom(&geom){} + + /// Shared iterator implementation + template + class base_iterator{ + public: + /** Define a new iterator. + * + * @param geom The geometry to access. + * @param index The point index to access. + */ + base_iterator(Container &geom, int index) + : _geom(&geom), _ix(index){} + + + /** Dereference to a single item pointer. + * + * @return The item at this iterator. + */ + Contained * operator * () const{ + auto *sub = _geom->getGeometryRef(_ix); + if(!sub){ + throw std::logic_error("MultiGeometryIterable null dereference"); + } + return static_cast(sub); + } + + /** Determine if two iterators are identical. + * + * @param other The iterator to compare against. + * @return True if the other iterator is the same. + */ + bool operator == (const base_iterator &other) const{ + return ((_geom == other._geom) && (_ix == other._ix)); + } + + /** Determine if two iterators are identical. + * + * @param other The iterator to compare against. + * @return True if the other iterator is different. + */ + bool operator != (const base_iterator &other) const{ + return !operator==(other); + } + + /** Pre-increment operator. + * + * @return A reference to this incremented object. + */ + base_iterator & operator ++ (){ + ++_ix; + return *this; + } + + private: + /// External geometry being accessed + Container *_geom; + /// Geometry index + int _ix; + }; + + /// Iterator for mutable containers + typedef base_iterator iterator; + /// Iterator for immutable containers + typedef base_iterator const_iterator; + + iterator begin(){ + return iterator(*_geom, 0); + } + iterator end(){ + return iterator(*_geom, _geom->getNumGeometries()); + } + +private: + /// Reference to the externally-owned object + Container *_geom; +}; + +template +class MultiGeometryIterableConst + : public MultiGeometryIterable{ +public: + MultiGeometryIterableConst(const OGRGeometryCollection &geom) + : MultiGeometryIterable(geom){} +}; + +template +class MultiGeometryIterableMutable + : public MultiGeometryIterable{ +public: + MultiGeometryIterableMutable(OGRGeometryCollection &geom) + : MultiGeometryIterable(geom){} +}; + +#endif /* MULTI_GEOMETRY_ITERABLE_H */ diff --git a/tools/geo_converters/proc_gdal/README.md b/tools/geo_converters/proc_gdal/README.md new file mode 100644 index 0000000..55e6982 --- /dev/null +++ b/tools/geo_converters/proc_gdal/README.md @@ -0,0 +1,56 @@ +# Lidar processing. + +## Download Data + +Lidar data can be downloaded from: +https://rockyweb.usgs.gov/vdelivery/Datasets/Staged/Elevation/Non_Standard_Contributed/NGA_US_Cities/ + +## Processing + +The attached script, `create_lidar_database_threaded.pl` was used to process the downloaded lidar data to the format used by the afc-engine. This script is written in perl and makes use of the following standard gdal programs: + +- `gdalbuildvrt` +- `gdalinfo` +- `gdal_rasterize` +- `gdal_translate` +- `gdalwarp` +- `ogr2ogr` +- `ogrinfo` +- `ogrmerge.py` + +The script also makes use of a custom C++ program: + +- `proc_gdal` + +On line 68 of `create_lidar_database_threaded.pl`, the following variables need to be set: + `$name` = name of dataset, used for some intermediate file names, not really important, I used "NGA_US_Cities". + `$sourceDir` = path to directory that contains downloaded lidar data. + `$destDir` = path to directory where processed lidar files will be placed. + `$tmpDir` = temporary directory where numerous intermediate files are generated. + `$numThread` = this perl script is threaded, number of threads to run. + +The script has several command line options. Running `create_lidar_database_threaded.pl -h` will list the command line options and gives a short description of each option. + +The downloaded lidar data is organized into a separate directory for each city. The script takes a list of cities and processes the lidar data for those cities. Line 114 defines the perl list `@cityList`, where each item in the list is the full path to the directory of a single city. Optionally, each city can have several parameters that control the processing for that city. These parameters are appended to the full path directory separated by colons ':'. + +Each city has an independent dataset. The function `processCity()` processes the lidar data for a single city. To parallelize this script, each city is processed in its own thread. The core processing of this script is in the function `processCity()`. Note that for each city, the file format, tile sizes, gdal coordinate system used, and several other parameters are not the same for each city. Also, the representation of "invalid values" in each dataset is not the same for all cities. These are challenges in developing this script, and make the implementation somewhat less than elegant. + +The function `processCity()` begins by hierarchically traversing the directory for the city looking for subdirectories that contain the following directories: + +- BE : subdirectory containing bare-earth raster data +- VEC/BLD3 : subdirectory containing 3D building data +- VEC/BLD2 : subdirectory containing 32 building data + +The list `@subdirlist` is created and contains a list of subdirectories that contain both bare-earth and building data. These subdirectories are then processed one at a time. + +For each subdirectory, the list `@srcBElist` is created and is a list of the bare earth raster files under the subdirectory. These files have a file extension of `.img` or `.tif`. Each of these files is "fixed" by removing invalid values and properly setting nodata values. Also, an image file showing coverage is created. This is done using the function fixRaster which executes the program `proc_gdal`. Next, the extents of each file (minLon, maxLon, minLat, maxLat) are determined using `gdalinfo`. + +The list `@srcBldglist` is created and is a list of 3D vector building data files under the subdirectory. These files have a file extension of `.shp`. The extents of each file are determined using `ogrinfo`. Each file is also checked to contain a field recognized as building height. Recognized field names are: `topelev_m`, `TOPELEV_M`, `TopElev3d`, `TopElev3D`, `TOPELEV3D`. Each of these files is "fixed" by changing the name of the building height field to `topelev_m` and by replacing MultiPolygon with polygons. Also, an image file showing coverage is created. This is done using the function `fixVector()` which executes the program `proc_gdal`. + +The list `@srcBldg2list` is created and is a list of 2D vector building data files under the subdirectory. These files have a file extension of `.shp`. The extents of each file are determined using `ogrinfo`. Each file is also checked to contain a field recognized as building height. Recognized field names are: `maxht_m`, `MAXHT_M`, `HGT2d`. Each of these files is "fixed" by changing the name of the building height field to `topelev_m` and by replacing MultiPolygon with polygons. Also, an image file showing coverage is created. This is done using the function fixVector which executes the program `proc_gdal`. + +The extents of all the bare earth files in the subdirectory are combined to find the total extents of all bare earth data in the subdirectory. This is also done for the 3D and 2D building data. KML is generated that shows where there is source lidar data. Bare earth, 3D building, and 2D building data are all shown in the KML. + +The script then iterates through bare-earth files and building files looking for a single bare earth file that covers the same region as a single building file. This is done by comparing the extents of the file. The function `isMatch()` compares extents and returns 1 if the overlap area is $\geq$ 80% of the total combined area. Next the scripts looks for matches by grouping multiple bare-earth files and multiple building files. + +For each match, the gdal program `gdalbuildvrt` is used to combine the bare-earth files into a single bare-earth file. Next, `gdalwarp` is used to set the gdal coordinate system used to be a WGS84, and a sampling resolution of $10^{-5}$ degrees which corresponds to 1.11 meters. Next the gdal program `ogr2ogr` is run on each building file in the match to remove all fields except the building height field. Then `ogrmerge.py` is run to combine these building files into a single building file. Then `ogr2ogr` is run to convert the building file into an sqlite format using the WGS84 coordinate system. Next, `gdal_rasterize` is run to convert the building polygon file into raster data having the same extents and resolution as the bare-earth data. The next step is to then use `gdalbuildvrt` to combine the overlapping bare-earth and building raster files into a single file with 2 layers. This 2 layer file has bare earth data on layer 1 and building data on layer 2. Finally, this this multilayer raster file is split into rectangular pieces where each dimension is $\leq$ `$gpMaxPixel`. The files are split using `gdal_translate` and the resulting files are saved in a multilayer raster `.tif` format. These are the final processed files that are used by the `afc-engine`. diff --git a/tools/geo_converters/proc_gdal/cltn6ghz.pro b/tools/geo_converters/proc_gdal/cltn6ghz.pro new file mode 100644 index 0000000..6bd44ba --- /dev/null +++ b/tools/geo_converters/proc_gdal/cltn6ghz.pro @@ -0,0 +1,23 @@ +###################################################################### +# Automatically generated by qmake (2.01a) Wed Feb 8 15:29:23 2012 +###################################################################### + +TEMPLATE = app +TARGET = cltn6ghz +DEPENDPATH += . +INCLUDEPATH += . /usr/include/gdal +CONFIG += release + +QMAKE_CXXFLAGS_WARN_ON += -Werror=format-extra-args +QMAKE_CXXFLAGS_WARN_ON += -Werror=format +QMAKE_CXXFLAGS_WARN_ON += -Werror=shadow +QMAKE_CXXFLAGS_WARN_ON += -Werror=return-type +QMAKE_CXXFLAGS += -std=gnu++11 +QMAKE_LIBS += -lz -lgdal -larmadillo -liturp452 + +DEFINES += QT_NO_DEBUG_OUTPUT + +# Input +HEADERS += $$files(*.h) + +SOURCES += $$files(*.cpp) diff --git a/tools/geo_converters/proc_gdal/create_lidar_database_threaded.pl b/tools/geo_converters/proc_gdal/create_lidar_database_threaded.pl new file mode 100755 index 0000000..2d78f96 --- /dev/null +++ b/tools/geo_converters/proc_gdal/create_lidar_database_threaded.pl @@ -0,0 +1,2107 @@ +#!/usr/bin/perl -w +################################################################################ +#### FILE: create_lidar_database.pl #### +#### Take list of cities for which src lidar data has been downloaded. #### +#### This data consists of: #### +#### (1) Bare Earth raster files #### +#### (2) 3D building shape files #### +#### (3) 2D building shape files (only when 3D files missing) #### +#### For each city: #### +#### (1) Identity bare earth and building files available. #### +#### (2) Identify pairs of files where a pair consists of a bare earth #### +#### and building polygon file that cover the same region. #### +#### (3) Convert bare earth into tif raster file, and convert building #### +#### polygon file into tif raster file where both files are on same #### +#### lon/lat grid. #### +#### (4) Combine bare earth and building tif files into a single tif #### +#### file with bare earth on Band 1 and building height on band 2. #### +#### (5) Under target directory create directory for each city, under #### +#### each city create dir structure containing combined tif files. #### +#### (6) For each city create info file listing tif files and min/max #### +#### lon/lat for each file. #### +#### Optionally create kml file: #### +#### (1) Src lidar data that has been downloaded. #### +#### (2) Rectangular bounding box for each file. #### +#### (3) Coverage region for shape files. #### +#### (4) Identify regions with no data in raster files. #### +################################################################################ + +use File::Path; +use List::Util; +use Cwd; +# use POSIX; +use strict; +use threads; +use threads::shared; +use IO::Handle; + +my $pi = 2*atan2(1.0, 0.0); +my $earthRadius = 6378.137e3; + +my $interactive = 0; +my $query = 1; +my $cleantmp = 1; +my $lonlatRes = "0.00001"; +my $gpMaxPixel = 32768; +my $nodataVal = 1.0e30; +my $imageLonLatRes = 0.001; # For kml image of vector coverage +my $heightFieldName = "topelev_m"; +my $processLidarFlag = 1; +my $srcKMLFlag = 1; +my $inclKMLBoundingBoxes = 0; + +my $sourceDir; +my $destDir; +my $tmpDir; +my $name; + +my $dataset; +# $dataset = "2019"; +# $dataset = "old"; +$dataset = "test"; + +if ($dataset eq "2019") { + $sourceDir = "."; # set path to dataset + $destDir = "."; # set path to destination directory + $tmpDir = "."; # set path to tmp directory + $name = "NGA_US_Cities"; +} elsif ($dataset eq "old") { + $sourceDir = "."; # set path to dataset + $destDir = "."; # set path to destination directory + $tmpDir = "."; # set path to tmp directory + $name = "old_lidar"; +} elsif ($dataset eq "test") { + $sourceDir = "."; # set path to dataset + $destDir = "."; # set path to destination directory + $tmpDir = "."; # set path to tmp directory + $name = "NGA_US_Cities"; +} else { + die "ERROR: dataset set to illegal value: $dataset : $!\n"; +} + +my $kmlTmp = "${tmpDir}/kml"; +my $logTmp = "${tmpDir}/log"; +my $srcKML = "${destDir}/src_${name}.kmz"; + +my $defaultRasterClampMin = -100.0; +my $defaultRasterClampMax = 5000.0; +my $defaultRasterMinMag = 0.0; + +my @cityList; + +##################################################################### +# Declare shared variables, set numThread +my @unprocessedCityList:shared; +my %loghash:shared; +my %kmlhash:shared; +my %discardhash:shared; + +my $numThread = 2; +##################################################################### +my $cityStartIdx = 0; # For debugging + +if ($dataset eq "old") { + @cityList = ( + "${sourceDir}/Washington_DC", + + "" + ); +} elsif ($dataset eq "test") { + @cityList = ( + # "${sourceDir}/Norfolk_VA", + # "${sourceDir}/Lancaster_PA", + # "${sourceDir}/Winston-Salem_NC:100:350:0.0", + # "${sourceDir}/Atlanta_GA", + # "${sourceDir}/San_Francisco_CA", + # "${sourceDir}/Jackson_MS", + # "${sourceDir}/Augusta_ME", + # "${sourceDir}/Cleveland_OH", + # "${sourceDir}/Chattanooga_TN", + "${sourceDir}/Norfolk_VA", + "${sourceDir}/Providence_RI", + "${sourceDir}/Cincinnati_OH", + "${sourceDir}/New_Orleans_LA", + "${sourceDir}/Chicago_IL", + "${sourceDir}/Tampa_FL", + "${sourceDir}/Grand_Rapids_MI", + "${sourceDir}/New_York_NY", + "${sourceDir}/Baton_Rouge_LA", + "${sourceDir}/Los_Angeles_CA", + "${sourceDir}/Charlotte_NC", + "${sourceDir}/Denver_CO", + "" + ); +} elsif ($dataset eq "2019") { +# $cityStartIdx = 93; +@cityList = ( + + # The selected 10 cities + "${sourceDir}/New_York_NY", + + "${sourceDir}/Atlanta_GA", + "${sourceDir}/Boston_MA", + "${sourceDir}/Chicago_IL", + "${sourceDir}/Dallas_TX", + "${sourceDir}/Houston_TX", + "${sourceDir}/Los_Angeles_CA", + "${sourceDir}/Miami_FL", + "${sourceDir}/Philadelphia_PA", + "${sourceDir}/San_Francisco_CA", + + "${sourceDir}/Albany_NY", + "${sourceDir}/Albuquerque_NM", + "${sourceDir}/Allentown_PA", + "${sourceDir}/Amarillo_TX", + "${sourceDir}/Anaheim_CA", + "${sourceDir}/Anchorage_AK", + "${sourceDir}/Augusta_GA", + "${sourceDir}/Augusta_ME", + "${sourceDir}/Austin_TX", + "${sourceDir}/Bakersfield_CA", + "${sourceDir}/Baltimore_MD", + "${sourceDir}/Barre_Montpelier_VT", + "${sourceDir}/Baton_Rouge_LA", + "${sourceDir}/Birmingham_AL", + "${sourceDir}/Bismarck_ND", + "${sourceDir}/Boise_ID", + "${sourceDir}/Bridgeport_CT", + "${sourceDir}/Buffalo_NY", + "${sourceDir}/Carson_City_NV", + "${sourceDir}/Charleston_SC", + "${sourceDir}/Charleston_WV", + "${sourceDir}/Charlotte_NC", + "${sourceDir}/Chattanooga_TN", + "${sourceDir}/Cheyenne_WY", + "${sourceDir}/Cincinnati_OH", + "${sourceDir}/Cleveland_OH", + "${sourceDir}/Colorado_Springs_CO", + "${sourceDir}/Columbia_SC", + "${sourceDir}/Columbus_GA", + "${sourceDir}/Columbus_OH", + "${sourceDir}/Concord_NH", + "${sourceDir}/Corpus_Christi_TX", + "${sourceDir}/Dayton_OH", + "${sourceDir}/Denver_CO", + "${sourceDir}/DesMoines_IA", + "${sourceDir}/Detroit_MI", + "${sourceDir}/Dover_DE", + "${sourceDir}/El_Paso_TX", + "${sourceDir}/Flint_MI", + "${sourceDir}/Fort_Wayne_IN", + "${sourceDir}/Frankfort_KY", + "${sourceDir}/Fresno_CA", + "${sourceDir}/Grand_Rapids_MI", + "${sourceDir}/Greensboro_NC", + "${sourceDir}/Harrisburg_PA", + "${sourceDir}/Hartford_CT", + "${sourceDir}/Helena_MT", + "${sourceDir}/Honolulu_HI", + "${sourceDir}/Huntsville_AL", + "${sourceDir}/Indianapolis_IN", + "${sourceDir}/Jackson_MS", + "${sourceDir}/Jacksonville_FL", + "${sourceDir}/Jefferson_City_MO", + "${sourceDir}/Juneau_AK", + "${sourceDir}/Kansas_City_MO", + "${sourceDir}/Knoxville_TN", + "${sourceDir}/Lancaster_PA", + "${sourceDir}/Lansing_MI", + "${sourceDir}/Las_Vegas_NV", + "${sourceDir}/Lexington_KY", + "${sourceDir}/Lincoln_NE", + "${sourceDir}/Little_Rock_AR", + "${sourceDir}/Louisville_KY", + "${sourceDir}/Lubbock_TX", + "${sourceDir}/Madison_WI", + "${sourceDir}/McAllen_TX", + "${sourceDir}/Memphis_TN", + "${sourceDir}/Milwaukee_WI", + "${sourceDir}/Minneapolis_MN", + "${sourceDir}/Mission_Viejo_CA", + "${sourceDir}/Mobile_AL", + "${sourceDir}/Modesto_CA", + "${sourceDir}/Montgomery_AL", + "${sourceDir}/Nashville_TN", + "${sourceDir}/New_Haven_CT", + "${sourceDir}/New_Orleans_LA", + "${sourceDir}/Norfolk_VA", + "${sourceDir}/Oklahoma_City_OK", + "${sourceDir}/Olympia_WA", + "${sourceDir}/Omaha_NE", + "${sourceDir}/Orlando_FL", + "${sourceDir}/Oxnard_CA", + "${sourceDir}/Palm_Bay_FL", + "${sourceDir}/Pensacola_FL", + "${sourceDir}/Phoenix_AZ", + "${sourceDir}/Pierre_SD", + "${sourceDir}/Pittsburgh_PA", + "${sourceDir}/Portland_OR", + "${sourceDir}/Poughkeepsie_NY", + "${sourceDir}/Providence_RI", + "${sourceDir}/Raleigh-Durham_NC:$defaultRasterClampMin:$defaultRasterClampMax:0.00001", + "${sourceDir}/Reno_NV", + "${sourceDir}/Richmond_VA", + "${sourceDir}/Riverside-San_Bernardino_CA", + "${sourceDir}/Rochester_NY", + "${sourceDir}/Sacramento_CA", + "${sourceDir}/Salem_OR", + "${sourceDir}/Salt_Lake_City_UT", + "${sourceDir}/San_Antonio_TX", + "${sourceDir}/San_Diego_CA", + "${sourceDir}/San_Jaun_PR", + "${sourceDir}/Santa_Fe_NM", + "${sourceDir}/Sarasota_FL", + "${sourceDir}/Scranton_PA", + "${sourceDir}/Seattle_WA", + "${sourceDir}/Shreveport_LA", + "${sourceDir}/Spokane_WA", + "${sourceDir}/Springfield_IL", + "${sourceDir}/Springfield_MA", + "${sourceDir}/St_Louis_MO", + "${sourceDir}/Stockton_CA", + "${sourceDir}/Syracuse_NY", + "${sourceDir}/Tallahassee_FL", + "${sourceDir}/Tampa_FL", + "${sourceDir}/Toledo_OH", + "${sourceDir}/Topeka_KS", + "${sourceDir}/Trenton_NJ", + "${sourceDir}/Tucson_AZ", + "${sourceDir}/Tulsa_OK", + "${sourceDir}/Vancouver_BC", + "${sourceDir}/Whistler_Mountain_BC", + "${sourceDir}/Wichita_KS", + "${sourceDir}/Winston-Salem_NC:100:350:0.0", + "${sourceDir}/Worcester_MA", + "${sourceDir}/Youngstown_OH", + + "" +); +} else { + die "ERROR: dataset set to illegal value: $dataset : $!\n"; +} +pop(@cityList); + +splice @cityList,0,$cityStartIdx; + +@unprocessedCityList = @cityList; + +$| = 1; + +my $discardedFiles; + +my $helpmsg = + "Script create_lidar_database.pl options:\n" + . " -no_query Don't prompt for comfirmation before running\n" + . " -no_process_lidar Don't actually create processed lidar files\n" + . " -no_source_kml Don't create source kml file\n" + . " -no_cleantmp Don't remove files from tmp, useful for debugging\n" + . " -i Run in interactive mode prompting before each command is executed\n" + . " -h Print this help message\n" + . "\n"; + +while ($_ = $ARGV[0]) { + shift; + if (/^-no_query/) { + $query = 0; + } elsif (/^-no_cleantmp/) { + $cleantmp = 0; + } elsif (/^-no_process_lidar/) { + $processLidarFlag = 0; + } elsif (/^-no_source_kml/) { + $srcKMLFlag = 0; + } elsif (/^-i/) { + $interactive = 1; + } elsif (/^-h/) { + print $helpmsg; + exit; + } else { + print "ERROR: Invalid command line options\n"; + exit; + } +} + +my $cityIdx; +print "Processing Lidar Data for " . int(@cityList) . " cities:\n"; +for ($cityIdx=0; $cityIdx); + if ($resp ne "y") { + exit; + } + print "\n"; +} + +if (!(-e $destDir)) { + die "ERROR: Destination directory: $destDir does not exist : $!\n"; +} + +if (-e $tmpDir) { + print "Removing Tmp Directory \"$tmpDir\" ... \n"; + rmtree(${tmpDir}); +} + +if (-e $tmpDir) { + die "ERROR removing Tmp Directory \"$tmpDir\"\n"; +} else { + print "Tmp Directory \"$tmpDir\" does not exist, creating ... \n"; + mkpath($tmpDir, 0, 0755); + if (-e $tmpDir) { + print "Tmp Directory \"$tmpDir\" successfully created.\n"; + } else { + die "ERROR: Unable to create directory: $tmpDir : $!\n"; + } +} + +if ($srcKMLFlag) { + mkpath("${kmlTmp}", 0, 0755); + if (!(-e "${kmlTmp}")) { + die "ERROR: Unable to create directory: ${kmlTmp} : $!\n"; + } +} + +mkpath("${logTmp}", 0, 0755); +if (!(-e "${logTmp}")) { + die "ERROR: Unable to create directory: ${logTmp} : $!\n"; +} + +my $tstart = time; + +my $thrIdx; +my @thrList; +for($thrIdx=0; $thrIdx<$numThread; $thrIdx++) { + $thrList[$thrIdx] = threads->create(\&runThread); +} + +for($thrIdx=0; $thrIdx<$numThread; $thrIdx++) { + $thrList[$thrIdx]->join(); +} + +############################################################################# +# Print log information +############################################################################# +if (1) { + my $city; + my $discardStr = ""; + foreach $city (sort @cityList) { + if (exists($loghash{$city})) { + print $loghash{$city}; + } + $discardStr .= $discardhash{$city}; + } + + my @discardList = split("\n", $discardStr); + print "TOTAL NUMBER OF DISCARDED FILES: " . int(@discardList) . "\n"; + + for(my $dIdx=0; $dIdx<@discardList; $dIdx++) { + print "($dIdx) $discardList[$dIdx]\n"; + } +} +############################################################################# + +############################################################################# +# Create source KML file +############################################################################# +if ($srcKMLFlag) { + my $kmlFile = "${kmlTmp}/doc.kml"; + open(SRCKMLFILE, ">${kmlFile}") || die "Can't open file $kmlFile : $!\n"; + print SRCKMLFILE genKML("HEAD"); + my $city; + foreach $city (sort @cityList) { + if (exists($kmlhash{$city})) { + print SRCKMLFILE $kmlhash{$city}; + } + } + print SRCKMLFILE genKML("TAIL"); + + close SRCKMLFILE; + + my $cwd = cwd(); + chdir($kmlTmp); + + my $cmd = "zip -r $srcKML ."; + print execute_command($cmd, $interactive); + + chdir($cwd); +} +############################################################################# + +my $tend = time; + +print "TOTAL RUNNING TIME = " . ($tend-$tstart) . " sec\n"; + +exit; + +################################################################################# +# runThread +################################################################################# +sub runThread +{ + my $city; + my $cont = 1; + do { + { + lock(@unprocessedCityList); + if (int(@unprocessedCityList) == 0) { + $cont = 0; + } else { + $city = shift(@unprocessedCityList); + } + } + + if ($cont) { + processCity($city); + } + } while($cont); +} +################################################################################# + +################################################################################# +# processCity +################################################################################# +sub processCity +{ + my $cityStr = $_[0]; + + my $kml = ""; + my $discard = ""; + + my @clist = split(":", $cityStr); + + my $city; + my $fixRasterParams = {}; + if (int(@clist) == 1) { + $city = $clist[0]; + $fixRasterParams->{clampMin} = $defaultRasterClampMin; + $fixRasterParams->{clampMax} = $defaultRasterClampMax; + $fixRasterParams->{minMag} = $defaultRasterMinMag; + } elsif (int(@clist) == 4) { + $city = $clist[0]; + $fixRasterParams->{clampMin} = $clist[1]; + $fixRasterParams->{clampMax} = $clist[2]; + $fixRasterParams->{minMag} = $clist[3]; + } else { + die "ERROR: Invalid cityStr = \"$cityStr\" : $!\n"; + } + + my $cityName = substr $city, rindex( $city, '/' ) + length '/'; + my $topDir = substr $city, 0, rindex( $city, '/' ); + my $infoFile = "${destDir}/${cityName}_info.csv"; + my $cmd; + my $t0 = time; + + print "Beginning: $cityName ...\n"; + + my $logFile = "${logTmp}/${cityName}_log.txt"; + open(LOGFILE, ">${logFile}") || die "Can't open file $logFile : $!\n"; + LOGFILE->autoflush(1); + + print LOGFILE "Processing City: $cityName\n"; + + if ($processLidarFlag) { + if (-e "${destDir}/${cityName}") { + die "ERROR: Destination directory ${destDir}/${cityName} already exists : $!\n"; + } + + ############################################################################# + # Open INFO file and write header + ############################################################################# + open(INFOFILE, ">$infoFile") || die "Can't open file $infoFile : $!\n"; + + print INFOFILE "FILE,MIN_LON_DEG,MAX_LON_DEG,MIN_LAT_DEG,MAX_LAT_DEG\n"; + ############################################################################# + } + + my @currPath = (); + + ############################################################################# + # Determine list of subdirectories under city + ############################################################################# + my @dirlist = ($cityName); + my @subdirlist; + + while(@dirlist) { + my $dir = pop @dirlist; + my $beDir = "${topDir}/${dir}/BE"; + my $bldgDir = "${topDir}/${dir}/VEC/BLD3"; + my $bldg2Dir = "${topDir}/${dir}/VEC/BLD2"; + if ((-e $beDir) && ((-e $bldgDir) || (-e $bldg2Dir)) ) { + if ((-d $beDir) && ((-d $bldgDir) || (-d $bldg2Dir)) ) { + push @subdirlist, $dir; + } + } + + opendir DIR, "${topDir}/$dir"; + my @filelist = grep !/^\.\.?$/, readdir DIR; + closedir DIR; + my $filename; + foreach $filename (@filelist) { + if (-d "${topDir}/${dir}/$filename") { + push @dirlist, "$dir/$filename"; + } + } + } + + # Sort subdirlist in reverse order so that when subdir contains a year, newest subdirs appear before older ones. + @subdirlist = sort {lc($b) cmp lc($a)} @subdirlist; + + print LOGFILE "Found ". (int(@subdirlist)) . " subdirectories: \n"; + my $sdIdx; + for($sdIdx=0; $sdIdx{"region_${beIdx}"}->{minLon} = $minLon; + $beData->{"region_${beIdx}"}->{maxLon} = $maxLon; + $beData->{"region_${beIdx}"}->{minLat} = $minLat; + $beData->{"region_${beIdx}"}->{maxLat} = $maxLat; + # print "$minLon, $minLat\n"; + # print "$minLon, $maxLat\n"; + # print "$maxLon, $maxLat\n"; + # print "$maxLon, $minLat\n"; + # print "$minLon, $minLat\n"; + # print "\n" + $beIdx++; + } else { + my $discFile = "${sd}/BE/" . $belist[$beIdx]; + print LOGFILE "Unable to extract coverage area for bare earth file: $discFile, DISCARDED\n"; + $discard .= "$discFile : Unable to extract coverage area for bare earth file\n"; + + splice @belist, $beIdx, 1; + } + } + ######################################################################### + + ######################################################################### + # Create list of SRC 3D Bldg files + ######################################################################### + my @srcBldglist = (); + if (-d $srcBldgDir) { + opendir DIR, $srcBldgDir; + @srcBldglist = grep /\.shp$/, readdir DIR; + closedir DIR; + } + ######################################################################### + + ######################################################################### + # For each SRC 3D Bldg file, get coverage area, run fixVector + # to replace MultiPolygon's with Polygons and count NUM_POLYGON + ######################################################################### + my $bldgDir = "${tmpDir}/fixed/${sd}/BLD3"; + my @bldglist = (); + if (!(-e "${bldgDir}")) { + mkpath("${bldgDir}", 0, 0755); + if (!(-e "${bldgDir}")) { + die "ERROR: Unable to create directory: ${bldgDir} : $!\n"; + } + } else { + die "ERROR: directory: ${bldgDir} already exists : $!\n"; + } + my $bldgData = {}; + my $srcBldgIdx; + my $bldgIdx = 0; + for($srcBldgIdx=0; $srcBldgIdx{"region_${bldgIdx}"}->{minLon} = $minLon; + $bldgData->{"region_${bldgIdx}"}->{maxLon} = $maxLon; + $bldgData->{"region_${bldgIdx}"}->{minLat} = $minLat; + $bldgData->{"region_${bldgIdx}"}->{maxLat} = $maxLat; + $bldgIdx++; + } else { + print LOGFILE "DISCARDED: $discardMsg"; + $discard .= $discardMsg; + } + } + ######################################################################### + + ######################################################################### + # Create list of SRC 2D Bldg files + ######################################################################### + my @srcBldg2list = (); + if (-d $srcBldg2Dir) { + opendir DIR, $srcBldg2Dir; + @srcBldg2list = grep /\.shp$/, readdir DIR; + closedir DIR; + } + ######################################################################### + + ######################################################################### + # For each SRC 2D Bldg file, get coverage area, run fixVector + # to replace MultiPolygon's with Polygons and count NUM_POLYGON + ######################################################################### + my $bldg2Dir = "${tmpDir}/fixed/${sd}/BLD2"; + my @bldg2list = (); + if (!(-e "${bldg2Dir}")) { + mkpath("${bldg2Dir}", 0, 0755); + if (!(-e "${bldg2Dir}")) { + die "ERROR: Unable to create directory: ${bldg2Dir} : $!\n"; + } + } else { + die "ERROR: directory: ${bldg2Dir} already exists : $!\n"; + } + my $bldg2Data = {}; + my $srcBldg2Idx = {}; + my $bldg2Idx = 0; + for($srcBldg2Idx=0; $srcBldg2Idx{"region_${bldg2Idx}"}->{minLon} = $minLon; + $bldg2Data->{"region_${bldg2Idx}"}->{maxLon} = $maxLon; + $bldg2Data->{"region_${bldg2Idx}"}->{minLat} = $minLat; + $bldg2Data->{"region_${bldg2Idx}"}->{maxLat} = $maxLat; + $bldg2Idx++; + } else { + print LOGFILE "DISCARDED: $discardMsg"; + $discard .= $discardMsg; + } + } + ######################################################################### + my $beRegion = {}; + for($beIdx=0; $beIdx{"region_${beIdx}"}); + } + my $bldgRegion = {}; + for($bldgIdx=0; $bldgIdx{"region_${bldgIdx}"}); + } + my $bldg2Region = {}; + for($bldg2Idx=0; $bldg2Idx{"region_${bldg2Idx}"}); + } + + my $sdRegion = {}; + $sdRegion = regionUnion($sdRegion, $beRegion); + $sdRegion = regionUnion($sdRegion, $bldgRegion); + $sdRegion = regionUnion($sdRegion, $bldg2Region); + + $cityRegion = regionUnion($cityRegion, $sdRegion); + + if ($srcKMLFlag) { + my @path = split("/", $sd); + + my $i; + my $currIdx; + my $found = 0; + for($i=0; ($i=$currIdx; $i--) { + $kml .= " \n"; + pop @currPath; + } + } + + $currIdx = int(@currPath); + + for($i=$currIdx; $i\n"; + if ($i == int(@path)-1) { + $s .= kmlLookAt($sdRegion); + } + $s .= " 1\n"; + + $kml .= $s; + + push @currPath, $path[$i]; + } + + my $s = ""; + + if (int(@belist)) { + + my $imageDir = "${kmlTmp}/${sd}/BE"; + if (!(-e "${imageDir}")) { + mkpath("${imageDir}", 0, 0755); + if (!(-e "${imageDir}")) { + die "ERROR: Unable to create directory: ${imageDir} : $!\n"; + } + } else { + die "ERROR: directory: ${imageDir} already exists : $!\n"; + } + + $s .= " \n"; + $s .= " Bare Earth\n"; + $s .= kmlLookAt($beRegion); + $s .= " 1\n"; + +if ($inclKMLBoundingBoxes) { + $s .= " \n"; + $s .= " Rect Bounding Box\n"; + $s .= " 1\n"; + my $beIdx; + for($beIdx=0; $beIdx{"region_${beIdx}"}->{minLon}); + my $maxLonStr = sprintf("%.10f", $beData->{"region_${beIdx}"}->{maxLon}); + my $minLatStr = sprintf("%.10f", $beData->{"region_${beIdx}"}->{minLat}); + my $maxLatStr = sprintf("%.10f", $beData->{"region_${beIdx}"}->{maxLat}); + + $s .= " \n"; + $s .= " $belist[$beIdx]\n"; + $s .= " 1\n"; + $s .= " #transBluePoly\n"; + $s .= " \n"; + $s .= " clampToGround\n"; + $s .= " \n"; + $s .= " \n"; + $s .= " \n"; + $s .= " $maxLonStr,$maxLatStr\n"; + $s .= " $maxLonStr,$minLatStr\n"; + $s .= " $minLonStr,$minLatStr\n"; + $s .= " $minLonStr,$maxLatStr\n"; + $s .= " \n"; + $s .= " \n"; + $s .= " \n"; + $s .= " \n"; + $s .= " \n"; + } + $s .= " \n"; +} + +if ($inclKMLBoundingBoxes) { + $s .= " \n"; + $s .= " Data Coverage\n"; + $s .= " 1\n"; +} + for($beIdx=0; $beIdx{"region_${beIdx}"}->{minLon}); + my $maxLonStr = sprintf("%.10f", $beData->{"region_${beIdx}"}->{maxLon}); + my $minLatStr = sprintf("%.10f", $beData->{"region_${beIdx}"}->{minLat}); + my $maxLatStr = sprintf("%.10f", $beData->{"region_${beIdx}"}->{maxLat}); + + my $imageBEFile = $belist[$beIdx]; + $imageBEFile =~ s/\.tif$/.png/; + + my $cmd = "cp ${beDir}/${imageBEFile} $imageDir"; + print LOGFILE execute_command($cmd, $interactive); + + $s .= " \n"; + $s .= " $belist[$beIdx]\n"; + + $s .= kmlLookAt($beData->{"region_${beIdx}"}); + + $s .= " 1\n"; + $s .= " 80ffffff\n"; + $s .= " \n"; + $s .= " ${sd}/BE/${imageBEFile}\n"; + $s .= " \n"; + $s .= " \n"; + $s .= " ${maxLatStr}\n"; + $s .= " ${minLatStr}\n"; + $s .= " ${minLonStr}\n"; + $s .= " ${maxLonStr}\n"; + $s .= " \n"; + $s .= " \n"; + } +if ($inclKMLBoundingBoxes) { + $s .= " \n"; +} + + $s .= " \n"; + } + + if (int(@bldglist)) { + + my $imageDir = "${kmlTmp}/${sd}/BLD3"; + if (!(-e "${imageDir}")) { + mkpath("${imageDir}", 0, 0755); + if (!(-e "${imageDir}")) { + die "ERROR: Unable to create directory: ${imageDir} : $!\n"; + } + } else { + die "ERROR: directory: ${imageDir} already exists : $!\n"; + } + + $s .= " \n"; + $s .= " 3D Buildings\n"; + $s .= kmlLookAt($bldgRegion); + $s .= " 1\n"; + +if ($inclKMLBoundingBoxes) { + $s .= " \n"; + $s .= " Rect Bounding Box\n"; + $s .= " 1\n"; + my $bldgIdx; + for($bldgIdx=0; $bldgIdx{"region_${bldgIdx}"}->{minLon}); + my $maxLonStr = sprintf("%.10f", $bldgData->{"region_${bldgIdx}"}->{maxLon}); + my $minLatStr = sprintf("%.10f", $bldgData->{"region_${bldgIdx}"}->{minLat}); + my $maxLatStr = sprintf("%.10f", $bldgData->{"region_${bldgIdx}"}->{maxLat}); + + $s .= " \n"; + $s .= " $bldglist[$bldgIdx]\n"; + $s .= " 1\n"; + $s .= " #transBluePoly\n"; + $s .= " \n"; + $s .= " clampToGround\n"; + $s .= " \n"; + $s .= " \n"; + $s .= " \n"; + $s .= " $maxLonStr,$maxLatStr\n"; + $s .= " $maxLonStr,$minLatStr\n"; + $s .= " $minLonStr,$minLatStr\n"; + $s .= " $minLonStr,$maxLatStr\n"; + $s .= " \n"; + $s .= " \n"; + $s .= " \n"; + $s .= " \n"; + $s .= " \n"; + } + $s .= " \n"; +} + +if ($inclKMLBoundingBoxes) { + $s .= " \n"; + $s .= " Data Coverage\n"; + $s .= " 1\n"; +} + for($bldgIdx=0; $bldgIdx{"region_${bldgIdx}"}->{minLon}); + my $maxLonStr = sprintf("%.10f", $bldgData->{"region_${bldgIdx}"}->{maxLon}); + my $minLatStr = sprintf("%.10f", $bldgData->{"region_${bldgIdx}"}->{minLat}); + my $maxLatStr = sprintf("%.10f", $bldgData->{"region_${bldgIdx}"}->{maxLat}); + + my $imageBldgFile = $bldglist[$bldgIdx]; + $imageBldgFile =~ s/\.shp$/.png/; + + my $cmd = "cp ${bldgDir}/${imageBldgFile} $imageDir"; + print LOGFILE execute_command($cmd, $interactive); + + $s .= " \n"; + $s .= " $bldglist[$bldgIdx]\n"; + + $s .= kmlLookAt($bldgData->{"region_${bldgIdx}"}); + + $s .= " 1\n"; + $s .= " 80ffffff\n"; + $s .= " \n"; + $s .= " ${sd}/BLD3/${imageBldgFile}\n"; + $s .= " \n"; + $s .= " \n"; + $s .= " ${maxLatStr}\n"; + $s .= " ${minLatStr}\n"; + $s .= " ${minLonStr}\n"; + $s .= " ${maxLonStr}\n"; + $s .= " \n"; + $s .= " \n"; + } +if ($inclKMLBoundingBoxes) { + $s .= " \n"; +} + + $s .= " \n"; + } + + if (int(@bldg2list)) { + + my $imageDir = "${kmlTmp}/${sd}/BLD2"; + if (!(-e "${imageDir}")) { + mkpath("${imageDir}", 0, 0755); + if (!(-e "${imageDir}")) { + die "ERROR: Unable to create directory: ${imageDir} : $!\n"; + } + } else { + die "ERROR: directory: ${imageDir} already exists : $!\n"; + } + + $s .= " \n"; + $s .= " 2D Buildings\n"; + $s .= kmlLookAt($bldg2Region); + $s .= " 1\n"; +if ($inclKMLBoundingBoxes) { + $s .= " \n"; + $s .= " Rect Bounding Box\n"; + $s .= " 1\n"; + my $bldg2Idx; + for($bldg2Idx=0; $bldg2Idx{"region_${bldg2Idx}"}->{minLon}); + my $maxLonStr = sprintf("%.10f", $bldg2Data->{"region_${bldg2Idx}"}->{maxLon}); + my $minLatStr = sprintf("%.10f", $bldg2Data->{"region_${bldg2Idx}"}->{minLat}); + my $maxLatStr = sprintf("%.10f", $bldg2Data->{"region_${bldg2Idx}"}->{maxLat}); + + $s .= " \n"; + $s .= " $bldg2list[$bldg2Idx]\n"; + $s .= " 1\n"; + $s .= " #transBluePoly\n"; + $s .= " \n"; + $s .= " clampToGround\n"; + $s .= " \n"; + $s .= " \n"; + $s .= " \n"; + $s .= " $maxLonStr,$maxLatStr\n"; + $s .= " $maxLonStr,$minLatStr\n"; + $s .= " $minLonStr,$minLatStr\n"; + $s .= " $minLonStr,$maxLatStr\n"; + $s .= " \n"; + $s .= " \n"; + $s .= " \n"; + $s .= " \n"; + $s .= " \n"; + } + $s .= " \n"; +} + +if ($inclKMLBoundingBoxes) { + $s .= " \n"; + $s .= " Data Coverage\n"; + $s .= " 1\n"; +} + for($bldg2Idx=0; $bldg2Idx{"region_${bldg2Idx}"}->{minLon}); + my $maxLonStr = sprintf("%.10f", $bldg2Data->{"region_${bldg2Idx}"}->{maxLon}); + my $minLatStr = sprintf("%.10f", $bldg2Data->{"region_${bldg2Idx}"}->{minLat}); + my $maxLatStr = sprintf("%.10f", $bldg2Data->{"region_${bldg2Idx}"}->{maxLat}); + + my $imageBldgFile = $bldg2list[$bldg2Idx]; + $imageBldgFile =~ s/\.shp$/.png/; + + my $cmd = "cp ${bldg2Dir}/${imageBldgFile} $imageDir"; + print LOGFILE execute_command($cmd, $interactive); + + $s .= " \n"; + $s .= " $bldg2list[$bldg2Idx]\n"; + + $s .= kmlLookAt($bldg2Data->{"region_${bldg2Idx}"}); + + $s .= " 1\n"; + $s .= " 80ffffff\n"; + $s .= " \n"; + $s .= " ${sd}/BLD2/${imageBldgFile}\n"; + $s .= " \n"; + $s .= " \n"; + $s .= " ${maxLatStr}\n"; + $s .= " ${minLatStr}\n"; + $s .= " ${minLonStr}\n"; + $s .= " ${maxLonStr}\n"; + $s .= " \n"; + $s .= " \n"; + } +if ($inclKMLBoundingBoxes) { + $s .= " \n"; +} + + $s .= " \n"; + } + + $kml .= $s; + } + + if ($processLidarFlag) { + ######################################################################### + # Match single BE files and BLDG files by looking at how coverage areas overlap + ######################################################################### + my @beIdxList = (0..int(@belist)-1); + my @bldgIdxList = (0..int(@bldglist)+int(@bldg2list)-1); + my @matchList = (); + my $i = 0; + my $k = 0; + my $bldgIdx; + my $bldg2Idx; + + while($i < int(@beIdxList)) { + $beIdx = $beIdxList[$i]; + my $foundMatch = 0; + for($k=0; $k{"region_${beIdx}"}, $bldgData->{"region_${bldgIdx}"}, \$mstr); + print LOGFILE $mstr; + } else { + my $mstr = ""; + $bldg2Idx = $bldgIdx - int(@bldglist); + $foundMatch = isMatch($beData->{"region_${beIdx}"}, $bldg2Data->{"region_${bldg2Idx}"}, \$mstr); + print LOGFILE $mstr; + } + if ($foundMatch) { + push @matchList, "$beIdx:$bldgIdx"; + splice @beIdxList, $i, 1; + splice @bldgIdxList, $k, 1; + } + } + if (!$foundMatch) { + $i++; + } + } + +# my $mIdx; +# print LOGFILE "Matches Found: " . int(@matchList) . "\n"; +# for($mIdx=0; $mIdx{"region_${beIdx}"}, $bldgData->{"region_${bldgIdx}"}); + } else { + $bldg2Idx = $bldgIdx - int(@bldglist); + $overlap = regionOverlap($beData->{"region_${beIdx}"}, $bldg2Data->{"region_${bldg2Idx}"}); + } + if ($overlap) { + push @matchBLDG, $k; + } + } + } + $nBE++; + } + if ($nBLDG < @matchBLDG) { + my $k2 = $matchBLDG[$nBLDG]; + $bldgIdx = $bldgIdxList[$k2]; + for(my $i2=0; $i2<@beIdxList; $i2++) { + my $contains = 0; + for(my $ppi=0; ($ppi{"region_${beIdx}"}, $bldgData->{"region_${bldgIdx}"}); + } else { + $bldg2Idx = $bldgIdx - int(@bldglist); + $overlap = regionOverlap($beData->{"region_${beIdx}"}, $bldg2Data->{"region_${bldg2Idx}"}); + } + if ($overlap) { + push @matchBE, $i2; + } + } + } + $nBLDG++; + } + } + if ($nBLDG) { + my @matchBEIdxList = map { $beIdxList[$_] } @matchBE; + my @matchBLDGIdxList = map { $bldgIdxList[$_] } @matchBLDG; + my $matchBEStr = join(",", @matchBEIdxList); + my $matchBLDGStr = join(",", @matchBLDGIdxList); + my $matchStr = "${matchBEStr}:${matchBLDGStr}"; + push @matchList, $matchStr; + my @sortMatchBE = sort { $a <=> $b } @matchBE; + for(my $i2=int(@sortMatchBE)-1; $i2>=0; $i2--) { + splice @beIdxList, $sortMatchBE[$i2], 1; + } + my @sortMatchBLDG = sort { $a <=> $b } @matchBLDG; + for(my $i2=int(@sortMatchBLDG)-1; $i2>=0; $i2--) { + splice @bldgIdxList, $sortMatchBLDG[$i2], 1; + } + } else { + $i++; + } + } + } + ######################################################################### + + print LOGFILE "\n"; + + ######################################################################### + # Print matched and unmatched files # + ######################################################################### + print LOGFILE "Subdirectory: ${sd}\n"; + my $mIdx; + print LOGFILE "Matches Found: " . int(@matchList) . "\n"; + for($mIdx=0; $mIdx{"region_${bldgIdx}"}->{aName}; + my $aName = $heightFieldName; + if ($aName eq $heightFieldName) { + $fileList .= " " . $bldgDir . "/" . $bldglist[$bldgIdx]; + } else { + my $fileA = $bldgDir . "/" . $bldglist[$bldgIdx]; + my $fileB = $tmpDir . "/r_" . $bldglist[$bldgIdx]; + my $layerName = substr $bldglist[$bldgIdx], 0, -4; + + $cmd = "ogr2ogr $fileB $fileA -sql \"SELECT $aName A $heightFieldName from \\\"$layerName\\\"\""; + print LOGFILE execute_command($cmd, $interactive); + push @delList, $fileB; + + $fileList .= " " . $fileB; + } + } else { + $bldg2Idx = $bldgIdx - int(@bldglist); + # my $aName = $bldg2Data->{"region_${bldg2Idx}"}->{aName}; + my $aName = $heightFieldName; + if ($aName eq $heightFieldName) { + $fileList .= " " . $bldg2Dir . "/" . $bldg2list[$bldg2Idx]; + } else { + my $fileA = $bldg2Dir . "/" . $bldg2list[$bldg2Idx]; + my $fileB = $tmpDir . "/r_" . $bldg2list[$bldg2Idx]; + my $layerName = substr $bldg2list[$bldg2Idx], 0, -4; + + $cmd = "ogr2ogr $fileB $fileA -sql \"SELECT $aName AS $heightFieldName from \\\"$layerName\\\"\""; + print LOGFILE execute_command($cmd, $interactive); + push @delList, $fileB; + + $fileList .= " " . $fileB; + } + } + } + + $cmd = "ogrmerge.py -single -o ${tmpDir}/${cityNameLC}_3d_buildings_${mIdx}.vrt $fileList"; + print LOGFILE execute_command($cmd, $interactive); + push @delList, "${tmpDir}/${cityNameLC}_3d_buildings_${mIdx}.vrt"; + + $cmd = "ogr2ogr -f SQLite -t_srs 'WGS84' ${tmpDir}/${cityNameLC}_3d_buildings_${mIdx}.sqlite3 ${tmpDir}/${cityNameLC}_3d_buildings_${mIdx}.vrt -dsco SPATIALITE=YES"; + print LOGFILE execute_command($cmd, $interactive); + push @delList, "${tmpDir}/${cityNameLC}_3d_buildings_${mIdx}.sqlite3"; + + $cmd = "gdal_rasterize -sql \"SELECT * FROM merged ORDER BY $heightFieldName \" " + . "-a $heightFieldName " + . "-of GTiff " + . "-co \"TILED=YES\" " + . "-ot Float32 " + . "-a_nodata $nodataVal " + . "-tr $lonlatRes $lonlatRes " + . "-a_srs WGS84 " + . "-te $llLon $llLat $urLon $urLat " + . "${tmpDir}/${cityNameLC}_3d_buildings_${mIdx}.sqlite3 " + . "${tmpDir}/${cityNameLC}_3d_buildings_${mIdx}.tif"; + print LOGFILE execute_command($cmd, $interactive); + push @delList, "${tmpDir}/${cityNameLC}_3d_buildings_${mIdx}.tif"; + + $cmd = "gdalbuildvrt -separate ${tmpDir}/${cityNameLC}_${mIdx}.vrt ${tmpDir}/${cityNameLC}_bare_earth_${mIdx}.tif ${tmpDir}/${cityNameLC}_3d_buildings_${mIdx}.tif"; + print LOGFILE execute_command($cmd, $interactive); + push @delList, "${tmpDir}/${cityNameLC}_${mIdx}.vrt"; + + if (!(-e "${destDir}/${sd}")) { + mkpath("${destDir}/${sd}", 0, 0755); + if (!(-e "$destDir/${sd}")) { + die "ERROR: Unable to create directory: ${destDir}/${sd} : $!\n"; + } + } + + my $nx = (($xsize+$gpMaxPixel-1) - (($xsize+$gpMaxPixel-1)%$gpMaxPixel))/$gpMaxPixel; + my $ny = (($ysize+$gpMaxPixel-1) - (($ysize+$gpMaxPixel-1)%$gpMaxPixel))/$gpMaxPixel; + my $tileIdx = 0; + my $ix; + my $iy; + my $oy = 0; + for($iy=0; $iy<$ny; $iy++) { + my $dy = (($ysize + $iy) - (($ysize + $iy) % $ny))/$ny; + my $ox = 0; + for($ix=0; $ix<$nx; $ix++) { + $tileIdx++; + my $tstr = sprintf("%.2d", $tileIdx); + + my $dx = (($xsize + $ix) - (($xsize + $ix) % $nx))/$nx; + + $cmd = "gdal_translate -srcwin $ox $oy $dx $dy -co \"TILED=YES\" ${tmpDir}/${cityNameLC}_${mIdx}.vrt ${destDir}/${sd}/${cityNameLC}_${mIdx}_${tstr}.tif"; + print LOGFILE execute_command($cmd, $interactive); + + $ox += $dx; + + my $tifInfo = `gdalinfo -norat -nomd -noct ${destDir}/${sd}/${cityNameLC}_${mIdx}_${tstr}.tif`; + + my $llLon; + my $llLat; + my $urLon; + my $urLat; + if ($tifInfo =~ /.*Lower Left\s*\(\s*([\d.-]*)\s*,\s*([\d.-]*)\s*\).*\nUpper Right\s*\(\s*([\d.-]*)\s*,\s*([\d.-]*)\s*\)/m) { + $llLon = $1; + $llLat = $2; + $urLon = $3; + $urLat = $4; + } else { + die "ERROR: Unable to extract coverage area from tif file: ${destDir}/${sd}/${cityNameLC}_${mIdx}_${tstr}.tif : $!\n"; + } + + print INFOFILE "${relSD}${cityNameLC}_${mIdx}_${tstr}.tif,$llLon,$urLon,$llLat,$urLat\n"; + } + $oy += $dy; + } + + if ($cleantmp) { + my $delFile; + foreach $delFile (@delList) { + unlink("$delFile") or die "ERROR: Unable to delete file \"$delFile\" : $!\n"; + } + } + } + ######################################################################### + } + } + + if ($cleantmp) { + my $fixedTmpDir = "${tmpDir}/fixed/${cityName}"; + if (-e $fixedTmpDir) { + print LOGFILE "Removing Directory \"$fixedTmpDir\" ... \n"; + rmtree(${fixedTmpDir}); + } + } + + if ($srcKMLFlag) { + my $i; + for($i=(int(@currPath)-1); $i>=0; $i--) { + if ($i == 0) { + $kml .= kmlLookAt($cityRegion); + } + $kml .= " \n"; + pop @currPath; + } + } + + if ($processLidarFlag) { + close INFOFILE; + } + + my $t1 = time; + print "Completed: $cityName, ELAPSED TIME = " . ($t1-$t0) . " sec\n"; + + print LOGFILE "Done Pocessing City: $cityName, ELAPSED TIME = " . ($t1-$t0) . " sec\n"; + close LOGFILE; + + $kmlhash{$cityStr} = $kml; + $discardhash{$cityStr} = $discard; + + my $log; + open(INFILE, "$logFile") || die "Can't open file $logFile : $!\n"; + binmode INFILE; + + my ($savedreadstate) = $/; + undef $/; + $log=; + $/ = $savedreadstate; + close INFILE; + + $loghash{$cityStr} = $log; + + return; +} +################################################################################# + +################################################################################# +# Routine fixRaster +################################################################################# +sub fixRaster +{ + my $inFile = $_[0]; + my $outFile = $_[1]; + my $imageFile = $_[2]; + my $tmpImageFile = $_[3]; + my $p = $_[4]; + my $cityName = $_[5]; + + my $templFile = "/tmp/templ_proc_gdal_${cityName}_$$.txt"; + + my $s = ""; + my $paramIdx = 0; + + $s .= "PARAM_${paramIdx}: FUNCTION \"fixRaster\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: SRC_FILE_RASTER \"$inFile\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: OUTPUT_FILE \"$outFile\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: NODATA_VAL $nodataVal\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: CLAMP_MIN $p->{clampMin}\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: CLAMP_MAX $p->{clampMax}\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: MIN_MAG $p->{minMag}\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: IMAGE_FILE \"$imageFile\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: TMP_IMAGE_FILE \"$tmpImageFile\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: IMAGE_LON_LAT_RES $imageLonLatRes\n"; + $paramIdx++; + + my $numParam = $paramIdx; + + open(FILE, ">$templFile") || die "Can't open file $templFile : $!\n"; + print FILE "# proc_gdal TEMPLATE FILE\n"; + print FILE "FORMAT: 1_0\n"; + print FILE "\n"; + print FILE "NUM_PARAM: ${numParam}\n"; + print FILE "\n"; + print FILE "$s"; + close FILE; + + my $resp = `./proc_gdal -templ $templFile 2>&1`; + + return($resp); +} +################################################################################# + +################################################################################# +# Routine fixVector +################################################################################# +sub fixVector +{ + my $inFile = $_[0]; + my $inHeightFieldName = $_[1]; + my $outFile = $_[2]; + my $imageFile = $_[3]; + my $tmpImageFile = $_[4]; + my $cityName = $_[5]; + + my $templFile = "/tmp/templ_proc_gdal_${cityName}_$$.txt"; + + my $s = ""; + my $paramIdx = 0; + + $s .= "PARAM_${paramIdx}: FUNCTION \"fixVector\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: SRC_FILE_VECTOR \"$inFile\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: SRC_HEIGHT_FIELD_NAME \"$inHeightFieldName\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: OUTPUT_FILE \"$outFile\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: OUTPUT_HEIGHT_FIELD_NAME \"$heightFieldName\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: IMAGE_FILE \"$imageFile\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: TMP_IMAGE_FILE \"$tmpImageFile\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: IMAGE_LON_LAT_RES $imageLonLatRes\n"; + $paramIdx++; + + my $numParam = $paramIdx; + + open(FILE, ">$templFile") || die "Can't open file $templFile : $!\n"; + print FILE "# proc_gdal TEMPLATE FILE\n"; + print FILE "FORMAT: 1_0\n"; + print FILE "\n"; + print FILE "NUM_PARAM: ${numParam}\n"; + print FILE "\n"; + print FILE "$s"; + close FILE; + + my $resp = `./proc_gdal -templ $templFile 2>&1`; + + return($resp); +} +################################################################################# + +################################################################################# +# Routine vectorCvg +################################################################################# +sub vectorCvg +{ + my $inFile = $_[0]; + my $imageFile = $_[1]; + my $tmpImageFile = $_[2]; + + my $templFile = "/tmp/templ_proc_gdal_$$.txt"; + + my $s = ""; + my $paramIdx = 0; + + $s .= "PARAM_${paramIdx}: FUNCTION \"vectorCvg\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: SRC_FILE_VECTOR \"$inFile\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: IMAGE_FILE \"$imageFile\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: TMP_IMAGE_FILE \"$tmpImageFile\"\n"; + $paramIdx++; + + $s .= "PARAM_${paramIdx}: IMAGE_LON_LAT_RES $imageLonLatRes\n"; + $paramIdx++; + + my $numParam = $paramIdx; + + open(FILE, ">$templFile") || die "Can't open file $templFile : $!\n"; + print FILE "# proc_gdal TEMPLATE FILE\n"; + print FILE "FORMAT: 1_0\n"; + print FILE "\n"; + print FILE "NUM_PARAM: ${numParam}\n"; + print FILE "\n"; + print FILE "$s"; + close FILE; + + my $resp = `./proc_gdal -templ $templFile`; + print $resp; + + return; +} +################################################################################# + +################################################################################# +# Routine isMatch +################################################################################# +sub isMatch +{ + my $regionA = $_[0]; + my $regionB = $_[1]; + my $logref = $_[2]; + my $match; + + if (($regionA->{maxLon} <= $regionB->{minLon}) || ($regionA->{minLon} >= $regionB->{maxLon})) { + $match = 0; + } elsif (($regionA->{maxLat} <= $regionB->{minLat}) || ($regionA->{minLat} >= $regionB->{maxLat})) { + $match = 0; + } else { + my $overlapMinLon = List::Util::max($regionA->{minLon}, $regionB->{minLon}); + my $overlapMaxLon = List::Util::min($regionA->{maxLon}, $regionB->{maxLon}); + my $overlapMinLat = List::Util::max($regionA->{minLat}, $regionB->{minLat}); + my $overlapMaxLat = List::Util::min($regionA->{maxLat}, $regionB->{maxLat}); + my $areaA = ($regionA->{maxLon} - $regionA->{minLon})*($regionA->{maxLat} - $regionA->{minLat}); + my $areaB = ($regionB->{maxLon} - $regionB->{minLon})*($regionB->{maxLat} - $regionB->{minLat}); + my $areaOverlap = ($overlapMaxLon - $overlapMinLon)*($overlapMaxLat - $overlapMinLat); + my $metric = $areaOverlap / ($areaA + $areaB - $areaOverlap); + + ${$logref} .= "METRIC = $metric\n"; + + $match = ($metric >= 0.80 ? 1 : 0); + } + + + return $match; +} +################################################################################# + +################################################################################# +# Routine regionOverlap +################################################################################# +sub regionOverlap +{ + my $regionA = $_[0]; + my $regionB = $_[1]; + my $overlap; + + if (($regionA->{maxLon} <= $regionB->{minLon}) || ($regionA->{minLon} >= $regionB->{maxLon})) { + $overlap = 0; + } elsif (($regionA->{maxLat} <= $regionB->{minLat}) || ($regionA->{minLat} >= $regionB->{maxLat})) { + $overlap = 0; + } else { + $overlap = 1; + } + + + return $overlap; +} +################################################################################# + +################################################################################# +# Routine regionUnion +################################################################################# +sub regionUnion +{ + my $regionA = $_[0]; + my $regionB = $_[1]; + my $unionRegion = {}; + + if (exists($regionA->{minLon}) && exists($regionB->{minLon})) { + $unionRegion->{minLon} = ($regionA->{minLon} < $regionB->{minLon} ? $regionA->{minLon} : $regionB->{minLon}); + $unionRegion->{maxLon} = ($regionA->{maxLon} > $regionB->{maxLon} ? $regionA->{maxLon} : $regionB->{maxLon}); + $unionRegion->{minLat} = ($regionA->{minLat} < $regionB->{minLat} ? $regionA->{minLat} : $regionB->{minLat}); + $unionRegion->{maxLat} = ($regionA->{maxLat} > $regionB->{maxLat} ? $regionA->{maxLat} : $regionB->{maxLat}); + } elsif (exists($regionB->{minLon})) { + $unionRegion->{minLon} = $regionB->{minLon}; + $unionRegion->{maxLon} = $regionB->{maxLon}; + $unionRegion->{minLat} = $regionB->{minLat}; + $unionRegion->{maxLat} = $regionB->{maxLat}; + } elsif (exists($regionA->{minLon})) { + $unionRegion->{minLon} = $regionA->{minLon}; + $unionRegion->{maxLon} = $regionA->{maxLon}; + $unionRegion->{minLat} = $regionA->{minLat}; + $unionRegion->{maxLat} = $regionA->{maxLat}; + } + + return $unionRegion; +} +################################################################################# + + +################################################################################# +# Routine kmlLookAt +################################################################################# +sub kmlLookAt +{ + my $region = $_[0]; + my $s = ""; + + my $centerLon = ($region->{minLon} + $region->{maxLon})/2; + my $centerLat = ($region->{minLat} + $region->{maxLat})/2; + + my $centerLonStr = sprintf("%.10f", $centerLon); + my $centerLatStr = sprintf("%.10f", $centerLat); + + my $deltaLonRad = ($region->{maxLon}-$region->{minLon})*$pi/180.0; + my $deltaLatRad = ($region->{maxLat}-$region->{minLat})*$pi/180.0; + + my $vDist = $deltaLatRad*$earthRadius; + my $hDist = $deltaLonRad*$earthRadius*cos($centerLat*$pi/180.0); + my $range = 2*($hDist > $vDist ? $hDist : $vDist); + + $s .= " \n"; + $s .= " ${centerLonStr}\n"; + $s .= " ${centerLatStr}\n"; + $s .= " 0\n"; + $s .= " 20.0\n"; + $s .= " $range\n"; + $s .= " relativeToGround\n"; + $s .= " \n"; + + return $s; +} +################################################################################# + +################################################################################# +# Routine extractLonLat +################################################################################# +sub extractLonLat +{ + my $str = $_[0]; + my $lonVal; + my $latVal; + + if ($str =~ /^\s*(\d+)d\s*(\d+)\'\s*([\d.]+)\"(E|W),\s*(\d+)d\s*(\d+)\'\s*([\d.]+)\"(N|S)$/) { + $lonVal = ($1 + ($2 + $3/60)/60)*($4 eq "W" ? -1 : 1); + $latVal = ($5 + ($6 + $7/60)/60)*($8 eq "S" ? -1 : 1); + } else { + die "ERROR: Unable to extract LON/LAT values from \"$str\" : $!\n"; + } + + return($lonVal, $latVal); +} +################################################################################# + +################################################################################# +################################################################################# +################################################################################# + +################################################################################# +# Routine execute_command: +################################################################################# +sub execute_command +{ + my $cmd = $_[0]; + my $interactive = $_[1]; + my $retval = ""; + my $resp; + + my $exec_flag = 1; + + if ($interactive) { + print "Are you sure you want to execute command: $cmd (y/n)? "; + chop($resp = ); + if ($resp ne "y") { + $exec_flag = 0; + } + } + if ($exec_flag) { + $retval .= "$cmd\n"; + $retval .= `$cmd 2>&1`; + if ($? == -1) { + die "failed to execute: $!\n"; + } elsif ($? & 127) { + my $errmsg = sprintf("child died with signal %d, %s coredump\n", ($? & 127), ($? & 128) ? 'with' : 'without'); + die "$errmsg : $!\n"; + } + } + + return $retval; +} +################################################################################# + +################################################################################# +# Routine genKML +################################################################################# +sub genKML +{ + my $codePart = $_[0]; + my $s = ""; + + if ($codePart eq "HEAD") { + $s .= "\n"; + $s .= "\n"; + $s .= "\n"; + $s .= "\n"; + $s .= "\n"; + $s .= " Source Lidar Data\n"; + $s .= " 1\n"; + $s .= " Display Regions Covered by Source Lidar Data\n"; + $s .= "\n"; + $s .= " \n"; + $s .= "\n"; + $s .= " \n"; + } elsif ($codePart eq "TAIL") { + $s .= "\n"; + $s .= "\n"; + } else { + die "ERROR: uncrecognized KML code part: \"$codePart\" : $!\n"; + } + + return $s; +} +################################################################################# + diff --git a/tools/geo_converters/proc_gdal/data_set.cpp b/tools/geo_converters/proc_gdal/data_set.cpp new file mode 100644 index 0000000..642a5a9 --- /dev/null +++ b/tools/geo_converters/proc_gdal/data_set.cpp @@ -0,0 +1,1138 @@ +/******************************************************************************************/ +/**** FILE : data_set.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "global_defines.h" +#include "GdalDataModel.h" +#include "data_set.h" + +/******************************************************************************************/ +/**** FUNCTION: DataSetClass::DataSetClass() ****/ +/******************************************************************************************/ +DataSetClass::DataSetClass() +{ +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: DataSetClass::~DataSetClass() ****/ +/******************************************************************************************/ +DataSetClass::~DataSetClass() +{ +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: DataSetClass::run ****/ +/******************************************************************************************/ +void DataSetClass::run() +{ + if (parameterTemplate.function == "combine3D2D") { + combine3D2D(); + } else if (parameterTemplate.function == "fixRaster") { + fixRaster(); + } else if (parameterTemplate.function == "fixVector") { + fixVector(); + } else if (parameterTemplate.function == "vectorCvg") { + vectorCvg(); + } else if (parameterTemplate.function == "mbRasterCvg") { + mbRasterCvg(); + } else { + std::ostringstream errStr; + errStr << "ERROR: function set to unrecognized value: " << parameterTemplate.function; + throw std::runtime_error(errStr.str()); + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: DataSetClass::combine3D2D ****/ +/******************************************************************************************/ +void DataSetClass::combine3D2D() +{ + GDALAllRegister(); + + GdalDataModel *gdalDataModel3D = new GdalDataModel(parameterTemplate.srcFile3D, parameterTemplate.heightFieldName3D); + // gdalDataModel3D->printDebugInfo(); + + GdalDataModel *gdalDataModel2D = new GdalDataModel(parameterTemplate.srcFile2D, parameterTemplate.heightFieldName2D); + // gdalDataModel2D->printDebugInfo(); + + + /**************************************************************************************/ + /**** Create Driver ****/ + /**************************************************************************************/ + const char *pszDriverName = "ESRI Shapefile"; + GDALDriver *poDriver; + + poDriver = GetGDALDriverManager()->GetDriverByName(pszDriverName ); + if( poDriver == NULL ) + { + printf( "%s driver not available.\n", pszDriverName ); + exit( 1 ); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Create Output Dataset (specify output file) ****/ + /**************************************************************************************/ + GDALDataset *outputDS; + + outputDS = poDriver->Create( parameterTemplate.outputFile.c_str(), 0, 0, 0, GDT_Unknown, NULL ); + if( outputDS == NULL ) + { + printf( "Creation of output file failed.\n" ); + exit( 1 ); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Create Layer ****/ + /**************************************************************************************/ + OGRLayer *poLayer; + + poLayer = outputDS->CreateLayer( parameterTemplate.outputLayer.c_str(), NULL, wkbPolygon, NULL ); + if( poLayer == NULL ) + { + printf( "Layer creation failed.\n" ); + exit( 1 ); + } + /**************************************************************************************/ + +#if 0 + /**************************************************************************************/ + /**** Create FIELD: OGR_FID ****/ + /**************************************************************************************/ + OGRFieldDefn oField1( "OGR_FID", OFTInteger ); + + oField1.SetWidth(9); + + if( poLayer->CreateField( &oField1 ) != OGRERR_NONE ) + { + printf( "Creating OGR_FID field failed.\n" ); + exit( 1 ); + } + /**************************************************************************************/ +#endif + + /**************************************************************************************/ + /**** Create FIELD: Height ****/ + /**************************************************************************************/ + OGRFieldDefn heightField( parameterTemplate.outputHeightFieldName.c_str(), OFTReal ); + + // 24.15 is ridiculous, change to 12.2 + heightField.SetWidth(12); + heightField.SetPrecision(2); + + if( poLayer->CreateField( &heightField ) != OGRERR_NONE ) + { + std::cout << "Creating " << parameterTemplate.outputHeightFieldName << " field failed.\n"; + exit( 1 ); + } + int heightFIeldIdx = poLayer->FindFieldIndex(parameterTemplate.outputHeightFieldName.c_str(), TRUE); + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Add all polygons from 3D layer ****/ + /**************************************************************************************/ + std::unique_ptr ptrOFeature; + OGRLayer *layer3D = gdalDataModel3D->getLayer(); + layer3D->ResetReading(); + while ((ptrOFeature = std::unique_ptr(layer3D->GetNextFeature())) != NULL) { + // GDAL maintains ownership of returned pointer, so we should not manage its memory + OGRGeometry *ptrOGeometry = ptrOFeature->GetGeometryRef(); + + if (ptrOGeometry != NULL && ptrOGeometry->getGeometryType() == OGRwkbGeometryType::wkbPolygon) { + double height = ptrOFeature->GetFieldAsDouble(gdalDataModel3D->heightFieldIdx); + + OGRFeature *poFeature; + + poFeature = OGRFeature::CreateFeature( poLayer->GetLayerDefn() ); + poFeature->SetField(heightFIeldIdx, height); + + OGRPolygon *poly = (OGRPolygon *) ptrOGeometry; + + poFeature->SetGeometry( poly ); + + if( poLayer->CreateFeature( poFeature ) != OGRERR_NONE ) + { + printf( "Failed to create feature in shapefile.\n" ); + exit( 1 ); + } + + OGRFeature::DestroyFeature( poFeature ); + } + } + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Add all polygons from 2D layer that dont overlay anything in 3D layer ****/ + /**************************************************************************************/ + int num2DPolygonUsed = 0; + int num2DPolygonDiscarded = 0; + + std::unique_ptr ptrOFeature2D; + OGRLayer *layer2D = gdalDataModel2D->getLayer(); + layer2D->ResetReading(); + while ((ptrOFeature2D = std::unique_ptr(layer2D->GetNextFeature())) != NULL) { + // GDAL maintains ownership of returned pointer, so we should not manage its memory + OGRGeometry *ptrOGeometry = ptrOFeature2D->GetGeometryRef(); + + if (ptrOGeometry != NULL && ptrOGeometry->getGeometryType() == OGRwkbGeometryType::wkbPolygon) { + + OGRPolygon *poly = (OGRPolygon *) ptrOGeometry; + + layer3D->SetSpatialFilter(poly); + + layer3D->ResetReading(); + + if ((ptrOFeature = std::unique_ptr(layer3D->GetNextFeature())) == NULL) { + + double height = ptrOFeature2D->GetFieldAsDouble(gdalDataModel2D->heightFieldIdx); + + OGRFeature *poFeature; + + poFeature = OGRFeature::CreateFeature( poLayer->GetLayerDefn() ); + poFeature->SetField(heightFIeldIdx, height); + + poFeature->SetGeometry( poly ); + + if( poLayer->CreateFeature( poFeature ) != OGRERR_NONE ) + { + printf( "Failed to create feature in shapefile.\n" ); + exit( 1 ); + } + + OGRFeature::DestroyFeature( poFeature ); + + num2DPolygonUsed++; + } else { + num2DPolygonDiscarded++; + } + } + } + /**************************************************************************************/ + + GDALClose( outputDS ); + + std::cout << "NUM 2D POLYGONS USED: " << num2DPolygonUsed << std::endl; + std::cout << "NUM 2D POLYGONS DISCARDED: " << num2DPolygonDiscarded << std::endl; + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: DataSetClass::fixRaster ****/ +/******************************************************************************************/ +void DataSetClass::fixRaster() +{ + std::ostringstream errStr; + GDALAllRegister(); + + const char *pszFormat = "GTiff"; + GDALDriver *poDriver; + char **papszMetadata; + poDriver = GetGDALDriverManager()->GetDriverByName(pszFormat); + if( !poDriver ) { + exit( 1 ); + } + + bool genImageFlag = (!parameterTemplate.imageFile.empty()); + std::string tmpImageFile = (parameterTemplate.tmpImageFile.empty() ? "/tmp/image.ppm" : parameterTemplate.tmpImageFile); + + papszMetadata = poDriver->GetMetadata(); + if( CSLFetchBoolean( papszMetadata, GDAL_DCAP_CREATE, FALSE ) ) + printf( "Driver %s supports Create() method.\n", pszFormat ); + if( CSLFetchBoolean( papszMetadata, GDAL_DCAP_CREATECOPY, FALSE ) ) + printf( "Driver %s supports CreateCopy() method.\n", pszFormat ); + + GDALDataset *srcDS = (GDALDataset *) GDALOpen( parameterTemplate.srcFileRaster.c_str(), GA_ReadOnly ); + + char **options = (char **) NULL; + + options = CSLSetNameValue(options, "TILED", "YES"); + options = CSLSetNameValue(options, "BLOCKXSIZE", "256"); + + GDALDataset *dstDS; + dstDS = poDriver->CreateCopy( parameterTemplate.outputFile.c_str(), srcDS, FALSE, options, GDALTermProgress, NULL ); + GDALClose( (GDALDatasetH) srcDS ); + + CSLDestroy(options); + + int nXSize = srcDS->GetRasterXSize(); + int nYSize = srcDS->GetRasterYSize(); + int numRasterBand = dstDS->GetRasterCount(); + + double adfGeoTransform[6]; + printf( "Driver: %s/%s\n", + dstDS->GetDriver()->GetDescription(), + dstDS->GetDriver()->GetMetadataItem( GDAL_DMD_LONGNAME ) ); + printf( "Size is %dx%dx%d\n", nXSize, nYSize, numRasterBand ); + if( dstDS->GetProjectionRef() != NULL ) { + printf( "Projection is `%s'\n", dstDS->GetProjectionRef() ); + } + const char *pszProjection = nullptr; + if( dstDS->GetGeoTransform( adfGeoTransform ) == CE_None ) { + printf( "Origin = (%.6f,%.6f)\n", adfGeoTransform[0], adfGeoTransform[3] ); + printf( "Pixel Size = (%.6f,%.6f)\n", adfGeoTransform[1], adfGeoTransform[5] ); + pszProjection = GDALGetProjectionRef(dstDS); + } else { + throw std::runtime_error("ERROR: getting GEO Transform"); + } + + double pixelSize = adfGeoTransform[1]; + if ( fabs(pixelSize + adfGeoTransform[5]) > 1.0e-8 ) { + throw std::runtime_error("ERROR: X / Y pixel sizes not properly set"); + } + + OGRCoordinateTransformationH hTransform = nullptr; + + if( pszProjection != nullptr && strlen(pszProjection) > 0 ) + { + OGRSpatialReferenceH hLatLong = nullptr; + + OGRSpatialReferenceH hProj = OSRNewSpatialReference( pszProjection ); + + if (hProj != nullptr) { + OGRErr eErr = OGRERR_NONE; + // Check that it looks like Earth before trying to reproject to wgs84... + hLatLong = OSRCloneGeogCS( hProj ); + if( hLatLong ) { + // Drop GEOGCS|UNIT child to be sure to output as degrees + OGRSpatialReference* poLatLong = reinterpret_cast< + OGRSpatialReference*>(hLatLong); + OGR_SRSNode *poGEOGCS = poLatLong->GetRoot(); + if( poGEOGCS ) + { + const int iUnitChild = + poGEOGCS->FindChild("UNIT"); + if( iUnitChild != -1 ) + poGEOGCS->DestroyChild(iUnitChild); + } + } + } + + if( hLatLong != nullptr ) + { + CPLPushErrorHandler( CPLQuietErrorHandler ); + hTransform = OCTNewCoordinateTransformation( hProj, hLatLong ); + CPLPopErrorHandler(); + + OSRDestroySpatialReference( hLatLong ); + } + + if( hProj != nullptr ) { + OSRDestroySpatialReference( hProj ); + } + } + + if (!hTransform) { + throw std::runtime_error("ERROR: unable to create coordinate transform"); + } + + double ULX = adfGeoTransform[0] + adfGeoTransform[1] * 0 + adfGeoTransform[2] * 0; + double ULY = adfGeoTransform[3] + adfGeoTransform[4] * 0 + adfGeoTransform[5] * 0; + double ULZ = 0.0; + + double LLX = adfGeoTransform[0] + adfGeoTransform[1] * 0 + adfGeoTransform[2] * nYSize; + double LLY = adfGeoTransform[3] + adfGeoTransform[4] * 0 + adfGeoTransform[5] * nYSize; + double LLZ = 0.0; + + double URX = adfGeoTransform[0] + adfGeoTransform[1] * nXSize + adfGeoTransform[2] * 0; + double URY = adfGeoTransform[3] + adfGeoTransform[4] * nXSize + adfGeoTransform[5] * 0; + double URZ = 0.0; + + double LRX = adfGeoTransform[0] + adfGeoTransform[1] * nXSize + adfGeoTransform[2] * nYSize; + double LRY = adfGeoTransform[3] + adfGeoTransform[4] * nXSize + adfGeoTransform[5] * nYSize; + double LRZ = 0.0; + + OCTTransform(hTransform,1,&ULX,&ULY,&ULZ); + OCTTransform(hTransform,1,&LLX,&LLY,&LLZ); + OCTTransform(hTransform,1,&URX,&URY,&URZ); + OCTTransform(hTransform,1,&LRX,&LRY,&LRZ); + + double resLon = (std::min(URX, LRX) - std::max(ULX, LLX))/nXSize; + double resLat = (std::min(ULY, URY) - std::max(LLY, LRY))/nYSize; + double resLonLat = std::min(resLon, resLat); + + if (parameterTemplate.verbose) { + std::cout << "UL LONLAT: " << ULX << " " << ULY << std::endl; + std::cout << "LL LONLAT: " << LLX << " " << LLY << std::endl; + std::cout << "UR LONLAT: " << URX << " " << URY << std::endl; + std::cout << "LR LONLAT: " << LRX << " " << LRY << std::endl; + + std::cout << "RES_LON = " << resLon << std::endl; + std::cout << "RES_LAT = " << resLat << std::endl; + std::cout << "RES_LONLAT = " << resLonLat << std::endl; + } + + std::cout << "NUMBER RASTER BANDS: " << numRasterBand << std::endl; + + if (numRasterBand != 1) { + throw std::runtime_error("ERROR numRasterBand must be 1"); + } + + int nBlockXSize, nBlockYSize; + GDALRasterBand *rasterBand = dstDS->GetRasterBand(1); + char **rasterMetadata = rasterBand->GetMetadata(); + + if (rasterMetadata) { + std::cout << "RASTER METADATA: " << std::endl; + char **chptr = rasterMetadata; + while (*chptr) { + std::cout << " " << *chptr << std::endl; + chptr++; + } + } else { + std::cout << "NO RASTER METADATA: " << std::endl; + } + + rasterBand->GetBlockSize( &nBlockXSize, &nBlockYSize ); + printf( "Block=%dx%d Type=%s, ColorInterp=%s\n", + nBlockXSize, nBlockYSize, + GDALGetDataTypeName(rasterBand->GetRasterDataType()), + GDALGetColorInterpretationName( + rasterBand->GetColorInterpretation()) ); + + int bGotMin, bGotMax; + double adfMinMax[2]; + adfMinMax[0] = rasterBand->GetMinimum( &bGotMin ); + adfMinMax[1] = rasterBand->GetMaximum( &bGotMax ); + if( ! (bGotMin && bGotMax) ) { + std::cout << "calling GDALComputeRasterMinMax()" << std::endl; + GDALComputeRasterMinMax((GDALRasterBandH)rasterBand, TRUE, adfMinMax); + } + + printf( "Min=%.3f\nMax=%.3f\n", adfMinMax[0], adfMinMax[1] ); + if( rasterBand->GetOverviewCount() > 0 ) { + printf( "Band has %d overviews.\n", rasterBand->GetOverviewCount() ); + } + if( rasterBand->GetColorTable() != NULL ) { + printf( "Band has a color table with %d entries.\n", + rasterBand->GetColorTable()->GetColorEntryCount() ); + } + + int hasNoData; + double origNodataValue = rasterBand->GetNoDataValue(&hasNoData); + float origNodataValueFloat = (float) origNodataValue; + if (hasNoData) { + std::cout << "ORIG NODATA: " << origNodataValue << std::endl; + std::cout << "ORIG NODATA (float): " << origNodataValueFloat << std::endl; + } else { + std::cout << "ORIG NODATA undefined" << std::endl; + } + + rasterBand->SetNoDataValue(parameterTemplate.nodataVal); + + int xIdx, yIdx; + if (parameterTemplate.verbose) { + std::cout << "nXSize: " << nXSize << std::endl; + std::cout << "nYSize: " << nYSize << std::endl; + std::cout << "GDALGetDataTypeSizeBytes(GDT_Float32) = " << GDALGetDataTypeSizeBytes(GDT_Float32) << std::endl; + std::cout << "sizeof(GDT_Float32) = " << sizeof(GDT_Float32) << std::endl; + std::cout << "sizeof(GDT_Float64) = " << sizeof(GDT_Float64) << std::endl; + std::cout << "sizeof(float) = " << sizeof(float) << std::endl; + } + float *pafScanline = (float *) CPLMalloc(nXSize*GDALGetDataTypeSizeBytes(GDT_Float32)); + + if (fabs(parameterTemplate.nodataVal) > std::numeric_limits::max()) { + errStr << "ERROR: nodataVal set to illegal value: " << parameterTemplate.nodataVal << ", max value for float is " << std::numeric_limits::max(); + throw std::runtime_error(errStr.str()); + } + + /**************************************************************************************/ + /* Define color scheme */ + /**************************************************************************************/ + std::vector colorList; + colorList.push_back(" 0 0 0"); // 0: NO DATA + colorList.push_back(" 0 255 0"); // 1: VALID DATA + colorList.push_back(" 0 255 255"); // 2: Mix of data/nodata samples + /**************************************************************************************/ + + /**************************************************************************************/ + /* Create PPM File */ + /**************************************************************************************/ + FILE *fppm = (FILE *) NULL; + int N = (int) ceil( (parameterTemplate.imageLonLatRes/resLonLat) - 1.0e-8 ); + if (N < 1) { N = 1; } + int imageXSize = (nXSize-1)/N + 1; + int imageYSize = (nYSize-1)/N + 1; + int *imageScanline = (int *) NULL; + int imgXIdx, imgYIdx; + if (genImageFlag) { + if ( !(fppm = fopen(tmpImageFile.c_str(), "wb")) ) { + throw std::runtime_error("ERROR"); + } + fprintf(fppm, "P3\n"); + fprintf(fppm, "%d %d %d\n", imageXSize, imageYSize, 255); + imageScanline = (int *) malloc(imageXSize*sizeof(int)); + for(imgXIdx=0; imgXIdxRasterIO( GF_Read, 0, yIdx, nXSize, 1, pafScanline, nXSize, 1, GDT_Float32, 0, 0 ); + int colorIdx; + for(xIdx=0; xIdx parameterTemplate.clampMax) { + numClampMax++; + pafScanline[xIdx] = (float) parameterTemplate.nodataVal; + colorIdx = 0; + } else if (fabs(pafScanline[xIdx]) < parameterTemplate.minMag) { + numMinMag++; + pafScanline[xIdx] = (float) parameterTemplate.nodataVal; + colorIdx = 0; + } else { + numValid++; + colorIdx = 1; + } + if (genImageFlag) { + imgXIdx = xIdx / N; + if (imageScanline[imgXIdx] == -1) { + imageScanline[imgXIdx] = colorIdx; + } else if (colorIdx != imageScanline[imgXIdx]) { + imageScanline[imgXIdx] = 2; + } + } + } + if (genImageFlag) { + if ((yIdx % N == N-1) || (yIdx == nYSize-1)) { + for(imgXIdx=0; imgXIdxRasterIO( GF_Write, 0, yIdx, nXSize, 1, pafScanline, nXSize, 1, GDT_Float32, 0, 0 ); + } + + if (genImageFlag) { + fclose(fppm); + } + + std::cout << "Num NODATA values " << numNoData << " (" << 100.0*numNoData / (1.0*nXSize*nYSize) << "%)" << std::endl; + std::cout << "Num values below min clamp " << parameterTemplate.clampMin << ": " << numClampMin << " (" << 100.0*numClampMin / (1.0*nXSize*nYSize) << "%)" << std::endl; + std::cout << "Num values above max clamp " << parameterTemplate.clampMax << ": " << numClampMax << " (" << 100.0*numClampMax / (1.0*nXSize*nYSize) << "%)" << std::endl; + std::cout << "Num values with fabs() below minMag " << parameterTemplate.minMag << ": " << numMinMag << " (" << 100.0*numMinMag / (1.0*nXSize*nYSize) << "%)" << std::endl; + std::cout << "Num VALID values " << numValid << " (" << 100.0*numValid / (1.0*nXSize*nYSize) << "%)" << std::endl; + + int numModified = numClampMin+numClampMax+numMinMag; + std::cout << "Num values modified: " << numModified << " (" << 100.0*numModified / (1.0*nXSize*nYSize) << "%)" << std::endl; + + int numData = numValid; + std::cout << "Num DATA values: " << numData << " (" << 100.0*numData / (1.0*nXSize*nYSize) << "%)" << std::endl; + + CPLFree(pafScanline); + + if (numValid) { + double pdfMin, pdfMax, pdfMean, pdfStdDev; + rasterBand->ComputeStatistics (0, &pdfMin, &pdfMax, &pdfMean, &pdfStdDev, NULL, (void *) NULL); + rasterBand->SetStatistics (pdfMin, pdfMax, pdfMean, pdfStdDev); + } else { + rasterBand->SetStatistics (0.0, 0.0, 0.0, 0.0); + } + + /* Once we're done, close properly the dataset */ + if( dstDS != NULL ) { + GDALClose( (GDALDatasetH) dstDS ); + } + + if (genImageFlag) { + std::string command = "convert " + tmpImageFile + " " + parameterTemplate.imageFile; + std::cout << "COMMAND: " << command << std::endl; + system(command.c_str()); + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: DataSetClass::fixVector ****/ +/******************************************************************************************/ +void DataSetClass::fixVector() +{ + GDALAllRegister(); + + bool genImageFlag = (!parameterTemplate.imageFile.empty()); + std::string tmpImageFile = (parameterTemplate.tmpImageFile.empty() ? "/tmp/image.ppm" : parameterTemplate.tmpImageFile); + + GdalDataModel *gdalDataModel = new GdalDataModel(parameterTemplate.srcFileVector, parameterTemplate.srcHeightFieldName); + + OGRLayer *layer = gdalDataModel->getLayer(); + + double minLon, maxLon; + double minLat, maxLat; + + OGREnvelope oExt; + if (layer->GetExtent(&oExt, TRUE) == OGRERR_NONE) { + minLon = oExt.MinX; + maxLon = oExt.MaxX; + minLat = oExt.MinY; + maxLat = oExt.MaxY; + } + + if (parameterTemplate.verbose) { + std::cout << "MIN_LON = " << minLon << std::endl; + std::cout << "MAX_LON = " << maxLon << std::endl; + std::cout << "MIN_LAT = " << minLat << std::endl; + std::cout << "MAX_LAT = " << maxLat << std::endl; + } + + double res = parameterTemplate.imageLonLatRes; + + int lonN0 = (int) floor(minLon / res); + int lonN1 = (int) ceil(maxLon / res); + int latN0 = (int) floor(minLat / res); + int latN1 = (int) ceil(maxLat / res); + + /**************************************************************************************/ + /* Create PPM File */ + /**************************************************************************************/ + FILE *fppm = (FILE *) NULL; + int imageXSize = lonN1 - lonN0; + int imageYSize = latN1 - latN0; + int imgXIdx, imgYIdx; + + if (genImageFlag) { + if ( !(fppm = fopen(tmpImageFile.c_str(), "wb")) ) { + throw std::runtime_error("ERROR"); + } + fprintf(fppm, "P3\n"); + fprintf(fppm, "%d %d %d\n", imageXSize, imageYSize, 255); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /* Define color scheme */ + /**************************************************************************************/ + std::vector colorList; + colorList.push_back("255 255 255"); // 0: NO BLDG + colorList.push_back("255 0 0"); // 1: BLDG + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Allocate and initialize image array ****/ + /**************************************************************************************/ + int **image = (int **) malloc(imageXSize*sizeof(int *)); + for(imgXIdx=0; imgXIdxGetDriverByName(pszDriverName ); + if( poDriver == NULL ) + { + printf( "%s driver not available.\n", pszDriverName ); + exit( 1 ); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Create Output Dataset (specify output file) ****/ + /**************************************************************************************/ + GDALDataset *outputDS; + + outputDS = poDriver->Create( parameterTemplate.outputFile.c_str(), 0, 0, 0, GDT_Unknown, NULL ); + if( outputDS == NULL ) + { + printf( "Creation of output file failed.\n" ); + exit( 1 ); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Create Layer ****/ + /**************************************************************************************/ + OGRLayer *poLayer; + + poLayer = outputDS->CreateLayer( parameterTemplate.outputLayer.c_str(), layer->GetSpatialRef(), wkbPolygon, NULL ); + if( poLayer == NULL ) + { + printf( "Layer creation failed.\n" ); + exit( 1 ); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Create FIELD: Height ****/ + /**************************************************************************************/ + OGRFieldDefn heightField( parameterTemplate.outputHeightFieldName.c_str(), OFTReal ); + + // 24.15 is ridiculous, change to 12.2 + heightField.SetWidth(12); + heightField.SetPrecision(2); + + if( poLayer->CreateField( &heightField ) != OGRERR_NONE ) + { + std::cout << "Creating " << parameterTemplate.outputHeightFieldName << " field failed.\n"; + exit( 1 ); + } + int heightFIeldIdx = poLayer->FindFieldIndex(parameterTemplate.outputHeightFieldName.c_str(), TRUE); + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Iterate through all features in layer, replace MultiPolygon with polygons ****/ + /**************************************************************************************/ + int numPolygon = 0; + int numNullGeometry = 0; + int numUnrecognizedGeometry = 0; + OGREnvelope env; + std::unique_ptr ptrOFeature; + layer->ResetReading(); + while ((ptrOFeature = std::unique_ptr(layer->GetNextFeature())) != NULL) { + // GDAL maintains ownership of returned pointer, so we should not manage its memory + OGRGeometry *ptrOGeometry = ptrOFeature->GetGeometryRef(); + bool useGeometry = false; + + if (ptrOGeometry != NULL) { + OGRwkbGeometryType geometryType = ptrOGeometry->getGeometryType(); + if (geometryType == OGRwkbGeometryType::wkbPolygon) { + double height = ptrOFeature->GetFieldAsDouble(gdalDataModel->heightFieldIdx); + + OGRFeature *poFeature = OGRFeature::CreateFeature( poLayer->GetLayerDefn() ); + poFeature->SetField(heightFIeldIdx, height); + + OGRPolygon *poly = (OGRPolygon *) ptrOGeometry; + + poFeature->SetGeometry( poly ); + + if( poLayer->CreateFeature( poFeature ) != OGRERR_NONE ) { + printf( "Failed to create feature in shapefile.\n" ); + exit( 1 ); + } + + OGRFeature::DestroyFeature( poFeature ); + useGeometry = true; + numPolygon++; + } else if (geometryType == OGRwkbGeometryType::wkbMultiPolygon ) { + double height = ptrOFeature->GetFieldAsDouble(gdalDataModel->heightFieldIdx); + OGRMultiPolygon *multipoly = (OGRMultiPolygon *) ptrOGeometry; + OGRPolygon *poly; + OGRPolygon **iter; + for (iter = multipoly->begin(); iter != multipoly->end(); ++iter) { + poly = *iter; + OGRFeature *poFeature = OGRFeature::CreateFeature( poLayer->GetLayerDefn() ); + poFeature->SetField(heightFIeldIdx, height); + poFeature->SetGeometry( poly ); + if( poLayer->CreateFeature( poFeature ) != OGRERR_NONE ) { + printf( "Failed to create feature in shapefile.\n" ); + exit( 1 ); + } + numPolygon++; + OGRFeature::DestroyFeature( poFeature ); + } + useGeometry = true; + } else { + std::cout << "WARNING: Unrecognized Geometry Type: " << OGRGeometryTypeToName(geometryType) << std::endl; + numUnrecognizedGeometry++; + useGeometry = false; + } + + if ( (genImageFlag) && (useGeometry) ) { + ptrOGeometry->getEnvelope(&env); + int x0 = ((int) floor(env.MinX / res)) - lonN0; + int x1 = ((int) floor(env.MaxX / res)) - lonN0; + int y0 = ((int) floor(env.MinY / res)) - latN0; + int y1 = ((int) floor(env.MaxY / res)) - latN0; + + for(imgXIdx=x0; imgXIdx<=x1; imgXIdx++) { + for(imgYIdx=y0; imgYIdx<=y1; imgYIdx++) { + image[imgXIdx][imgYIdx] = 1; + } + } + } + } else { + numNullGeometry++; + } + } + /**************************************************************************************/ + + std::cout << "NUM_UNRECOGNIZED_GEOMETRY: " << numUnrecognizedGeometry << std::endl; + std::cout << "NUM_NULL_GEOMETRY: " << numNullGeometry << std::endl; + std::cout << "NUM_POLYGON: " << numPolygon << std::endl; + + if (genImageFlag) { + for(imgYIdx=imageYSize-1; imgYIdx>=0; --imgYIdx) { + for(imgXIdx=0; imgXIdxprintDebugInfo(); + + double minLon, maxLon; + double minLat, maxLat; + + OGRLayer *layer = gdalDataModel->getLayer(); + OGREnvelope oExt; + if (layer->GetExtent(&oExt, TRUE) == OGRERR_NONE) { + minLon = oExt.MinX; + maxLon = oExt.MaxX; + minLat = oExt.MinY; + maxLat = oExt.MaxY; + } + + std::cout << "MIN_LON = " << minLon << std::endl; + std::cout << "MAX_LON = " << maxLon << std::endl; + std::cout << "MIN_LAT = " << minLat << std::endl; + std::cout << "MAX_LAT = " << maxLat << std::endl; + + double res = parameterTemplate.imageLonLatRes; + + int lonN0 = (int) floor(minLon / res); + int lonN1 = (int) ceil(maxLon / res); + int latN0 = (int) floor(minLat / res); + int latN1 = (int) ceil(maxLat / res); + + std::string tmpImageFile = (parameterTemplate.tmpImageFile.empty() ? "/tmp/image.ppm" : parameterTemplate.tmpImageFile); + + /**************************************************************************************/ + /* Create PPM File */ + /**************************************************************************************/ + FILE *fppm = (FILE *) NULL; + int imageXSize = lonN1 - lonN0; + int imageYSize = latN1 - latN0; + int imgXIdx, imgYIdx; + + if ( !(fppm = fopen(tmpImageFile.c_str(), "wb")) ) { + throw std::runtime_error("ERROR"); + } + fprintf(fppm, "P3\n"); + fprintf(fppm, "%d %d %d\n", imageXSize, imageYSize, 255); + /**************************************************************************************/ + + /**************************************************************************************/ + /* Define color scheme */ + /**************************************************************************************/ + std::vector colorList; + colorList.push_back("255 255 255"); // 0: NO BLDG + colorList.push_back("255 0 0"); // 1: BLDG + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Allocate and initialize image array ****/ + /**************************************************************************************/ + int **image = (int **) malloc(imageXSize*sizeof(int *)); + for(imgXIdx=0; imgXIdxGetDriverByName(pszDriverName ); + if( poDriver == NULL ) + { + printf( "%s driver not available.\n", pszDriverName ); + exit( 1 ); + } + /**************************************************************************************/ + + /**************************************************************************************/ + /**** Add all polygons from layer ****/ + /**************************************************************************************/ + OGREnvelope env; + std::unique_ptr ptrOFeature; + layer->ResetReading(); + while ((ptrOFeature = std::unique_ptr(layer->GetNextFeature())) != NULL) { + // GDAL maintains ownership of returned pointer, so we should not manage its memory + OGRGeometry *ptrOGeometry = ptrOFeature->GetGeometryRef(); + + if (ptrOGeometry != NULL) { + OGRwkbGeometryType geometryType = ptrOGeometry->getGeometryType(); + if (geometryType == OGRwkbGeometryType::wkbPolygon) { + + ptrOGeometry->getEnvelope(&env); +#if 0 + std::cout << " MIN_X " << env.MinX + << " MAX_X " << env.MaxX + << " MIN_Y " << env.MinY + << " MAX_Y " << env.MaxY << std::endl; +#endif + + int x0 = ((int) floor(env.MinX / res)) - lonN0; + int x1 = ((int) floor(env.MaxX / res)) - lonN0; + int y0 = ((int) floor(env.MinY / res)) - latN0; + int y1 = ((int) floor(env.MaxY / res)) - latN0; + + for(imgXIdx=x0; imgXIdx<=x1; imgXIdx++) { + for(imgYIdx=y0; imgYIdx<=y1; imgYIdx++) { + image[imgXIdx][imgYIdx] = 1; + } + } + } else { + std::cout << "Contains features of type: " << OGRGeometryTypeToName(geometryType) << std::endl; + } + } + } + /**************************************************************************************/ + + for(imgYIdx=imageYSize-1; imgYIdx>=0; --imgYIdx) { + for(imgXIdx=0; imgXIdxGetDriverByName(pszFormat); + if( !poDriver ) { + exit( 1 ); + } + + bool genImageFlag = (!parameterTemplate.imageFile.empty()); + + papszMetadata = poDriver->GetMetadata(); + if( CSLFetchBoolean( papszMetadata, GDAL_DCAP_CREATE, FALSE ) ) + printf( "Driver %s supports Create() method.\n", pszFormat ); + if( CSLFetchBoolean( papszMetadata, GDAL_DCAP_CREATECOPY, FALSE ) ) + printf( "Driver %s supports CreateCopy() method.\n", pszFormat ); + + GDALDataset *srcDS = (GDALDataset *) GDALOpen( parameterTemplate.srcFileRaster.c_str(), GA_ReadOnly ); + + int nXSize = srcDS->GetRasterXSize(); + int nYSize = srcDS->GetRasterYSize(); + int numRasterBand = srcDS->GetRasterCount(); + + printf( "Driver: %s/%s\n", + srcDS->GetDriver()->GetDescription(), + srcDS->GetDriver()->GetMetadataItem( GDAL_DMD_LONGNAME ) ); + printf( "Size is %dx%dx%d\n", nXSize, nYSize, numRasterBand ); + + if( srcDS->GetProjectionRef() != NULL ) { + printf( "Projection is `%s'\n", srcDS->GetProjectionRef() ); + } + + double adfGeoTransform[6]; + if( srcDS->GetGeoTransform( adfGeoTransform ) == CE_None ) { + printf( "Origin = (%.6f,%.6f)\n", adfGeoTransform[0], adfGeoTransform[3] ); + printf( "Pixel Size = (%.6f,%.6f)\n", adfGeoTransform[1], adfGeoTransform[5] ); + } else { + throw std::runtime_error("ERROR in mbRasterCvg(), unable to determine origin/pixel size"); + } + + double pixelSize = adfGeoTransform[1]; + if ( fabs(pixelSize + adfGeoTransform[5]) > 1.0e-8 ) { + throw std::runtime_error("ERROR: X / Y pixel sizes not properly set"); + } + + std::cout << "NUMBER RASTER BANDS: " << numRasterBand << std::endl; + + if (numRasterBand != 2) { + throw std::runtime_error("ERROR in mbRasterCvg(), numRasterBand must be 2"); + } + + /**************************************************************************************/ + /* Define color scheme */ + /**************************************************************************************/ + std::vector colorList; + colorList.push_back(" 0 0 0"); // 0: BE NO DATA + colorList.push_back(" 0 255 0"); // 1: BE VALID DATA + colorList.push_back(" 0 255 255"); // 2: BE Mix of data/nodata samples + colorList.push_back("255 255 255"); // 3: NO BLDG + colorList.push_back("255 0 0"); // 4: BLDG + /**************************************************************************************/ + + int N = (int) ceil( (parameterTemplate.imageLonLatRes/pixelSize) - 1.0e-8 ); + int imageXSize = (nXSize-1)/N + 1; + int imageYSize = (nYSize-1)/N + 1; + int imgXIdx, imgYIdx; + int *imageScanline = (int *) malloc(imageXSize*sizeof(int)); + float *pafScanline = (float *) CPLMalloc(nXSize*GDALGetDataTypeSizeBytes(GDT_Float32)); + + int rasterBandIdx; + for(rasterBandIdx=1; rasterBandIdx<=numRasterBand; rasterBandIdx++) { + GDALRasterBand *rasterBand = srcDS->GetRasterBand(rasterBandIdx); + + int hasNoData; + float nodataValue = (float) rasterBand->GetNoDataValue(&hasNoData); + if (hasNoData) { + std::cout << "NODATA: " << nodataValue << std::endl; + } else { + std::cout << "NODATA undefined" << std::endl; + } + + int xIdx, yIdx; + if (parameterTemplate.verbose) { + std::cout << "GDALGetDataTypeSizeBytes(GDT_Float32) = " << GDALGetDataTypeSizeBytes(GDT_Float32) << std::endl; + std::cout << "sizeof(GDT_Float32) = " << sizeof(GDT_Float32) << std::endl; + std::cout << "sizeof(GDT_Float64) = " << sizeof(GDT_Float64) << std::endl; + std::cout << "sizeof(float) = " << sizeof(float) << std::endl; + } + + /**************************************************************************************/ + /* Create PPM File */ + /**************************************************************************************/ + std::string ppmFile = "/tmp/image_" + std::to_string(rasterBandIdx) + ".ppm"; + FILE *fppm = (FILE *) NULL; + if ( !(fppm = fopen(ppmFile.c_str(), "wb")) ) { + throw std::runtime_error("ERROR"); + } + fprintf(fppm, "P3\n"); + fprintf(fppm, "%d %d %d\n", imageXSize, imageYSize, 255); + for(imgXIdx=0; imgXIdxRasterIO( GF_Read, 0, yIdx, nXSize, 1, pafScanline, nXSize, 1, GDT_Float32, 0, 0 ); + int colorIdx; + for(xIdx=0; xIdx +#endif + +#include +#include + +#define MAX_LINE_SIZE 5000 +#define CHDELIM " \t\n" /* Delimiting characters, used for string parsing */ +#define LEFT_BRACE "{" +#define RIGHT_BRACE "}" +#define LEFT_PAREN "(" +#define RIGHT_PAREN ")" +#define PI 3.1415926535897932384626433832795029 + +#define IVECTOR(nn) (int *) ( (nn) ? malloc((nn) *sizeof(int) ) : NULL ) +#define DVECTOR(nn) (double *) ( (nn) ? malloc((nn) *sizeof(double)) : NULL ) +#define CVECTOR(nn) (char *) ( (nn) ? malloc(((nn)+1)*sizeof(char) ) : NULL ) + +#define CORE_DUMP printf("%d", *((int *) NULL)) +/******************************************************************************************/ + +#define BITWIDTH(www, nnn) { \ + int tmp; \ + tmp = (nnn); \ + www = 0; \ + while (tmp) { \ + www++; \ + tmp>>=1; \ + } \ +} + +#define MOD(m, n) ( (m) >= 0 ? (m)%(n) : ((n)-1) - ((-(m)-1)%(n)) ) + +#define DIV(m, n) ( \ + ((m) >= 0)&&((n) >= 0) ? (m)/(n) : \ + ((m) < 0)&&((n) < 0) ? (-(m))/(-(n)) : \ + ((m) < 0)&&((n) >= 0) ? -((-(m)-1)/(n) + 1) : \ + -(((m)-1)/(-(n)) + 1) ) + +#ifdef __linux__ +#include +#define LONGLONG_TYPE long long +#define LONGLONG_VAL(x) x ## LL +#define UNSLONGLONG_TYPE unsigned long long +#define UNSLONGLONG_VAL(x) x ## ULL +#define UNSLONG_TYPE uint32_t +#define FPATH_SEPARATOR '/' +#define SOCKET_ERRNO errno +#define STRCASECMP strcasecmp +#define MKDIR(ddd) mkdir(ddd, 0755) +#else +#include +#define LONGLONG_TYPE __int64 +#define LONGLONG_VAL(x) x ## i64 +#define UNSLONGLONG_TYPE __uint64 +#define UNSLONGLONG_VAL(x) x ## ui64 +#define UNSLONG_TYPE u_long +#define FPATH_SEPARATOR '\\' +#define SOCKET_ERRNO WSAGetLastError() +#define STRCASECMP stricmp +#define MKDIR(ddd) mkdir(ddd) +#define snprintf sprintf_s +#endif + +#define GUIDBG(mmm) QMessageBox::critical(0, "GUIDBG", mmm, \ + QMessageBox::Ok | QMessageBox::Default | QMessageBox::Escape, \ + QMessageBox::NoButton, \ + QMessageBox::NoButton \ + ); + +#define xstr(s) mfkstr(s) +#define mfkstr(s) #s + +#endif diff --git a/tools/geo_converters/proc_gdal/global_fn.cpp b/tools/geo_converters/proc_gdal/global_fn.cpp new file mode 100644 index 0000000..1f1d71c --- /dev/null +++ b/tools/geo_converters/proc_gdal/global_fn.cpp @@ -0,0 +1,1230 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#include +#else +#include +#include +#include +#include +#endif + +#include "global_fn.h" + +/******************************************************************************************/ +/**** Read a line into string s, return length. From "C Programming Language" Pg. 29 ****/ +/**** Modified to be able to read both DOS and UNIX files. ****/ +/**** 2013.03.11: use std::string so not necessary to pre-allocate storage. ****/ +/**** Return value is number of characters read from FILE, which may or may not equal ****/ +/**** the length of string s depending on whether '\r' or '\n' has been removed from ****/ +/**** the string. ****/ +/******************************************************************************************/ +int fgetline(FILE *file, std::string& s, bool keepcr) +{ + int c, i; + + s.clear(); + for (i=0; (c=fgetc(file)) != EOF && c != '\n'; i++) { + s += c; + } + if ( (i >= 1) && (s[i-1] == '\r') ) { + s.erase(i-1,1); + // i--; + } + if (c == '\n') { + if (keepcr) { + s += c; + } + i++; + } + return(i); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Read a line into string s, return length. From "C Programming Language" Pg. 29 ****/ +/**** Modified to be able to read both DOS and UNIX files. ****/ +/******************************************************************************************/ +int fgetline(FILE *file, char *s) +{ + int c, i; + + for (i=0; (c=fgetc(file)) != EOF && c != '\n'; i++) { + s[i] = c; + } + if ( (i >= 1) && (s[i-1] == '\r') ) { + i--; + } + if (c == '\n') { + s[i] = c; + i++; + } + s[i] = '\0'; + return(i); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Split string into vector of strings using specified delim. ****/ +/******************************************************************************************/ +std::vector& split(const std::string &s, char delim, std::vector &elems) { + std::stringstream ss(s); + std::string item; + while (std::getline(ss, item, delim)) { + elems.push_back(item); + } + return elems; +} + + +std::vector split(const std::string &s, char delim) { + std::vector elems; + split(s, delim, elems); + return elems; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Split string into vector of strings for command line processing. ****/ +/******************************************************************************************/ +std::vector splitOptions(const std::string &cmd) +{ + int i, fieldLength; + int fieldStartIdx = -1; + std::ostringstream s; + std::vector elems; + std::string field; + + // state 0 = looking for next field + // state 1 = found beginning of field, not "'", looking for end of field (space) + // state 2 = found beginning of field, is "'", looking for end of field ("'"). + int state = 0; + + for(i=0; i<(int) cmd.length(); i++) { + switch(state) { + case 0: + if (cmd.at(i) == ' ') { + // do nothing + } else if (cmd.at(i) == '\'') { + fieldStartIdx = i+1; + state = 2; + } else { + fieldStartIdx = i; + state = 1; + } + break; + case 1: + if (cmd.at(i) == ' ') { + fieldLength = i-fieldStartIdx; + field = cmd.substr(fieldStartIdx, fieldLength); + elems.push_back(field); + state = 0; + } + break; + case 2: + if (cmd.at(i) == '\'') { + fieldLength = i-fieldStartIdx; + field = cmd.substr(fieldStartIdx, fieldLength); + elems.push_back(field); + state = 0; + } + break; + default: + CORE_DUMP; + break; + } + if (i == ((int) cmd.length())-1) { + if (state == 1) { + fieldLength = i-fieldStartIdx+1; + field = cmd.substr(fieldStartIdx, fieldLength); + elems.push_back(field); + state = 0; + } else if (state == 2) { + s << "ERROR: Unable to splitOptions() for command \"" << cmd << "\" unmatched single quote.\n"; + throw std::runtime_error(s.str()); + } + } + } + + if (state != 0) { + CORE_DUMP; + } + + return elems; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Split string into vector of strings for each CSV field properly treating fields ****/ +/**** enclosed in double quotes with embedded commas. Also, double quotes can me ****/ +/**** embedded in a field using 2 double quotes "". ****/ +/**** This format is compatible with excel and libreoffice CSV files. ****/ +/******************************************************************************************/ +std::vector splitCSV(const std::string &line) +{ + int i, fieldLength; + int fieldStartIdx = -1; + std::ostringstream s; + std::vector elems; + std::string field; + bool skipChar = false; + + // state 0 = looking for next field + // state 1 = found beginning of field, not ", looking for end of field (comma) + // state 2 = found beginning of field, is ", looking for end of field ("). + // state 3 = found end of field, is ", pass over 0 or more spaces until (comma) + int state = 0; + + for(i=0; i<(int) line.length(); i++) { + if (skipChar) { + skipChar = false; + } else { + switch(state) { + case 0: + if (line.at(i) == '\"') { + fieldStartIdx = i+1; + state = 2; + } else if (line.at(i) == ',') { + field.clear(); + elems.push_back(field); + } else if (line.at(i) == ' ') { + // do nothing + } else { + fieldStartIdx = i; + state = 1; + } + break; + case 1: + if (line.at(i) == ',') { + fieldLength = i-fieldStartIdx; + field = line.substr(fieldStartIdx, fieldLength); + + std::size_t start = field.find_first_not_of(" \n\t"); + std::size_t end = field.find_last_not_of(" \n\t"); + if (start == std::string::npos) { + field.clear(); + } else { + field = field.substr(start, end-start+1); + } + + elems.push_back(field); + state = 0; + } + break; + case 2: + if (line.at(i) == '\"') { + if ( (i+1 < (int) line.length()) && (line.at(i+1) == '\"') ) { + skipChar = true; + } else { + fieldLength = i-fieldStartIdx; + field = line.substr(fieldStartIdx, fieldLength); + std::size_t k = field.find("\"\""); + while(k != std::string::npos) { + field.erase(k,1); + k = field.find("\"\""); + } + elems.push_back(field); + state = 3; + } + } + break; + case 3: + if (line.at(i) == ' ') { + // do nothing + } else if (line.at(i) == ',') { + state = 0; + } else { + s << "ERROR: Unable to splitCSV() for command \"" << line << "\" invalid quotes.\n"; + throw std::runtime_error(s.str()); + } + break; + default: + CORE_DUMP; + break; + } + } + if (i == ((int) line.length())-1) { + if (state == 0) { + field.clear(); + elems.push_back(field); + } else if (state == 1) { + fieldLength = i-fieldStartIdx+1; + field = line.substr(fieldStartIdx, fieldLength); + + std::size_t start = field.find_first_not_of(" \n\t"); + std::size_t end = field.find_last_not_of(" \n\t"); + if (start == std::string::npos) { + field.clear(); + } else { + field = field.substr(start, end-start+1); + } + + elems.push_back(field); + state = 0; + } else if (state == 2) { + s << "ERROR: Unable to splitCSV() for command \"" << line << "\" unmatched quote.\n"; + throw std::runtime_error(s.str()); + } else if (state == 3) { + state = 0; + } + } + } + + if (state != 0) { + CORE_DUMP; + } + + return elems; +} +/******************************************************************************************/ + +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Get field from a std::string, equivalent to strtok. ****/ +/******************************************************************************************/ +std::string getField(const std::string& strVal, int& posn, const char *chdelim) +{ + int fstart, fstop; + std::string field; + + fstart = strVal.find_first_not_of(chdelim, posn); + if (fstart == (int) std::string::npos) { + field.clear(); + fstop = fstart; + } else { + fstop = strVal.find_first_of(chdelim, fstart); + if (fstop == (int) std::string::npos) { + field = strVal.substr(fstart); + fstop = strVal.length(); + } else { + field = strVal.substr(fstart, fstop-fstart); + } + } + posn = fstop; + + return(field); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Copy file src to file dest. Return 1 if successful, 0 otherwise. ****/ +/******************************************************************************************/ +int copyFile(const char *src, const char *dest) +{ + int c; + FILE *fsrc, *fdest; + + if ( !(fsrc = fopen(src, "rb")) ) { + printf("ERROR: Unable to read file \"%s\"\n", src); + return(0); + } + + if ( !(fdest = fopen(dest, "wb")) ) { + printf("ERROR: Unable to write to file \"%s\"\n", dest); + fclose(fsrc); + return(0); + } + + while((c=getc(fsrc))!=EOF) { + putc(c, fdest); + } + + fclose(fsrc); + fclose(fdest); + + return(1); +} +/******************************************************************************************/ +/**** Check if file or directory exists. ****/ +/**** Return 0 if doesn't exist, 1 if it is a file, 2 if it is a directory ****/ +/******************************************************************************************/ +int fileExists(const char *filename) +{ + struct stat statInfo; + int exists; + int intStat; + + intStat = stat(filename, &statInfo); + + exists = (intStat ? 0 : + (statInfo.st_mode & S_IFDIR) ? 2 : 1); + + return(exists); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** Remove double quotes that surround a string. Note that spaces are removed from ****/ +/**** the beginning of the string before the first quote and both spaces and are ****/ +/**** removed from the end of the string after the final quote. If a matching pair of ****/ +/**** quotes is not found the string is left unchanged. Versions for char* and ****/ +/**** std::string ****/ +/******************************************************************************************/ +char *remove_quotes(char *str) +{ + int n; + char *start, *end; + char *ret_str = str; + + if (str) { + start = str; + while( (*start) == ' ' ) { start++; } + n = strlen(str); + end = str + n - 1; + while( (end > start) && (((*end) == ' ') || ((*end) == '\n')) ) { end--; } + if ( (end > start) && ((*start) == '\"') && ((*end) == '\"') ) { + ret_str = start+1; + (*end) = (char) NULL; + } + } + + return(ret_str); +} + +std::string remove_quotes(const std::string& str) +{ + std::size_t start, end; + std::string ret_str = str; + + if (!str.empty()) { +// printf("REMOVE QUOTES FROM ::%s::\n", str.c_str()); + start = str.find_first_not_of(' '); + end = str.find_last_not_of(" \n"); + if ( (start != std::string::npos) + && (end != std::string::npos) + && (end > start) + && (str[start] == '\"') + && (str[end] == '\"') + ) { + ret_str = str.substr(start+1, end-start-1); + } + } + + return(ret_str); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: escape_quotes ****/ +/**** INPUTS: str ****/ +/**** OUTPUTS: possibly modified str ****/ +/**** Insert backslash '\' in from of each double quote character '"' in str. ****/ +char *escape_quotes(char *&str) +{ + char *ptrA, *ptrB; + + int num = 0; + ptrA = str; + while(*ptrA) { + if ((*ptrA) == '"') { + num++; + } + ptrA++; + } + + if (num) { + char *tmpstr = CVECTOR(strlen(str) + num); + ptrA = str; + ptrB = tmpstr; + + while(*ptrA) { + if ((*ptrA) == '"') { + (*ptrB) = '\\'; + ptrB++; + } + *ptrB = *ptrA; + ptrA++; + ptrB++; + } + *ptrB = *ptrA; + + free(str); + str = tmpstr; + } + + return(str); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: getCSVField ****/ +/**** INPUTS: str, rmWhiteSpaceFlag, fs ****/ +/**** OUTPUTS: startPtr, fieldLen ****/ +/**** Return val is ptr to beginning of next field, or NULL if no more fields. This ****/ +/**** is diffenent than strtok() in that multiple fs characters will be interpreted as ****/ +/**** separate fields. The number of fields is always the number of fs characters ****/ +/**** plus one. Examples: ****/ +/**** "A,B,C": 3 Fields => "A", "B", "C". ****/ +/**** ",,": 3 Fields => "", "", "". ****/ +/**** "": 1 Fields => "" ****/ +/**** ****/ +/**** Iterate over fields like this: ****/ +/**** chptr = str; ****/ +/**** do { ****/ +/**** chptr = getCSVField(chptr, startPtr, fieldLen); ****/ +/**** fieldStr = strndup(startPtr, fieldLen); ****/ +/**** free(fieldStr); ****/ +/**** } while(chptr); ****/ +/******************************************************************************************/ +const char *getCSVField(const char *str, const char *&startPtr, int& fieldLen, const bool rmWhiteSpaceFlag, const char fs) +{ + startPtr = str; + + const char *endPtr = startPtr; + + while((*endPtr)&&(*endPtr != fs)) { endPtr++; } + + fieldLen = endPtr-startPtr; + + if(rmWhiteSpaceFlag) { + while((fieldLen)&&isspace(*startPtr)) { startPtr++; fieldLen--; } + while((fieldLen)&&isspace(*(startPtr+fieldLen-1))) { fieldLen--; } + } + + if (*endPtr == fs) { + endPtr++; + } else { + endPtr = (char *) NULL; + } + + return(endPtr); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: gcd ****/ +/**** INPUTS: a, b ****/ +/**** OUTPUTS: gcd(a,b) ****/ +/**** Returns the greatest common divisor of two integers. ****/ +int gcd(int a, int b) +{ int c; + + a = abs(a); b = abs(b); + while (a) { + c = b; + b = a; + a = c % a; + } + + return(b); +} +/******************************************************************************************/ +void extended_euclid(int a, int b, int& gcd, int& p1, int& p2) +{ + int next_gcd, next_p1, next_p2; + int new_gcd, new_p1, new_p2; + + if ( (a<0) || (b<0) ) { + printf("ERROR in routine extended_euclid():"); + printf(" values must be positive %d, %d\n", a,b); + CORE_DUMP; + } + + gcd = a; p1 = 1; p2 = 0; + next_gcd = b; next_p1 = 0; next_p2 = 1; + + while(next_gcd) { + new_gcd = gcd % next_gcd; + new_p1 = p1 - next_p1*(gcd/next_gcd); + new_p2 = p2 - next_p2*(gcd/next_gcd); + gcd = next_gcd; + p1 = next_p1; + p2 = next_p2; + next_gcd = new_gcd; + next_p1 = new_p1; + next_p2 = new_p2; + } + + return; +} +/******************************************************************************************/ +/**** FUNCTION: set_current_dir_from_file ****/ +/**** INPUTS: filename ****/ +/**** OUTPUTS: ****/ +/**** Set current working directory to that of the specified filename. ****/ +void set_current_dir_from_file(char *filename) +{ + char *path = strdup(filename); + int n = strlen(filename); + int found = 0; + int i; + + for (i=n-1; (i>=0)&&(!found); i--) { + if ( (path[i] == '\\') || (path[i] == '/') ) { + found = 1; + path[i+1] = (char) NULL; + } + } + + if (found) { + if (chdir(path) != 0) { +#if CDEBUG + CORE_DUMP; +#endif + } + } + + free(path); +} +/******************************************************************************************/ +/**** FUNCTION: insertFilePfx ****/ +/**** INPUTS: pfx, filename ****/ +/**** OUTPUTS: ****/ +/**** For filename = "d0/d1/d2/f.txt", output is "d0/d1/d2/pfx_f.txt". ****/ +char *insertFilePfx(const char *pfx, const char *filename) +{ + int n = strlen(filename); + bool found = false; + int posn = 0; + int i; + + for (i=n-1; (i>=0)&&(!found); i--) { + if ( (filename[i] == '\\') || (filename[i] == '/') ) { + found = true; + if (i==n-1) { +#if CDEBUG + CORE_DUMP; +#endif + } else { + posn = i+1; + } + } + } + + char *pfxFilename = (char *) malloc(strlen(filename) + strlen(pfx) + 1); + + int k = 0; + for(i=0; i<=posn-1; i++) { + pfxFilename[k] = filename[i]; + k++; + } + for(i=0; i<=((int) strlen(pfx))-1; i++) { + pfxFilename[k] = pfx[i]; + k++; + } + for(i=posn; i<=n-1; i++) { + pfxFilename[k] = filename[i]; + k++; + } + pfxFilename[k] = '\0'; + + return(pfxFilename); +} +/******************************************************************************************/ +/**** FUNCTION: insertFilePfx ****/ +/**** INPUTS: pfx, filename ****/ +/**** OUTPUTS: ****/ +/**** For filename = "d0/d1/d2/f.txt", output is "d0/d1/d2/pfx_f.txt". ****/ +std::string insertFilePfx(const std::string& pfx, const std::string& filename) +{ + std::string pfxFilename; + size_t posn = filename.find_last_of("/\\"); + + if (posn == std::string::npos) { + pfxFilename = pfx + filename; + } else { + posn++; + pfxFilename = filename.substr(0, posn) + pfx + filename.substr(posn); + } + + return(pfxFilename); +} +/******************************************************************************************/ +int stringcmp(const char *s1, const char *s2) +{ + int n1 = strlen(s1); + int n2 = strlen(s2); + + int done = 0; + int d1, d2; + int nd1=0, nd2=0; + int v1, v2, num_eq; + int retval = 0; + char c1, c2; + + if ( (n1 == 0) && (n2 == 0) ) { + return(0); + } else if (n1 == 0) { + return(-1); + } else if (n2 == 0) { + return(1); + } + + int i1 = 0; + int i2 = 0; + + while(!done) { + c1 = s1[i1]; + d1 = ((c1 >= '0') && (c1 <= '9')) ? 1 : 0; + if (d1) { + nd1 = 1; + while ((i1+nd1= '0') && (s1[i1+nd1] <= '9')) { + nd1++; + } + } + + c2 = s2[i2]; + d2 = ((c2 >= '0') && (c2 <= '9')) ? 1 : 0; + if (d2) { + nd2 = 1; + while ((i2+nd2= '0') && (s2[i2+nd2] <= '9')) { + nd2++; + } + } + + if ((!d1) && (!d2)) { + if (c1 < c2) { + retval = -1; + done = 1; + } else if (c1 > c2) { + retval = 1; + done = 1; + } else if ((i1 == n1-1) && (i2 < n2-1)) { + retval = -1; + done = 1; + } else if ((i2 == n2-1) && (i1 < n1-1)) { + retval = 1; + done = 1; + } else if ((i1 == n1-1) && (i2 == n2-1)) { + retval = 0; + done = 1; + } else { + i1++; + i2++; + } + } else if ((!d1) && (d2)) { + retval = 1; + done = 1; + } else if ((d1) && (!d2)) { + retval = -1; + done = 1; + } else if ((d1) && (d2)) { + while ((nd1 > nd2)&&(!done)) { + if (s1[i1] > '0') { + retval = 1; + done = 1; + } else { + i1++; + nd1--; + } + } + while ((nd2 > nd1)&&(!done)) { + if (s2[i2] > '0') { + retval = -1; + done = 1; + } else { + i2++; + nd2--; + } + } + num_eq = 0; + while( (!done) && (!num_eq) ) { + if (nd1==0) { + if ((i1 == n1) && (i2 < n2)) { + retval = -1; + done = 1; + } else if ((i2 == n2) && (i1 < n1)) { + retval = 1; + done = 1; + } else if ((i1 == n1) && (i2 == n2)) { + retval = 0; + done = 1; + } else { + num_eq = 1; + } + } else { + v1 = s1[i1] - '0'; + v2 = s2[i2] - '0'; + if (v1 < v2) { + retval = -1; + done = 1; + } else if (v1 > v2) { + retval = 1; + done = 1; + } else { + i1++; + i2++; + nd1--; + nd2--; + } + } + } + } + } + + // printf("S1 = \"%s\" S2 = \"%s\" RETVAL = %d\n", s1.latin1(), s2.latin1(), retval); + + return(retval); +} +/******************************************************************************************/ +/**** FUNCTION: lowercase ****/ +/**** Convert string to lowercase. ****/ +/******************************************************************************************/ +void lowercase(char *s1) +{ + char *chptr = s1; + + while(*chptr) { + if ( ((*chptr) >= 'A') && ((*chptr) <= 'Z') ) { + *chptr += 'a' - 'A'; + } + chptr++; + } +} +void lowercase(std::string& s1) +{ + int i; + for(i=0; i<=(int) s1.length()-1; i++) { + s1[i] = std::tolower(s1[i]); + } + return; +} +/******************************************************************************************/ +/**** FUNCTION: uppercase ****/ +/**** Convert string to uppercase. ****/ +/******************************************************************************************/ +void uppercase(std::string& s1) +{ + int i; + for(i=0; i<=(int) s1.length()-1; i++) { + s1[i] = std::toupper(s1[i]); + } + return; +} +/******************************************************************************************/ +void get_bits(char *str, int n, int num_bits, int insNull) +{ + int i, bit; + + for (i=num_bits-1; i>=0; i--) + { bit = (n >> i)&1; + str[(num_bits-1) - i] = (bit ? '1' : '0'); + } + + if (insNull) { + str[num_bits] = '\0'; + } +} +/******************************************************************************************/ +void get_bits(char *str, LONGLONG_TYPE n, int num_bits, int insNull) +{ + int i, bit; + + for (i=num_bits-1; i>=0; i--) { + bit = (int) ((n >> i)&1); + str[(num_bits-1) - i] = (bit ? '1' : '0'); + } + + if (insNull) { + str[num_bits] = '\0'; + } +} +/******************************************************************************************/ +void get_hex(char *str, int n, int num_hex, int insNull) +{ + int i, hex; + + for (i=num_hex-1; i>=0; i--) + { hex = (n >> 4*i)&0x0F; + str[(num_hex-1) - i] = (hex<=9 ? '0'+hex : 'A' + hex - 10); + } + + if (insNull) { + str[num_hex] = '\0'; + } +} +/******************************************************************************************/ +int is_big_endian() +{ + int big_endian; + int n = 0x89abcdef; + unsigned char *ch_ptr; + + ch_ptr = (unsigned char *) &n; + + if ( (ch_ptr[0] == 0x89) + && (ch_ptr[1] == 0xab) + && (ch_ptr[2] == 0xcd) + && (ch_ptr[3] == 0xef) ) { + big_endian = 1; + } else if ( + (ch_ptr[3] == 0x89) + && (ch_ptr[2] == 0xab) + && (ch_ptr[1] == 0xcd) + && (ch_ptr[0] == 0xef) ) { + big_endian = 0; + } else { + big_endian = -1; + } + + return(big_endian); +} +/******************************************************************************************/ +#ifdef __linux__ +bool deleteFile(const char *filename, bool) +{ + char *cmd = (char *) malloc(strlen(filename) + 20); + sprintf(cmd, "rm -fr \"%s\"", filename); + int ret = system(cmd); + free(cmd); + + return(ret==-1 ? false : true); +} +#else +bool deleteFile(const char *filename, bool noRecycleBin) +{ + int i; + int len = strlen(filename); + TCHAR *pszFrom = new TCHAR[len+2]; + if (sizeof(TCHAR)==1) { + _tcscpy((char *) pszFrom, filename); + } else { + MultiByteToWideChar( + CP_ACP, // code page + NULL, // character-type options + filename, // string to map + -1, // number of bytes in string + (wchar_t *) pszFrom, // wide-character buffer + len+2 // size of buffer + ); + } + + /**************************************************************************************/ + /**** Replace '/' with '\', SHFileOperation does NOT work with '/' (BUG ??) ****/ + /**************************************************************************************/ + for(i=0; i<=len-1; i++) { + if (pszFrom[i] == '/') { + pszFrom[i] = '\\'; + } + } + /**************************************************************************************/ + + pszFrom[len] = 0; + pszFrom[len+1] = 0; + + SHFILEOPSTRUCT fileop; + fileop.hwnd = NULL; // no status display + fileop.wFunc = FO_DELETE; // delete operation + fileop.pFrom = pszFrom; // source file name as double null terminated string + fileop.pTo = NULL; // no destination needed + fileop.fFlags = FOF_NOCONFIRMATION|FOF_SILENT; // do not prompt the user + + if(!noRecycleBin) { + fileop.fFlags |= FOF_ALLOWUNDO; + } + + fileop.fAnyOperationsAborted = FALSE; + fileop.lpszProgressTitle = NULL; + fileop.hNameMappings = NULL; + + int ret = SHFileOperation(&fileop); + delete [] pszFrom; + return (ret == 0); +} +#endif +/******************************************************************************************/ +#ifdef __linux__ +bool isFirstInstance(const char *) +{ + return(true); +} +#else +bool isFirstInstance(const char *mutexName) +{ + bool retval; + int i; + int len = strlen(mutexName); + TCHAR *mutexNameTCHAR = new TCHAR[len+2]; + if (sizeof(TCHAR)==1) { + _tcscpy((char *) mutexNameTCHAR, mutexName); + } else { + MultiByteToWideChar( + CP_ACP, // code page + NULL, // character-type options + mutexName, // string to map + -1, // number of bytes in string + (wchar_t *) mutexNameTCHAR, // wide-character buffer + len+2 // size of buffer + ); + } + mutexNameTCHAR[len] = 0; + mutexNameTCHAR[len+1] = 0; + + HANDLE hMutex = CreateMutex(NULL, TRUE, mutexNameTCHAR); + + if (GetLastError() == ERROR_ALREADY_EXISTS) { + // Another instance of the app exists + retval = false; + } else { + // This is the first instance + retval = true; + } + + delete [] mutexNameTCHAR; + + return retval; +} +#endif +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: readTwoCol ****/ +/**** INPUTS: file "flname" ****/ +/**** OUTPUTS: n, x[0...n-1], y[0...n-1] ****/ +/**** Reads two column tabular data from the file "flname". The number of lines of ****/ +/**** data in the file is n. This first column is read into the variables x[i] and ****/ +/**** the second column is read into the variables y[i] (i=0,1,...,n-1). Lines ****/ +/**** beginning with the character '#' are ignored (to allow for comments). The ****/ +/**** parameters maxLineSize, and maxPts specify the maximum number of characters per ****/ +/**** line and the maximum number of points in the file respectively. ****/ +/**** Return value: 1 if successfull, 0 if not successful ****/ +/******************************************************************************************/ +int readTwoCol(double *x, double *y, int& numPts, const char *flname, int maxNumPts, int maxLineSize) +{ + char *line, *lnptr, *chptr, *errmsg; + FILE *fp; + + line = CVECTOR(maxLineSize); + errmsg = CVECTOR(maxLineSize); + +#define TMP_NEDELIM (lnptr[0] != ',')&&(lnptr[0] != ' ')&&(lnptr[0] != '\t') +#define TMP_EQDELIM (lnptr[0] == ',')||(lnptr[0] == ' ')||(lnptr[0] == '\t') + + if ( strcmp(flname,"stdin") == 0 ) { + fp = stdin; + } else if ( !(fp = fopen(flname, "rb")) ) { + sprintf(errmsg, "ERROR: Unable to read from file %s\n", flname); + printf("%s", errmsg); + return(0); + } + + numPts = 0; + + while ( fgetline(fp, line) ) { + lnptr = line; + while ( (lnptr[0] == ' ') || (lnptr[0] == '\t') ) lnptr++; + if ( (lnptr[0] != '#') && (lnptr[0] != '\n') ) { + if ((numPts) >= maxNumPts) { + chptr = errmsg; + chptr += sprintf(chptr, "ERROR in routine readTwoCol()\n"); + chptr += sprintf(chptr, "Number of points in file %s exceeds maxNumPts = %d\n", flname, maxNumPts); + printf("%s", errmsg); + return(0); + } + + x[numPts] = atof(lnptr); + while ( TMP_NEDELIM ) lnptr++; + while ( TMP_EQDELIM ) lnptr++; + y[numPts] = atof(lnptr); + + numPts++; + } + } + if (fp != stdin) { + fclose(fp); + } + + free(line); + free(errmsg); +#undef TMP_NEDELIM +#undef TMP_EQDELIM + + return(1); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: readThreeCol ****/ +/**** INPUTS: file "flname" ****/ +/**** OUTPUTS: n, x[0...n-1], y[0...n-1], z[0...n-1] ****/ +/**** Reads three column tabular data from the file "flname". The number of lines of ****/ +/**** data in the file is n. This first column is read into the variables x[i], ****/ +/**** the second column is read into the variables y[i], and the third column is read ****/ +/**** into the variables z[i] (i=0,1,...,n-1). Lines ****/ +/**** beginning with the character '#' are ignored (to allow for comments). The ****/ +/**** parameters maxLineSize, and maxPts specify the maximum number of characters per ****/ +/**** line and the maximum number of points in the file respectively. ****/ +/**** Return value: 1 if successfull, 0 if not successful ****/ +/******************************************************************************************/ +int readThreeCol(double *x, double *y, double *z, int& numPts, const char *flname, int maxNumPts, int maxLineSize) +{ + char *line, *lnptr, *chptr, *errmsg; + FILE *fp; + + line = CVECTOR(maxLineSize); + errmsg = CVECTOR(maxLineSize); + +#define TMP_NEDELIM (lnptr[0] != ',')&&(lnptr[0] != ' ')&&(lnptr[0] != '\t') +#define TMP_EQDELIM (lnptr[0] == ',')||(lnptr[0] == ' ')||(lnptr[0] == '\t') + + if ( strcmp(flname,"stdin") == 0 ) { + fp = stdin; + } else if ( !(fp = fopen(flname, "rb")) ) { + sprintf(errmsg, "ERROR: Unable to read from file %s\n", flname); + printf("%s", errmsg); + return(0); + } + + numPts = 0; + + while ( fgetline(fp, line) ) { + lnptr = line; + while ( (lnptr[0] == ' ') || (lnptr[0] == '\t') ) lnptr++; + if ( (lnptr[0] != '#') && (lnptr[0] != '\n') ) { + if ((numPts) >= maxNumPts) { + chptr = errmsg; + chptr += sprintf(chptr, "ERROR in routine readTwoCol()\n"); + chptr += sprintf(chptr, "Number of points in file %s exceeds maxNumPts = %d\n", flname, maxNumPts); + printf("%s", errmsg); + return(0); + } + + x[numPts] = atof(lnptr); + while ( TMP_NEDELIM ) lnptr++; + while ( TMP_EQDELIM ) lnptr++; + y[numPts] = atof(lnptr); + while ( TMP_NEDELIM ) lnptr++; + while ( TMP_EQDELIM ) lnptr++; + z[numPts] = atof(lnptr); + + numPts++; + } + } + if (fp != stdin) { + fclose(fp); + } + + free(line); + free(errmsg); +#undef TMP_NEDELIM +#undef TMP_EQDELIM + + return(1); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/*** FUNCTION: writeTwoCol ***/ +/*** INPUTS: x[], y[], n, file "flname" ***/ +/*** OUTPUTS: n, x[0...n-1], y[0...n-1] ***/ +/*** Writes two column tabular data to the file "flname". The number of lines of ***/ +/*** data in the file is n. This first column is written from the variables x[i] and ***/ +/*** the second column is written from the variables y[i] (i=0,1,...,n-1). The double ***/ +/*** values of x[i] and y[i] are written with the format of the string fmt. Thus to ***/ +/*** write each set of values on a separate line with a %12.10f format and tab ***/ +/*** delimited, fmt should be "%12.10f\t%12.10f\n". ***/ +void writeTwoCol(const double *x, const double *y, int n, const char *fmt, const char *flname) +{ + FILE *fp; + int i; + std::ostringstream ss; + + if ( !(fp = fopen(flname, "w")) ) { + ss << "Error writing to file: \"" << flname << "\""; + throw std::runtime_error(ss.str()); + } + + for (i=0; i<=n-1; i++) { + fprintf(fp, fmt, x[i], y[i]); + } + fprintf(fp, "\n"); + fclose(fp); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: writeOneCol ****/ +/**** INPUTS: x[], n, file "flname" ****/ +/**** OUTPUTS: n, x[0...n-1] ****/ +/**** Writes one column tabular data to the file "flname". The number of lines of ****/ +/**** data in the file is n. This single column data values are written from the ****/ +/**** variables x[i] (i=0,1,...,n-1). The double values of x[i] are written with the ****/ +/**** format of the string fmt. ****/ +/******************************************************************************************/ +void writeOneCol(const double *x, int n, const char *fmt, const char *flname) +{ + FILE *fp; + int i; + std::ostringstream ss; + + if (flname) { + if ( !(fp = fopen(flname, "w")) ) { + ss << "Error writing to file: \"" << flname << "\""; + throw std::runtime_error(ss.str()); + } + } else { + fp = stdout; + } + + for (i=0; i<=n-1; i++) { + fprintf(fp, fmt, x[i]); + } + fprintf(fp, "\n"); + + if (flname) { + fclose(fp); + } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: cvtStrToVal (std::complex) ****/ +/******************************************************************************************/ +int cvtStrToVal(char const* strptr, std::complex& val) +{ + const char *chptr = strptr; + char *nptr; + int i; + double rval, ival; + + for(i=0; i<=1; i++) { + switch(i) { + case 0: rval = strtod(chptr, &nptr); break; + case 1: ival = strtod(chptr, &nptr); break; + } + if (nptr == chptr) { + std::stringstream errorStr; + errorStr << "ERROR in cvtStrToVal() : Unable to cvt to std::complex \"" << strptr << "\""; + throw std::runtime_error(errorStr.str()); + return(0); + + } + chptr = nptr; + } + val = std::complex(rval, ival); + + return(chptr-strptr); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: cvtStrToVal (double) ****/ +/******************************************************************************************/ +int cvtStrToVal(char const* strptr, double& val) +{ + const char *chptr = strptr; + char *nptr; + + val = strtod(chptr, &nptr); + if (nptr == chptr) { + std::stringstream errorStr; + errorStr << "ERROR in cvtStrToVal() : Unable to cvt to double \"" << strptr << "\""; + throw std::runtime_error(errorStr.str()); + return(0); + + } + + return(chptr-strptr); +} +/******************************************************************************************/ + diff --git a/tools/geo_converters/proc_gdal/global_fn.h b/tools/geo_converters/proc_gdal/global_fn.h new file mode 100644 index 0000000..4145b43 --- /dev/null +++ b/tools/geo_converters/proc_gdal/global_fn.h @@ -0,0 +1,47 @@ +/******************************************************************************************/ +/**** FILE: global_fn.h ****/ +/******************************************************************************************/ + +#ifndef GLOBAL_FN_H +#define GLOBAL_FN_H + +#include +#include +#include +#include "global_defines.h" + +int gcd(int a, int b); +void extended_euclid(int a, int b, int& gcd, int& p1, int& p2); +int fgetline(FILE *file, std::string& s, bool keepcr = true); +std::string getField(const std::string& string, int& posn, const char *chdelim); +std::vector split(const std::string &s, char delim); +std::vector splitOptions(const std::string &cmd); +std::vector splitCSV(const std::string &cmd); +int fgetline(FILE *, char *); +int copyFile(const char *src, const char *dest); +int fileExists(const char *filename); +char *remove_quotes(char *str); +std::string remove_quotes(const std::string& str); +char *escape_quotes(char *&str); +const char *getCSVField(const char *str, const char *&startPtr, int& fieldLen, const bool rmWhitsSpaceFlag = false, const char fs = ','); +void set_current_dir_from_file(char *filename); +char *insertFilePfx(const char *pfx, const char *filename); +std::string insertFilePfx(const std::string& pfx, const std::string& filename); +int stringcmp(const char *s1, const char *s2); +void lowercase(char *s1); +void lowercase(std::string& s1); +void uppercase(std::string& s1); +void get_bits(char *str, int n, int num_bits, int insNull = 1); +void get_bits(char *str, LONGLONG_TYPE n, int num_bits, int insNull = 1); +void get_hex(char *str, int n, int num_hex, int insNull = 1); +int is_big_endian(); +bool deleteFile(const char *filename, bool noRecycleBin = true); +bool isFirstInstance(const char *mutexName); +int readTwoCol(double *x, double *y, int& numPts, const char *flname, int maxNumPts, int maxLineSize=500); +int readThreeCol(double *x, double *y, double *z, int& numPts, const char *flname, int maxNumPts, int maxLineSize=500); +void writeTwoCol(const double *x, const double *y, int n, const char *fmt, const char *flname); +void writeOneCol(const double *x, int n, const char *fmt, const char *flname); +int cvtStrToVal(char const* strptr, double& val); +int cvtStrToVal(char const* strptr, std::complex& val); + +#endif diff --git a/tools/geo_converters/proc_gdal/inline_fn.h b/tools/geo_converters/proc_gdal/inline_fn.h new file mode 100644 index 0000000..0fe42af --- /dev/null +++ b/tools/geo_converters/proc_gdal/inline_fn.h @@ -0,0 +1,99 @@ +/******************************************************************************************/ +/**** FILE: inline_fn.h ****/ +/******************************************************************************************/ + +#ifndef INLINE_FN_H +#define INLINE_FN_H + +#include "global_defines.h" +#include +#include +#include + +/******************************************************************************************/ +/**** Inline functions for reading data ****/ +/******************************************************************************************/ +inline void checkStr(const char *varname, int linenum, char *strname, const char *filename) +{ + std::ostringstream errStr; + + if (strcmp(strname, varname) != 0) { + errStr << "ERROR: Invalid file \"" << filename << "\":" << linenum + << " expecting \"" << varname << "\" NOT \"" << strname << "\"" << std::endl; + throw std::runtime_error(errStr.str()); + return; + } +} + +inline void checkStr(const char *varname, int idx, int linenum, char *strname, const char *filename) +{ + char *tmpstr = (char *) malloc(100); + sprintf(tmpstr, "%s_%d", varname, idx); + checkStr(tmpstr, linenum, strname, filename); + free(tmpstr); +} + +inline void checkStr(const char *varname, int idx1, int idx2, int linenum, char *strname, const char *filename) +{ + char *tmpstr = (char *) malloc(100); + sprintf(tmpstr, "%s_%d_%d", varname, idx1, idx2); + checkStr(tmpstr, linenum, strname, filename); + free(tmpstr); +} + +inline void getParamVal(int &ivar, const char *varname, int linenum, char *strname, char *strval, const char *filename) +{ + char *chptr; + std::ostringstream errStr; + + checkStr(varname, linenum, strname, filename); + + chptr = strtok(strval, CHDELIM); + if (chptr) { + ivar = atoi(chptr); + } else { + errStr << "ERROR: Invalid file \"" << filename << "\":" << linenum + << " variable \"" << varname << "\" not specified" << std::endl; + throw std::runtime_error(errStr.str()); + return; + } +} + +inline void getParamVal(double &dvar, const char *varname, int linenum, char *strname, char *strval, const char *filename) +{ + char *chptr; + std::ostringstream errStr; + + checkStr(varname, linenum, strname, filename); + + chptr = strtok(strval, CHDELIM); + if (chptr) { + dvar = atof(chptr); + } else { + errStr << "ERROR: Invalid file \"" << filename << "\":" << linenum + << " variable \"" << varname << "\" not specified" << std::endl; + throw std::runtime_error(errStr.str()); + return; + } +} + +inline void getParamVal(char *&svar, const char *varname, int linenum, char *strname, char *strval, const char *filename) +{ + char *chptr; + std::ostringstream errStr; + + checkStr(varname, linenum, strname, filename); + + chptr = strtok(strval, CHDELIM); + if (chptr) { + svar = strdup(chptr); + } else { + errStr << "ERROR: Invalid file \"" << filename << "\":" << linenum + << " variable \"" << varname << "\" not specified" << std::endl; + throw std::runtime_error(errStr.str()); + return; + } +} +/******************************************************************************************/ + +#endif diff --git a/tools/geo_converters/proc_gdal/main.cpp b/tools/geo_converters/proc_gdal/main.cpp new file mode 100644 index 0000000..2f5f688 --- /dev/null +++ b/tools/geo_converters/proc_gdal/main.cpp @@ -0,0 +1,127 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "data_set.h" + +void set_options(int argc, char **argv, std::string& templateFile); + +/******************************************************************************************/ +int main(int argc, char **argv) +{ + char hostname[HOST_NAME_MAX]; + char username[LOGIN_NAME_MAX]; + gethostname(hostname, HOST_NAME_MAX); + getlogin_r(username, LOGIN_NAME_MAX); + + std::cout << "Running on " << hostname << " USER = " << username << std::endl; + + char *tstr; + + time_t t1 = time(NULL); + tstr = strdup(ctime(&t1)); + strtok(tstr, "\n"); + std::cout << tstr << " : Beginning ANALYSIS." << std::endl; + free(tstr); + + // feenableexcept(FE_INVALID | FE_OVERFLOW); + + std::string templateFile; + set_options(argc, argv, templateFile); + std::cout << "TEMPLATE FILE = " << templateFile << std::endl; + + DataSetClass *dataSet = new DataSetClass(); + + try { + dataSet->parameterTemplate.readFile(templateFile.c_str()); + dataSet->parameterTemplate.print(stdout); + + if (dataSet->parameterTemplate.seed == 0) { + struct timespec tseed; + clock_gettime(CLOCK_REALTIME, &tseed); + unsigned int seed = (unsigned int) ((((unsigned int) tseed.tv_sec)*1000000000 + tseed.tv_nsec)&0xFFFFFFFF); + std::cout << "SEED GENERATED FROM clock_gettime() = " << seed << std::endl; + srand(seed); + } else { + srand(dataSet->parameterTemplate.seed); + } + dataSet->run(); + } + catch (std::exception &e) { + std::cout << e.what() << std::endl; + } + + delete dataSet; + + time_t t2 = time(NULL); + tstr = strdup(ctime(&t2)); + strtok(tstr, "\n"); + std::cout << tstr << " : Completed ANALYSIS." << std::endl; + free(tstr); + + int elapsedTime = (int) (t2-t1); + + int et = elapsedTime; + int elapsedTimeSec = et % 60; + et = et / 60; + int elapsedTimeMin = et % 60; + et = et / 60; + int elapsedTimeHour = et % 24; + et = et / 24; + int elapsedTimeDay = et; + + std::cout << "Elapsed time = " << (t2-t1) << " sec = " + << elapsedTimeDay << " days " + << elapsedTimeHour << " hours " + << elapsedTimeMin << " min " + << elapsedTimeSec << " sec." << std::endl; + + return(1); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/*** Set all variables defined by command line options. ******/ +void set_options(int argc, char **argv, std::string& templateFile) +{ + static const char *help_msg[] = { + " -templ --file parameter template file", + " -h --help print this help message", + " ", + 0}; + static const char *usage[] = { + " [ -option value] [ -h ]", + 0}; + char *name = *argv; + const char **p = help_msg; + const char **u = usage; + while ( --argc > 0 ) { + argv++; + if (strcmp(*argv,"-templ") ==0) { templateFile = std::string(*++argv); argc--; } + + else if (strcmp(*argv,"-h")==0) + { fprintf(stdout, "\n\n"); + fprintf(stdout, "usage:\n%s", name); + while (*u) fprintf(stdout, "%s\n", *u++); + fprintf(stdout, "\n"); + while (*p) fprintf(stdout, "%s\n", *p++); + exit(1); } + else + { fprintf(stderr, "\n\n%s Invalid Option: %s \n", name, *argv); + fprintf(stderr, "\n\n"); + fprintf(stderr, "usage:\n%s", name); + while (*u) fprintf(stderr, "%s\n", *u++); + fprintf(stderr, "\n"); + exit(1); } + } +} +/******************************************************************************************/ + diff --git a/tools/geo_converters/proc_gdal/param_proc.cpp b/tools/geo_converters/proc_gdal/param_proc.cpp new file mode 100644 index 0000000..b6732b8 --- /dev/null +++ b/tools/geo_converters/proc_gdal/param_proc.cpp @@ -0,0 +1,240 @@ +/******************************************************************************************/ +/**** PROGRAM: param_proc.cpp ****/ +/******************************************************************************************/ + +#include +#include +#include +#include + +#include "global_fn.h" +#include "param_proc.h" + +/******************************************************************************************/ +/**** FUNCTION: ParamProcClass::ParamProcClass ****/ +/******************************************************************************************/ +ParamProcClass::ParamProcClass(std::string p_filename, std::string p_filetype) { + filename = p_filename; + filetype = p_filetype; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ParamProcClass::~ParamProcClass ****/ +/******************************************************************************************/ +ParamProcClass::~ParamProcClass() { +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ParamProcClass::getParamVal (std::string versions) ****/ +/******************************************************************************************/ +void ParamProcClass::getParamVal(bool &bvar, const std::string& varname, int linenum, const std::string& strname, const std::string& strval) +{ + int posn = 0; + std::string fieldVal; + std::ostringstream s; + + if (strname != varname) { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "Expecting \"" << varname << "\" NOT \"" << strname << "\""; + throw std::runtime_error(s.str()); + return; + } + fieldVal = getField(strval, posn, CHDELIM); + if (!fieldVal.empty()) { + if (fieldVal == "true") { + bvar = true; + } else if (fieldVal == "false") { + bvar = false; + } else { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "Variable \"" << varname << "\" set to illegal bool value \"" << fieldVal << "\""; + throw std::runtime_error(s.str()); + } + } else { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "No \"" << varname << "\" specified"; + throw std::runtime_error(s.str()); + return; + } +} + +void ParamProcClass::getParamVal(int &ivar, const std::string& varname, int linenum, const std::string& strname, const std::string& strval) +{ + int posn = 0; + std::string fieldVal; + std::ostringstream s; + + if (strname != varname) { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "Expecting \"" << varname << "\" NOT \"" << strname << "\""; + throw std::runtime_error(s.str()); + return; + } + fieldVal = getField(strval, posn, CHDELIM); + if (!fieldVal.empty()) { + ivar = atoi(fieldVal.c_str()); + } else { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "No \"" << varname << "\" specified"; + throw std::runtime_error(s.str()); + return; + } +} + + +#if 0 +void ParamProcClass::getParamVal(int &ivar, int idx, const char *varname, int linenum, char *strname, char *strval) +{ + sprintf(tmpstr, "%s_%d", varname, idx); + getParamVal(ivar, tmpstr, linenum, strname, strval); +} +#endif + +void ParamProcClass::getParamVal(double &dvar, const std::string& varname, int linenum, const std::string& strname, const std::string& strval) +{ + int posn = 0; + std::string fieldVal; + std::ostringstream s; + + if (strname != varname) { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "Expecting \"" << varname << "\" NOT \"" << strname << "\""; + throw std::runtime_error(s.str()); + return; + } + fieldVal = getField(strval, posn, CHDELIM); + if (!fieldVal.empty()) { + dvar = atof(fieldVal.c_str()); + } else { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "No \"" << varname << "\" specified"; + throw std::runtime_error(s.str()); + return; + } +} + +void ParamProcClass::getParamVal(double &dvar, int idx, const std::string& varname, int linenum, const std::string& strname, const std::string& strval) +{ + std::ostringstream s; + s << varname << "_" << idx; + getParamVal(dvar, s.str(), linenum, strname, strval); +} + +void ParamProcClass::getParamVal(char* &svar, const std::string& varname, int linenum, const std::string& strname, const std::string& strval) +{ + int posn = 0; + std::string fieldVal; + std::ostringstream s; + + if (strname != varname) { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "Expecting \"" << varname << "\" NOT \"" << strname << "\""; + throw std::runtime_error(s.str()); + return; + } + fieldVal = remove_quotes(strval); + if (svar) { free(svar); } + if (fieldVal != strval) { // Double quotes were removed + svar = strdup(fieldVal.c_str()); + } else { + fieldVal = getField(strval, posn, CHDELIM); + if (fieldVal == "NULL") { + svar = (char *) NULL; + } else { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "Invalid double-quoted string specified for " << varname; + throw std::runtime_error(s.str()); + return; + } + } +} + +#if 0 +void ParamProcClass::getParamVal(char* &svar, int idx, const char *varname, int linenum, char *strname, char *strval) +{ + sprintf(tmpstr, "%s_%d", varname, idx); + getParamVal(svar, tmpstr, linenum, strname, strval); +} +#endif + +void ParamProcClass::getParamVal(std::string &svar, const std::string& varname, int linenum, const std::string& strname, const std::string& strval) +{ + int posn = 0; + std::string fieldVal; + std::ostringstream s; + + if (strname != varname) { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "Expecting \"" << varname << "\" NOT \"" << strname << "\""; + throw std::runtime_error(s.str()); + return; + } + fieldVal = remove_quotes(strval); + if (fieldVal != strval) { // Double quotes were removed + svar = fieldVal; + } else { + fieldVal = getField(strval, posn, CHDELIM); + if (fieldVal == "NULL") { + svar.clear(); + } else { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "Invalid double-quoted string specified for " << varname; + throw std::runtime_error(s.str()); + return; + } + } +} + +void ParamProcClass::getParamVal(std::complex &cvar, const std::string& varname, int linenum, const std::string& strname, const std::string& strval) +{ + std::string fieldVal; + std::ostringstream s; + + if (strname != varname) { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "Expecting \"" << varname << "\" NOT \"" << strname << "\""; + throw std::runtime_error(s.str()); + return; + } + if (!strval.empty()) { + cvtStrToVal(strval.c_str(), cvar); + } else { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "No \"" << varname << "\" specified"; + throw std::runtime_error(s.str()); + return; + } +} + +void ParamProcClass::getParamVal(std::complex &cvar, int idx, const std::string& varname, int linenum, const std::string& strname, const std::string& strval) +{ + std::ostringstream s; + s << varname << "_" << idx; + getParamVal(cvar, s.str(), linenum, strname, strval); +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** FUNCTION: ParamProcClass::checkStr ****/ +/******************************************************************************************/ +void ParamProcClass::checkStr(const std::string& varname, int linenum, const std::string& strname) +{ + std::ostringstream s; + + if (strname != varname) { + s << "ERROR: Invalid " << filetype << " file \"" << filename << "(" << linenum << ")\"" << std::endl + << "Expecting \"" << varname << "\" NOT \"" << strname << "\""; + throw std::runtime_error(s.str()); + return; + } +} +void ParamProcClass::checkStr(const std::string& varname, int idx, int linenum, const std::string& strname) +{ + std::ostringstream tmpStream; + tmpStream << varname << "_" << idx; + checkStr(tmpStream.str(), linenum, strname); +} +/******************************************************************************************/ + diff --git a/tools/geo_converters/proc_gdal/param_proc.h b/tools/geo_converters/proc_gdal/param_proc.h new file mode 100644 index 0000000..995acb9 --- /dev/null +++ b/tools/geo_converters/proc_gdal/param_proc.h @@ -0,0 +1,45 @@ +/******************************************************************************************/ +/**** FILE: param_proc.h ****/ +/******************************************************************************************/ + +#ifndef PARAM_PROC_H +#define PARAM_PROC_H + +#include +#include + +/******************************************************************************************/ +/**** CLASS: ParamProcClass ****/ +/******************************************************************************************/ +class ParamProcClass +{ +public: + ParamProcClass(std::string p_filename, std::string p_filetype); + ~ParamProcClass(); + + /**************************************************************************************/ + /* Use std::string */ + /**************************************************************************************/ + void getParamVal(bool &bvar, const std::string& varname, int linenum, const std::string& strname, const std::string& strval); + void getParamVal(int &ivar, const std::string& varname, int linenum, const std::string& strname, const std::string& strval); + // void getParamVal(int &ivar, int idx, const char *varname, int linenum, char *strname, char *strval); + void getParamVal(double &dvar, const std::string& varname, int linenum, const std::string& strname, const std::string& strval); + void getParamVal(double &dvar, int idx, const std::string& varname, int linenum, const std::string& strname, const std::string& strval); + void getParamVal(char* &svar, const std::string& varname, int linenum, const std::string& strname, const std::string& strval); + // void getParamVal(char* &svar, int idx, const char *varname, int linenum, char *strname, char *strval); + void getParamVal(std::string &svar, const std::string& varname, int linenum, const std::string& strname, const std::string& strval); + void getParamVal(std::complex &cvar, const std::string& varname, int linenum, const std::string& strname, const std::string& strval); + void getParamVal(std::complex &cvar, int idx, const std::string& varname, int linenum, const std::string& strname, const std::string& strval); + + void checkStr(const std::string& varname, int linenum, const std::string& strname); + void checkStr(const std::string& varname, int idx, int linenum, const std::string& strname); + /**************************************************************************************/ + +private: + int *error_state_ptr; + std::string filename; + std::string filetype; +}; +/******************************************************************************************/ + +#endif diff --git a/tools/geo_converters/proc_gdal/parameter_template.cpp b/tools/geo_converters/proc_gdal/parameter_template.cpp new file mode 100644 index 0000000..24bfb29 --- /dev/null +++ b/tools/geo_converters/proc_gdal/parameter_template.cpp @@ -0,0 +1,357 @@ +/******************************************************************************************/ +/**** Automatically Generated file, DO NOT EDIT. ****/ +/**** CPP file generated by gen_parameter_template.pl. ****/ +/**** FILE: "parameter_template.cpp" ****/ +/******************************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include + +#include "parameter_template.h" +#include "global_defines.h" +#include "global_fn.h" +#include "param_proc.h" + +#include "inline_fn.h" + +/******************************************************************************************/ +/**** CONSTRUCTOR: ParameterTemplateClass::ParameterTemplateClass ****/ +/******************************************************************************************/ +ParameterTemplateClass::ParameterTemplateClass() +{ + name = "test"; + function = "combine3D2D"; + srcFile3D.clear(); + srcFile2D.clear(); + srcFileRaster.clear(); + srcFileVector.clear(); + srcHeightFieldName.clear(); + heightFieldName3D.clear(); + heightFieldName2D.clear(); + outputHeightFieldName.clear(); + outputFile.clear(); + outputLayer.clear(); + nodataVal = 1.0e30; + clampMin = -100.0; + clampMax = 5000.0; + minMag = 0.0; + tmpImageFile.clear(); + imageFile.clear(); + imageFile2.clear(); + imageLonLatRes = 0.0001; + verbose = 0; + seed = 0; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** DESTRUCTOR: ParameterTemplateClass::~ParameterTemplateClass ****/ +/******************************************************************************************/ +ParameterTemplateClass::~ParameterTemplateClass() +{ +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** ParameterTemplateClass::readFile() ****/ +/******************************************************************************************/ +void ParameterTemplateClass::readFile(const char *filename) +{ + int linenum; + char *str1; + char *line = (char *) malloc(10000*sizeof(char)); + char *format_str = (char *) NULL; + FILE *fp = (FILE *) NULL; + std::ostringstream errStr; + + if (!filename) { + errStr << "ERROR: No template file specified." << std::endl; + throw std::runtime_error(errStr.str()); + } + + fp = fopen(filename, "rb"); + if (!fp) { + errStr << "ERROR: Unable to open template file \"" << filename << "\" for reading." << std::endl; + throw std::runtime_error(errStr.str()); + } + +#if CDEBUG + printf("Reading template file: \"%s\"\n", filename); +#endif + + enum StateEnum { + STATE_FORMAT, + STATE_READ_VERSION + }; + + StateEnum state; + + state = STATE_FORMAT; + linenum = 0; + + while ( (state != STATE_READ_VERSION) && fgetline(fp, line) ) { + linenum++; + str1 = strtok(line, CHDELIM); + if ( str1 && (str1[0] != '#') ) { + switch(state) { + case STATE_FORMAT: + if (strcmp(str1, "FORMAT:") != 0) { + errStr << "ERROR: Invalid template file \"" << filename << "\":" << linenum + << " expecting \"FORMAT:\" NOT \"" << str1 << "\"" << std::endl; + throw std::runtime_error(errStr.str()); + } else { + str1 = strtok(NULL, CHDELIM); + format_str = strdup(str1); + } + state = STATE_READ_VERSION; + break; + default: + errStr << "ERROR: Invalid template file \"" << filename << "\"" << std::endl; + throw std::runtime_error(errStr.str()); + break; + } + } + } + + if (state != STATE_READ_VERSION) { + errStr << "ERROR: Invalid template file \"" << filename << "\":" << linenum + << " premature end of file encountered" << std::endl; + throw std::runtime_error(errStr.str()); + } + + if (strcmp(format_str,"1_0")==0) { + readFile_1_0(fp, line, filename, linenum); + } else { + errStr << "ERROR: Invalid template file \"" << filename << "\"" + << " format set to illegal value \"" << format_str << "\"" << std::endl; + throw std::runtime_error(errStr.str()); + } + + free(line); + if (format_str) { free(format_str); } + + if (fp) { fclose(fp); } +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** ParameterTemplateClass::readFile_1_0() ****/ +/******************************************************************************************/ +void ParameterTemplateClass::readFile_1_0(FILE *fp, char *line, const char *filename, int linenum) +{ + int paramIdx, numParam; + double scale, dval; + bool found; + int valid; + char *str1, *str2; + char *paramName, *paramVal, *paramUnit; + std::ostringstream errStr; + + ParamProcClass *paramProc = new ParamProcClass(filename, "Template File"); + +#if CDEBUG + printf("Reading template file format 1.0: \"%s\"\n", filename); +#endif + + enum StateEnum { + STATE_NUM_PARAM, + STATE_PARAM, + + STATE_DONE + }; + + StateEnum state; + + state = STATE_NUM_PARAM; + + while (fgetline(fp, line)) { +#if 0 + printf("%s", line); +#endif + linenum++; + str1 = strtok(line, CHDELIM ":"); + if ( str1 && (str1[0] != '#') ) { + str2 = strtok(NULL, "\n"); + while((str2) && (*str2 == ' ')) { str2++; } + switch(state) { + case STATE_NUM_PARAM: + paramProc->getParamVal(numParam, "NUM_PARAM", linenum, str1, str2); + if (numParam) { + paramIdx = 0; + state = STATE_PARAM; + } else { + state = STATE_DONE; + } + break; + case STATE_PARAM: + checkStr("PARAM", paramIdx, linenum, str1, filename); + paramName = strtok(str2, CHDELIM); + paramVal = strtok(NULL, "\n"); + found = false; + valid = true; + + if ((!found) && (strcmp(paramName, "NAME")==0)) { + paramProc->getParamVal(name, "NAME", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "FUNCTION")==0)) { + paramProc->getParamVal(function, "FUNCTION", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "SRC_FILE_3D")==0)) { + paramProc->getParamVal(srcFile3D, "SRC_FILE_3D", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "SRC_FILE_2D")==0)) { + paramProc->getParamVal(srcFile2D, "SRC_FILE_2D", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "SRC_FILE_RASTER")==0)) { + paramProc->getParamVal(srcFileRaster, "SRC_FILE_RASTER", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "SRC_FILE_VECTOR")==0)) { + paramProc->getParamVal(srcFileVector, "SRC_FILE_VECTOR", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "SRC_HEIGHT_FIELD_NAME")==0)) { + paramProc->getParamVal(srcHeightFieldName, "SRC_HEIGHT_FIELD_NAME", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "HEIGHT_FIELD_NAME_3D")==0)) { + paramProc->getParamVal(heightFieldName3D, "HEIGHT_FIELD_NAME_3D", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "HEIGHT_FIELD_NAME_2D")==0)) { + paramProc->getParamVal(heightFieldName2D, "HEIGHT_FIELD_NAME_2D", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "OUTPUT_HEIGHT_FIELD_NAME")==0)) { + paramProc->getParamVal(outputHeightFieldName, "OUTPUT_HEIGHT_FIELD_NAME", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "OUTPUT_FILE")==0)) { + paramProc->getParamVal(outputFile, "OUTPUT_FILE", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "OUTPUT_LAYER")==0)) { + paramProc->getParamVal(outputLayer, "OUTPUT_LAYER", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "NODATA_VAL")==0)) { + paramProc->getParamVal(nodataVal, "NODATA_VAL", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "CLAMP_MIN")==0)) { + paramProc->getParamVal(clampMin, "CLAMP_MIN", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "CLAMP_MAX")==0)) { + paramProc->getParamVal(clampMax, "CLAMP_MAX", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "MIN_MAG")==0)) { + paramProc->getParamVal(minMag, "MIN_MAG", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "TMP_IMAGE_FILE")==0)) { + paramProc->getParamVal(tmpImageFile, "TMP_IMAGE_FILE", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "IMAGE_FILE")==0)) { + paramProc->getParamVal(imageFile, "IMAGE_FILE", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "IMAGE_FILE_2")==0)) { + paramProc->getParamVal(imageFile2, "IMAGE_FILE_2", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "IMAGE_LON_LAT_RES")==0)) { + paramProc->getParamVal(imageLonLatRes, "IMAGE_LON_LAT_RES", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "VERBOSE")==0)) { + paramProc->getParamVal(verbose, "VERBOSE", linenum, paramName, paramVal); + found = true; + } + if ((!found) && (strcmp(paramName, "SEED")==0)) { + paramProc->getParamVal(seed, "SEED", linenum, paramName, paramVal); + found = true; + } + + if (!found) { + errStr << "ERROR: Invalid template file \"" << filename << "\":" << linenum + << ", invalid parameter name \"" << paramName << "\"" << std::endl; + throw std::runtime_error(errStr.str()); + } + if (!valid) { + errStr << "ERROR: Invalid template file \"" << filename << "\":" << linenum + << ", invalid parameter value \"" << paramName << "\" = \"" << paramVal << "\"" << std::endl; + throw std::runtime_error(errStr.str()); + } + paramIdx++; + if (paramIdx == numParam) { + state = STATE_DONE; + } + break; + default: + errStr << "ERROR: Invalid template file \"" << filename << "\":" << linenum + << ", invalid state encountered." << std::endl; + throw std::runtime_error(errStr.str()); + break; + } + } + } + + if (state != STATE_DONE) { + errStr << "ERROR: Invalid template file \"" << filename << "\":" << linenum + << " premature end of file encountered" << std::endl; + throw std::runtime_error(errStr.str()); + } + + delete paramProc; + + return; +} +/******************************************************************************************/ + +/******************************************************************************************/ +/**** ParameterTemplateClass::print() ****/ +/******************************************************************************************/ +void ParameterTemplateClass::print(FILE *fp) const +{ + fprintf(fp, "NAME: %s\n", (name.empty() ? "NONE" : name.c_str())); + fprintf(fp, "FUNCTION: %s\n", (function.empty() ? "NONE" : function.c_str())); + fprintf(fp, "SRC_FILE_3D: %s\n", (srcFile3D.empty() ? "NONE" : srcFile3D.c_str())); + fprintf(fp, "SRC_FILE_2D: %s\n", (srcFile2D.empty() ? "NONE" : srcFile2D.c_str())); + fprintf(fp, "SRC_FILE_RASTER: %s\n", (srcFileRaster.empty() ? "NONE" : srcFileRaster.c_str())); + fprintf(fp, "SRC_FILE_VECTOR: %s\n", (srcFileVector.empty() ? "NONE" : srcFileVector.c_str())); + fprintf(fp, "SRC_HEIGHT_FIELD_NAME: %s\n", (srcHeightFieldName.empty() ? "NONE" : srcHeightFieldName.c_str())); + fprintf(fp, "HEIGHT_FIELD_NAME_3D: %s\n", (heightFieldName3D.empty() ? "NONE" : heightFieldName3D.c_str())); + fprintf(fp, "HEIGHT_FIELD_NAME_2D: %s\n", (heightFieldName2D.empty() ? "NONE" : heightFieldName2D.c_str())); + fprintf(fp, "OUTPUT_HEIGHT_FIELD_NAME: %s\n", (outputHeightFieldName.empty() ? "NONE" : outputHeightFieldName.c_str())); + fprintf(fp, "OUTPUT_FILE: %s\n", (outputFile.empty() ? "NONE" : outputFile.c_str())); + fprintf(fp, "OUTPUT_LAYER: %s\n", (outputLayer.empty() ? "NONE" : outputLayer.c_str())); + fprintf(fp, "NODATA_VAL: %15.10e\n", nodataVal); + fprintf(fp, "CLAMP_MIN (m): %15.10e\n", clampMin); + fprintf(fp, "CLAMP_MAX (m): %15.10e\n", clampMax); + fprintf(fp, "MIN_MAG (m): %15.10e\n", minMag); + fprintf(fp, "TMP_IMAGE_FILE: %s\n", (tmpImageFile.empty() ? "NONE" : tmpImageFile.c_str())); + fprintf(fp, "IMAGE_FILE: %s\n", (imageFile.empty() ? "NONE" : imageFile.c_str())); + fprintf(fp, "IMAGE_FILE_2: %s\n", (imageFile2.empty() ? "NONE" : imageFile2.c_str())); + fprintf(fp, "IMAGE_LON_LAT_RES (deg): %15.10e\n", imageLonLatRes); + fprintf(fp, "VERBOSE: %d\n", verbose); + fprintf(fp, "SEED: %d\n", seed); + fprintf(fp, "\n"); +} +/******************************************************************************************/ + + +/******************************************************************************************/ +/**** END of CPP file generated by gen_parameter_template.pl ****/ +/******************************************************************************************/ diff --git a/tools/geo_converters/proc_gdal/parameter_template.h b/tools/geo_converters/proc_gdal/parameter_template.h new file mode 100644 index 0000000..40eb509 --- /dev/null +++ b/tools/geo_converters/proc_gdal/parameter_template.h @@ -0,0 +1,52 @@ +/******************************************************************************************/ +/**** Automatically Generated file, DO NOT EDIT. ****/ +/**** HEADER file generated by gen_parameter_template.pl. ****/ +/**** FILE: "parameter_template.h" ****/ +/******************************************************************************************/ + +#ifndef PARAMETER_TEMPLATE_H +#define PARAMETER_TEMPLATE_H + +/******************************************************************************************/ +/**** CLASS: ParameterTemplateClass ****/ +/******************************************************************************************/ +class ParameterTemplateClass +{ +public: + ParameterTemplateClass(); + ~ParameterTemplateClass(); + void readFile(const char *filename); + void print(FILE *fp) const; + std::string name; + std::string function; + std::string srcFile3D; + std::string srcFile2D; + std::string srcFileRaster; + std::string srcFileVector; + std::string srcHeightFieldName; + std::string heightFieldName3D; + std::string heightFieldName2D; + std::string outputHeightFieldName; + std::string outputFile; + std::string outputLayer; + double nodataVal; + double clampMin; + double clampMax; + double minMag; + std::string tmpImageFile; + std::string imageFile; + std::string imageFile2; + double imageLonLatRes; + int verbose; + int seed; + +private: + void readFile_1_0(FILE *fp, char *line, const char *filename, int linenum); +}; +/******************************************************************************************/ + +#endif + +/******************************************************************************************/ +/**** END of HEADER file generated by gen_parameter_template.pl ****/ +/******************************************************************************************/ diff --git a/tools/geo_converters/proc_gdal/proc_gdal b/tools/geo_converters/proc_gdal/proc_gdal new file mode 100755 index 0000000..6ba9250 Binary files /dev/null and b/tools/geo_converters/proc_gdal/proc_gdal differ diff --git a/tools/geo_converters/proc_gdal/proc_gdal.pro b/tools/geo_converters/proc_gdal/proc_gdal.pro new file mode 100644 index 0000000..0d68082 --- /dev/null +++ b/tools/geo_converters/proc_gdal/proc_gdal.pro @@ -0,0 +1,25 @@ +###################################################################### +# Automatically generated by qmake (2.01a) Wed Feb 8 15:29:23 2012 +###################################################################### + +TEMPLATE = app +TARGET = proc_gdal +DEPENDPATH += . +INCLUDEPATH += . /usr/include/gdal +CONFIG += release +CONFIG -= QT +QT -= core gui + +QMAKE_CXXFLAGS_WARN_ON += -Werror=format-extra-args +QMAKE_CXXFLAGS_WARN_ON += -Werror=format +QMAKE_CXXFLAGS_WARN_ON += -Werror=shadow +QMAKE_CXXFLAGS_WARN_ON += -Werror=return-type +QMAKE_CXXFLAGS += -std=gnu++11 +QMAKE_LIBS += -lgdal + +# DEFINES += QT_NO_DEBUG_OUTPUT + +# Input +HEADERS += $$files(*.h) + +SOURCES += $$files(*.cpp) diff --git a/tools/geo_converters/tiler.py b/tools/geo_converters/tiler.py new file mode 100755 index 0000000..e04736e --- /dev/null +++ b/tools/geo_converters/tiler.py @@ -0,0 +1,531 @@ +#!/usr/bin/env python3 +# Tool for tiling geospatial image files (wrapper around gdal_translate) + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=unused-wildcard-import, invalid-name, too-few-public-methods +# pylint: disable=broad-exception-caught, too-many-arguments, wildcard-import +# pylint: disable=too-many-statements, too-many-branches, too-many-locals +# pylint: disable=too-many-return-statements + +import argparse +import datetime +import enum +import glob +import multiprocessing +import multiprocessing.pool +import os +import shlex +import signal +import sys +from typing import List, NamedTuple, Optional, Set + +from geoutils import * + +_EPILOG = """This script expects that GDAL utilities are installed and in PATH. +Also it expects that as part of this installation proj/proj.db is somehow +installed as well (if it is, but gdalwarp still complains - set PROJ_LIB +environment variable to point to this proj directory). + +Some examples: +- Tile NLCD files in nlcd/production to TILED_NLCD_PROD, using 8 CPUs. + Tile names are like usa_lc_prd_n40w90.tif + $ tiler.py --threads 8 --tile_pattern \ + TILED_NLCD_PROD/usa_lc_prd_{lat_hem}{lat_u:02}{lon_hem}{lon_l03}.tif \\ + nlcd/production/*.tif +""" + +# Conversion result status +ConvStatus = enum.Enum("ConvStatus", ["Success", "Exists", "Dropped", "Error"]) + + +class ConvResult(NamedTuple): + """ Conversion result """ + + # Name of tile + tilename: str + + # Conversion status + status: ConvStatus + + # Conversion duration + duration: datetime.timedelta + + # Command lines + command_lines: Optional[List[str]] = None + + # Optional error message + msg: Optional[str] = None + + +def tile_creator(tile_pattern: str, sources: List[str], top: int, left: int, + margin: int, pixel_size_lat: float, pixel_size_lon: float, + scale: Optional[List[float]], no_data: Optional[str], + data_type: Optional[str], resampling: str, overwrite: bool, + remove_values: Optional[List[float]], + out_format: Optional[str], format_params: List[str], + verbose: bool) -> ConvResult: + """ Worker function that creates tile + + Arguments: + tile_pattern -- Tile file name pattern + sources -- List of source files (maybe empty) + top -- Top latitude + left -- Left longitude + margin -- Number of margin pixels + pixel_size_lat -- Pixel size in latitudinal direction + pixel_size_lon -- Pixel size in longitudinal direction + scale -- Optional scale as [src_min,src_max,dst_min, dst_max] + no_data -- Optional NoData value + data_type -- Optional pixel data type + pixel_size_lat -- Pixel size in latitudinal direction + pixel_size_lon -- Pixel size in longitudinal direction + resampling -- Resampling method + overwrite -- True to overwrite existing files, False to skip + remove_values -- Optional list of values values in monochrome tiles to + drop + out_format -- Optional output format + format_params -- Optional output format parameters + verbose -- Print all output, fail on first failure + Returns ConvResult object + """ + while left < -180: + left += 360 + while left >= 180: + left -= 360 + + temp_filename: Optional[str] = None + temp_filename_vrt: Optional[str] = None + temp_filename_xml: Optional[str] = None + try: + tile_filename = \ + tile_pattern.format(lat_u=abs(top), lat_d=abs(top - 1), + lon_l=abs(left), lon_r=abs(left + 1), + lat_hem='n' if top > 0 else 's', + LAT_HEM='N' if top > 0 else 'S', + lon_hem='e' if left >= 0 else 'w', + LON_HEM='E' if left >= 0 else 'W') + temp_filename = os.path.splitext(tile_filename)[0] + ".incomplete" + \ + os.path.splitext(tile_filename)[1] + temp_filename_vrt = \ + os.path.splitext(tile_filename)[0] + ".incomplete.vrt" + temp_filename_xml = temp_filename + ".aux.xml" + start_time = datetime.datetime.now() + if os.path.isfile(tile_filename) and (not overwrite): + return ConvResult(tilename=tile_filename, status=ConvStatus.Exists, + duration=datetime.datetime.now() - start_time) + + # Preparing source - only source or vrt of all sources + command_lines: List[str] = [] + if len(sources) > 1: + vrt_args = ["gdalbuildvrt", "-ignore_srcmaskband", + "-r", resampling, temp_filename_vrt] + sources[::-1] + command_lines.append( + " ".join(shlex.quote(arg) for arg in vrt_args)) + exec_result = execute(vrt_args, env=gdal_env(), + disable_output=True, return_error=True, + fail_on_error=False) + if exec_result is not None: + assert isinstance(exec_result, str) + return ConvResult( + tilename=tile_filename, + status=ConvStatus.Error, + duration=datetime.datetime.now() - + start_time, + msg=exec_result, + command_lines=command_lines) + src = temp_filename_vrt + else: + src = sources[0] + + # Translate from source to tile + trans_args = \ + ["gdal_translate", "-strict", "-r", resampling, "-projwin", + str(left - pixel_size_lon * margin), + str(top + pixel_size_lat * margin), + str(left + 1 + pixel_size_lon * margin), + str(top - 1 - pixel_size_lat * margin)] + if out_format: + trans_args += ["-of", out_format] + for fp in format_params: + trans_args += ["-co", fp] + if scale: + trans_args += ["-scale"] + [str(s) for s in scale] + if no_data: + trans_args += ["-a_nodata", no_data] + if data_type: + trans_args += ["-ot", data_type] + trans_args += [src, temp_filename] + command_lines.append(" ".join(shlex.quote(arg) for arg in trans_args)) + exec_result = execute(trans_args, env=gdal_env(), + disable_output=not verbose, + return_error=not verbose, fail_on_error=verbose) + + if verbose: + assert exec_result is True + elif exec_result is not None: + assert isinstance(exec_result, str) + return ConvResult(tilename=tile_filename, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg=exec_result, command_lines=command_lines) + gi = GdalInfo(temp_filename, options=["-stats"], fail_on_error=False) + if not gi: + return ConvResult(tilename=tile_filename, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg="gdalinfo inspection failed", + command_lines=command_lines) + if os.path.isfile(tile_filename): + if verbose: + print(f"Removing '{tile_filename}'") + os.unlink(tile_filename) + if gi.valid_percent == 0: + return \ + ConvResult(tilename=tile_filename, status=ConvStatus.Dropped, + duration=datetime.datetime.now() - start_time, + msg="All pixels are NoData", + command_lines=command_lines) + if (gi.min_value is not None) and (gi.min_value == gi.max_value) and \ + (gi.min_value in (remove_values or [])): + return \ + ConvResult( + tilename=tile_filename, status=ConvStatus.Dropped, + duration=datetime.datetime.now() - start_time, + msg=f"All valid pixels are equal to {gi.min_value}", + command_lines=command_lines) + if verbose: + print(f"Renaming '{temp_filename}' to '{tile_filename}'") + os.rename(temp_filename, tile_filename) + + return ConvResult(tilename=tile_filename, status=ConvStatus.Success, + duration=datetime.datetime.now() - start_time, + command_lines=command_lines) + except (Exception, KeyboardInterrupt, SystemExit) as ex: + return ConvResult(tilename=tile_filename, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg=repr(ex)) + finally: + for filename in (temp_filename, temp_filename_xml, temp_filename_vrt): + try: + if filename and os.path.isfile(filename): + os.unlink(filename) + except OSError: + pass + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + + argument_parser = argparse.ArgumentParser( + description="Cut geospatial image to 1x1 tiles (wrapper around " + "gdal_translate)", + formatter_class=argparse.RawDescriptionHelpFormatter, epilog=_EPILOG) + argument_parser.add_argument( + "--pixel_size", metavar="DEGREES", type=float, + help="Resulting file resolution in pixel size (expressed in degrees). " + "Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--pixels_per_degree", metavar="NUMBER", type=float, + help="Resulting file resolution in pixels per degree. Default is to " + "use one from 'gdalinfo'") + argument_parser.add_argument( + "--top", metavar="MAX_LATITUDE", type=float, + help="Maximum latitude") + argument_parser.add_argument( + "--bottom", metavar="MIN_LATITUDE", type=float, + help="Minimum latitude") + argument_parser.add_argument( + "--left", metavar="MIN_LONGITUDE", type=float, + help="Minimum longitude. Should be provided with --right or not at " + "all") + argument_parser.add_argument( + "--right", metavar="MAX_LONGITUDE", type=float, + help="Maximum longitude. Should be provided with --left or not at all") + argument_parser.add_argument( + "--margin", metavar="MARGIN_PIXELS", type=int, default=0, + help="Size of outer margin in pixels. Default is 0 (no margin)") + argument_parser.add_argument( + "--scale", metavar="V", nargs=4, type=float, + help=f"Scale/offset output data. Parameter order is: SRC_MIN SRC_MAX " + f"DST_MIN DST_MAX where [SRC_MIN, SRC_MAX] interval " + f"maps to [DST_MIN, DST_MAX]. For Int16/Int32 -> PNG default is " + f"'{' '.join(str(v) for v in DEFAULT_INT_PNG_SCALE)}', for " + f"Float32/Float64 -> PNG default is " + f"'{' '.join(str(v) for v in DEFAULT_FLOAT_PNG_SCALE)}") + argument_parser.add_argument( + "--no_data", metavar="NODATA", + help=f"No Data value for target files. If target is UInt16 PNG and " + f"source is not Byte/UInt16, default is {DEFAULT_PNG_NO_DATA}") + data_types = translate_datatypes() + argument_parser.add_argument( + "--data_type", metavar="DATA_TYPE", choices=data_types, + help=f"Pixel data type for output file. Possible values: " + f"{', '.join(data_types)}") + resampling_methods = warp_resamplings() + argument_parser.add_argument( + "--resampling", metavar="METHOD", + choices=resampling_methods, + help=f"Resampling method. Possible values: " + f"{', '.join(resampling_methods)}. See " + f"https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r for " + f"explanations. Default is `near' for byte data, 'cubic' for other " + f"data") + argument_parser.add_argument( + "--overwrite", action="store_true", + help="Overwrite existing files. By default they are skipped (to " + "achieve easy resumption of the process)") + argument_parser.add_argument( + "--format", metavar="GDAL_DRIVER_NAME", + help="File format expressed as GDAL driver name (see " + "https://gdal.org/drivers/raster/index.html ). By default derived " + "from target file extension") + argument_parser.add_argument( + "--format_param", metavar="NAME=VALUE", action="append", + default=[], + help="Format option. May be specified several times") + argument_parser.add_argument( + "--threads", metavar="COUNT_OR_PERCENT%", + help="Number of threads to use. If positive - number of threads, if " + "negative - number of CPU cores NOT to use, if followed by `%%` - " + "percent of CPU cores. Default is total number of CPU cores") + argument_parser.add_argument( + "--nice", action="store_true", + help="Lower priority of this process and its subprocesses") + argument_parser.add_argument( + "--tile_pattern", metavar="PATTERN", required=True, + help="Pattern for tile filenames. May include path. Final (filename) " + "part may include '{VALUE[:FORMAT]}' format specifiers. Here VALUE is " + "one of 'lat_u', 'lat_d', 'lon_l', lon_r', 'lat_hem', 'LAT_HEM', " + "'lon_hem', 'LON_HEM'. Where lat/lon for absolute integer part of " + "latitude/longitude in degrees, '_u/_d' are for lower/upper latitude " + "of tile, '_l/_r' for left/right longitude of tile, '_hem' for " + "lowercase hemisphere (n/s/e/w), '_HEM' for uppercase hemisphere " + "(N/S/E/W). FORMAT is for f-string format (e.g. 03 for 3 digits with " + "leading zeros). This parameter is required") + argument_parser.add_argument( + "--remove_value", metavar="PIXEL_VALUE", action="append", type=float, + help="Remove tiles all valid points of which consists only of given " + "value (e.g. artificial NLCD in far sea. This parameter may be " + "specified more than once") + argument_parser.add_argument( + "--verbose", action="store_true", + help="Create tiles sequentially, printing gdal_translate output in " + "real time, failing on first fail. For debug purposes") + argument_parser.add_argument( + "SRC", metavar="FILENAMES", nargs="+", + help="Source filenames. May contain wildcards") + + if not argv: + argument_parser.print_help() + sys.exit(1) + args = argument_parser.parse_args(argv) + + setup_logging() + + if args.nice: + nice() + + start_time = datetime.datetime.now() + + error_if((args.left is None) != (args.right is None), + "--left and --right should be specified together or not at all") + try: + args.tile_pattern.format(lat_u=0, lat_d=0, lon_l=0, lon_r=0, + lat_hem="n", LAT_HEM="N", + lon_hem="e", LON_HEM="E") + except (KeyError, ValueError) as ex: + error(f"Invalid filename pattern syntax: {repr(ex)}") + + if os.path.dirname(args.tile_pattern): + os.makedirs(os.path.dirname(args.tile_pattern), exist_ok=True) + + PixelSizes = NamedTuple("PixelSizes", + [("pixel_size_lat", Optional[float]), + ("pixel_size_lon", Optional[float])]) + source_pixel_sizes: Set[PixelSizes] = set() + source_data_types: Set[str] = set() + SourceBoundary = NamedTuple("SourceBoundary", + [("filename", str), + ("boundaries", Boundaries)]) + source_boundaries: List[SourceBoundary] = [] + global_boundaries: Optional[Boundaries] = None + for src in args.SRC: + found = False + for filename in glob.glob(src): + found = True + file_boundaries = Boundaries(filename) + source_boundaries.append( + SourceBoundary(filename=filename, boundaries=file_boundaries)) + source_pixel_sizes.add( + PixelSizes(pixel_size_lat=file_boundaries.pixel_size_lat, + pixel_size_lon=file_boundaries.pixel_size_lon)) + source_data_types.update(GdalInfo(filename).data_types) + + global_boundaries = \ + (global_boundaries if global_boundaries is not None + else file_boundaries).combine(file_boundaries, + round_boundaries_to_degree=True) + error_if(not found, + f"No files matching '{src}' were found") + assert global_boundaries is not None + global_boundaries = \ + global_boundaries.crop( + Boundaries(top=args.top, bottom=args.bottom, + left=args.left, right=args.right), + round_boundaries_to_degree=True) + error_if(global_boundaries is None, + "Given source files lie completely outside of given boundaries") + if args.pixel_size is not None: + error_if(args.pixels_per_degree is not None, + "--pixel_size and --pixels_per_degree may not be specified " + "together") + pixel_size_lat = pixel_size_lon = args.pixel_size + elif args.pixels_per_degree is not None: + pixel_size_lat = pixel_size_lon = 1 / args.pixels_per_degree + elif len(source_pixel_sizes) == 1: + pixel_size_lat = list(source_pixel_sizes)[0].pixel_size_lat + pixel_size_lon = list(source_pixel_sizes)[0].pixel_size_lon + error_if((pixel_size_lat is None) or (pixel_size_lon is None), + "Unable to derive pixel size from source files. It should be " + "specified explicitly with --pixel_size or " + "--pixels_per_degree") + else: + error("Source files have different pixel sizes. It should be " + "specified explicitly with --pixel_size or --pixels_per_degree") + + scale = get_scale(arg_scale=args.scale, src_data_types=source_data_types, + dst_format=args.format, dst_data_type=args.data_type, + dst_ext=os.path.splitext(args.tile_pattern)[1]) + no_data = get_no_data(arg_no_data=args.no_data, + src_data_types=source_data_types, + dst_format=args.format, dst_data_type=args.data_type, + dst_ext=os.path.splitext(args.tile_pattern)[1]) + resampling = get_resampling(arg_resampling=args.resampling, + src_data_types=source_data_types, + dst_data_type=args.data_type) + + assert (global_boundaries is not None) and \ + (global_boundaries.top is not None) and \ + (global_boundaries.bottom is not None) and \ + (global_boundaries.right is not None) and \ + (global_boundaries.left is not None) + + Tile = NamedTuple("Tile", + [("top", int), ("left", int), ("sources", List[str])]) + tiles: List[Tile] = [] + for top in range(round(global_boundaries.top), + round(global_boundaries.bottom), -1): + for left in range(round(global_boundaries.left), + round(global_boundaries.right)): + tile_boundaries = \ + Boundaries(top=top, bottom=top - 1, left=left, right=left + 1) + sources = [sb.filename for sb in source_boundaries + if sb.boundaries.intersects(tile_boundaries)] + if not sources: + continue + tiles.append(Tile(top=top, left=left, sources=sources)) + + total_tiles = len(tiles) + + completed_tiles = [0] # List to facilitate closure in completer() + skipped_tiles: List[str] = [] + failed_tiles: List[str] = [] + + def completer(cr: ConvResult) -> None: + """ Processes completion of a single tile """ + completed_tiles[0] += 1 + msg = f"{completed_tiles[0]} of {total_tiles} " \ + f"({completed_tiles[0] * 100 // total_tiles}%) {cr.tilename}: " + if cr.status == ConvStatus.Exists: + msg += "Tile exists. Skipped" + skipped_tiles.append(cr.tilename) + elif cr.status == ConvStatus.Error: + assert cr.msg is not None + error_if(args.verbose, cr.msg) + if cr.command_lines: + msg += "\n" + "\n".join(cr.command_lines) + msg += "\n" + cr.msg + elif cr.status == ConvStatus.Dropped: + assert cr.msg is not None + msg += f"{cr.msg}. Tile dropped" + else: + assert cr.status == ConvStatus.Success + if cr.msg is not None: + msg += "\n" + cr.msg + msg += f"Converted in {Durator.duration_to_hms(cr.duration)}" + print(msg) + + print(f"{len(tiles)} tiles in {global_boundaries}") + + original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) + pool = None if args.verbose else \ + multiprocessing.pool.ThreadPool( + processes=threads_arg(args.threads)) + signal.signal(signal.SIGINT, original_sigint_handler) + try: + for tile in tiles: + kwargs = { + "tile_pattern": args.tile_pattern, + "sources": tile.sources, + "top": tile.top, + "left": tile.left, + "margin": args.margin, + "pixel_size_lat": pixel_size_lat, + "pixel_size_lon": pixel_size_lon, + "scale": scale, + "no_data": no_data, + "data_type": args.data_type, + "resampling": resampling, + "overwrite": args.overwrite, + "remove_values": args.remove_value, + "out_format": args.format, + "format_params": args.format_param, + "verbose": args.verbose} + if args.verbose: + completer(tile_creator(**kwargs)) + else: + assert pool is not None + pool.apply_async(tile_creator, kwds=kwargs, + callback=completer) + if pool: + pool.close() + pool.join() + pool = None + + if skipped_tiles: + print(f"{len(skipped_tiles)} previously existing tiles skipped") + if failed_tiles: + print("Following tiles were not created due to errors:") + for tilename in sorted(failed_tiles): + print(f" {tilename}") + + implicit_conversion_params: List[str] = [] + if scale != args.scale: + implicit_conversion_params.append(f"scale of {scale}") + if no_data != args.no_data: + implicit_conversion_params.append(f"NoData of {no_data}") + if resampling != args.resampling: + implicit_conversion_params.append(f"resampling of {resampling}") + if implicit_conversion_params: + print(f"Implicitly chosen conversion parameters: " + f"{', '.join(implicit_conversion_params)}") + + print( + f"Total duration: " + f"{Durator.duration_to_hms(datetime.datetime.now() - start_time)}") + except KeyboardInterrupt: + sys.exit(1) + finally: + if pool is not None: + pool.terminate() + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tools/geo_converters/to_png.py b/tools/geo_converters/to_png.py new file mode 100755 index 0000000..f84e029 --- /dev/null +++ b/tools/geo_converters/to_png.py @@ -0,0 +1,459 @@ +#!/usr/bin/env python3 +# Converts whatever geospatial file to PNG (wrapper around gdal_translate) + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=wildcard-import, unused-wildcard-import, invalid-name +# pylint: disable=too-many-statements, too-many-branches, too-many-arguments +# pylint: disable=too-many-locals + +import argparse +from collections.abc import Sequence +import datetime +import enum +import glob +import multiprocessing.pool +import os +import shlex +import signal +import sys +from typing import List, NamedTuple, Optional, Set + +from geoutils import * + +# Default file extension +DEFAULT_EXT = ".png" + +_EPILOG = """This script expects that GDAL utilities are installed and in PATH. +Also it expects that as part of this installation proj/proj.db is somehow +installed as well (if it is, but gdalwarp still complains - set PROJ_LIB +environment variable to point to this proj directory). + +Some examples: +- Convert 3DEP files in 3dep/1_arcsec to png in 3DEP_PNG directory, using 8 + CPUs: + $ tp_png.py --threads 8 --out_dir 3DEP_PNG 3dep/1_arcsec/*.tif +""" + +DataTypeResult = NamedTuple("DataTypeResult", + [("filename", str), + ("data_types", Optional[Sequence[str]])]) + + +def data_type_worker(filename: str) -> DataTypeResult: + """ Data type worker. Returns per-band sequence of used data types """ + try: + return DataTypeResult(filename=filename, + data_types=GdalInfo(filename).data_types) + except (Exception, KeyboardInterrupt, SystemExit) as ex: + return DataTypeResult(filename=filename, data_types=None) + + +# Conversion result status +ConvStatus = enum.Enum("ConvStatus", ["Success", "Exists", "Error"]) + + +class ConvResult(NamedTuple): + """Conversion result """ + + # Name of converted file + filename: str + + # Conversion status + status: ConvStatus + + # Conversion duration + duration: datetime.timedelta + + # Optional error message + msg: Optional[str] = None + + # Optional command line + command_line: Optional[str] = None + + +def conversion_worker( + src: str, dst: str, resampling: str, top: Optional[float], + bottom: Optional[float], left: Optional[float], right: Optional[float], + pixel_size: Optional[float], pixels_per_degree: Optional[float], + scale: Optional[List[float]], no_data: Optional[str], + data_type: Optional[str], wld: bool, round_boundaries_to_degree: bool, + round_pixels_to_degree: bool, format_params: List[str], + overwrite: bool, quiet: bool) -> ConvResult: + """ Worker function performing the conversion + + Arguments: + src -- Source file name + dst -- Destination file name + resampling -- Resampling method + top -- Optional top boundary of resulting file + bottom -- Optional bottom boundary of resulting file + left -- Optional left boundary of resulting file + right -- Optional right boundary of resulting file + pixel_size -- Optional pixel size in degrees + pixels_per_degree -- Optional number of pixels per degree + scale -- Optional scale as [src_min,src_max,dst_min, + dst_max] + no_data -- Optional NoData value + data_type -- Optional pixel data type + wld -- Generate .wld file (containing translation + parameters) + round_boundaries_to_degree -- True to round not explicitly specified + boundaries outward to next degree + round_pixels_to_degree -- True to round pixel sizes to whole number per + degree + format_params -- Output format options + overwrite -- True to overwrite file if it exists + quiet -- True to not print anything, returning message + instead + Returns ConvResult object + """ + stem, ext = os.path.splitext(dst) + dst_wld = stem + ".wld" + temp_file = stem + ".incomplete" + ext + temp_file_wld = stem + ".incomplete.wld" + temp_file_xml = temp_file + ".aux.xml" + start_time = datetime.datetime.now() + try: + if os.path.isfile(dst) and (not overwrite): + return ConvResult(filename=src, status=ConvStatus.Exists, + duration=datetime.datetime.now() - start_time) + boundaries = \ + Boundaries( + src, pixel_size=pixel_size, + pixels_per_degree=pixels_per_degree, top=top, bottom=bottom, + left=left, right=right, + round_boundaries_to_degree=round_boundaries_to_degree, + round_pixels_to_degree=round_pixels_to_degree) + gi = GdalInfo(src, fail_on_error=False) + if not (gi and boundaries): + return ConvResult(filename=src, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg="gdalinfo inspection failed") + warn: Optional[str] = None + if (not scale) and \ + any(dt in ("Float32", "Float64") for dt in gi.data_types): + warn = "Float source data converted to integer data without " + \ + "scaling. Loss of precision may ensue" + + trans_args = \ + ["gdal_translate", "-strict", "-r", resampling, "-of", PNG_FORMAT] + for fp in format_params: + trans_args += ["-co", fp] + if wld: + trans_args += ["-co", "WORLDFILE=YES"] + if boundaries.edges_overridden: + trans_args += \ + ["-projwin", str(boundaries.left), str(boundaries.top), + str(boundaries.right), str(boundaries.bottom)] + if scale: + trans_args += ["-scale"] + [str(s) for s in scale] + if no_data: + trans_args += ["-a_nodata", no_data] + if data_type: + trans_args += ["-ot", data_type] + trans_args += [src, temp_file] + command_line = " ".join(shlex.quote(arg) for arg in trans_args) + exec_result = execute(trans_args, env=gdal_env(), + disable_output=quiet, return_error=quiet, + fail_on_error=not quiet) + if quiet: + if exec_result is not None: + assert isinstance(exec_result, str) + return ConvResult( + filename=src, + status=ConvStatus.Error, + duration=datetime.datetime.now() - + start_time, + msg=exec_result, + command_line=command_line) + else: + assert isinstance(exec_result, bool) and exec_result + if os.path.isfile(dst): + os.unlink(dst) + if os.path.isfile(dst_wld): + os.unlink(dst_wld) + os.rename(temp_file, dst) + if wld: + os.rename(temp_file_wld, dst_wld) + return ConvResult(filename=src, status=ConvStatus.Success, + duration=datetime.datetime.now() - start_time, + msg=warn) + except (Exception, KeyboardInterrupt, SystemExit) as ex: + return ConvResult(filename=src, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg=repr(ex)) + finally: + for filename in (temp_file_xml, temp_file_wld, temp_file): + try: + if os.path.isfile(filename): + os.unlink(filename) + except OSError: + pass + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + argument_parser = argparse.ArgumentParser( + description="Converts geospatial file to PNG (wrapper around " + "gdal_translate)", + formatter_class=argparse.RawDescriptionHelpFormatter, epilog=_EPILOG) + argument_parser.add_argument( + "--pixel_size", metavar="DEGREES", type=float, + help="Resulting file resolution in pixel size (expressed in degrees). " + "Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--pixels_per_degree", metavar="NUMBER", type=float, + help="Resulting file resolution in pixels per degree. Default is to " + "use one from 'gdalinfo'") + argument_parser.add_argument( + "--top", metavar="MAX_LATITUDE", type=float, + help="Maximum latitude. Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--bottom", metavar="MIN_LATITUDE", type=float, + help="Minimum latitude. Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--left", metavar="MIN_LONGITUDE", type=float, + help="Minimum longitude. Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--right", metavar="MAX_LONGITUDE", type=float, + help="Maximum longitude. Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--round_boundaries_to_degree", action="store_true", + help="Round boundaries (that were not explicitly specified) to next " + "degree in outward direction. Default is to keep boundaries") + argument_parser.add_argument( + "--round_pixels_to_degree", action="store_true", + help="Round pixel sizes to whole number of pixels per degree") + argument_parser.add_argument( + "--scale", metavar="V", nargs=4, type=float, + help=f"Scale/offset output data. Parameter order is: SRC_MIN SRC_MAX " + f"DST_MIN DST_MAX where [SRC_MIN, SRC_MAX] interval " + f"maps to [DST_MIN, DST_MAX]. For Int16/Int32 sources default is " + f"'{' '.join(str(v) for v in DEFAULT_INT_PNG_SCALE)}', for " + f"Float32/Float64 sources default is " + f"'{' '.join(str(v) for v in DEFAULT_FLOAT_PNG_SCALE)}") + argument_parser.add_argument( + "--no_data", metavar="NODATA", + help=f"No Data value for target files. If target is UInt16 PNG and " + f"source is not Byte/UInt16, default is {DEFAULT_PNG_NO_DATA}") + argument_parser.add_argument( + "--data_type", metavar="DATA_TYPE", choices=["Byte", "UInt16"], + help="Pixel data type for output file. Possible values: Byte, UInt16") + argument_parser.add_argument( + "--wld", action="store_true", + help="Generate .wld file, containing translation parameters") + argument_parser.add_argument( + "--resampling", metavar="METHOD", + choices=warp_resamplings(), + help="Resampling method. See " + "https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r for " + "explanations. Default is `near' for byte data, 'cubic' for other " + "data") + argument_parser.add_argument( + "--format_param", metavar="NAME=VALUE", action="append", + default=[], + help="Format option (e.g. COMPRESS=PACKBITS or BIGTIFF=IF_NEEDED)") + argument_parser.add_argument( + "--out_dir", metavar="OUTPUT_DIRECTORY", + help="If present - mass conversion is performed and this is the " + "target directory") + argument_parser.add_argument( + "--out_ext", metavar=".EXT", default=DEFAULT_EXT, + help=f"Extension for output files to use in case of mass conversion. " + f"Default is '{DEFAULT_EXT}'") + argument_parser.add_argument( + "--overwrite", action="store_true", + help="Overwrite target file if exists. By default if mass conversion " + "is performed (--out_dir provided), already existing files are " + "skipped (making possible a restartable conversion), otherwise " + "already existing file reported as an error") + argument_parser.add_argument( + "--threads", metavar="COUNT_OR_PERCENT%", + help="Number of threads to use. If positive - number of threads, if " + "negative - number of CPU cores NOT to use, if followed by `%%` - " + "percent of CPU cores. Default is total number of CPU cores") + argument_parser.add_argument( + "--nice", action="store_true", + help="Lower priority of this process and its subprocesses") + argument_parser.add_argument( + "FILES", nargs="+", + help="In case of mass conversion (--out_dir is provided) - source " + "file names. Otherwise it should be 'SRC DST' pair") + + if not argv: + argument_parser.print_help() + sys.exit(1) + args = argument_parser.parse_args(argv) + + setup_logging() + + if args.nice: + nice() + + start_time = datetime.datetime.now() + + sources: List[str] = [] + if args.out_dir is None: + error_if(len(args.FILES) < 2, + "Destination file name not provided") + error_if(len(args.FILES) > 2, + "Exactly two file names (source and destination) should be " + "provided") + sources.append(args.FILES[0]) + else: + for files_arg in args.FILES: + files = glob.glob(files_arg) + error_if(not files, + f"No source files matching '{files_arg}' found") + sources += list(files) + source_data_types: Set[str] = set() + + def data_type_completer(dtr: DataTypeResult) -> None: + """ Callback processing results of data_type_worker """ + error_if(dtr.data_types is None, + f"'{dtr.filename}' failed gdalinfo inspection") + source_data_types.update(dtr.data_types) + + try: + original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) + with multiprocessing.pool.ThreadPool( + processes=threads_arg(args.threads)) as pool: + signal.signal(signal.SIGINT, original_sigint_handler) + for filename in sources: + pool.apply_async(data_type_worker, kwds={"filename": filename}, + callback=data_type_completer) + pool.close() + pool.join() + except KeyboardInterrupt: + sys.exit(1) + + data_type = args.data_type + if data_type is None: + if any(dt.startswith("Float") for dt in source_data_types): + data_type = "UInt16" + else: + error_if(not all(dt == "Byte" for dt in source_data_types), + "Data type must be specified explicitly with --data_type") + + scale = get_scale(arg_scale=args.scale, src_data_types=source_data_types, + dst_format=PNG_FORMAT, dst_data_type=data_type, + dst_ext=None) + no_data = get_no_data(arg_no_data=args.no_data, + src_data_types=source_data_types, + dst_format=PNG_FORMAT, dst_data_type=data_type, + dst_ext=None) + resampling = get_resampling(arg_resampling=args.resampling, + src_data_types=source_data_types, + dst_data_type=data_type) + + common_kwargs = { + "resampling": resampling, + "top": args.top, + "bottom": args.bottom, + "left": args.left, + "right": args.right, + "pixel_size": args.pixel_size, + "pixels_per_degree": args.pixels_per_degree, + "scale": scale, + "no_data": no_data, + "data_type": data_type, + "wld": args.wld, + "round_boundaries_to_degree": args.round_boundaries_to_degree, + "round_pixels_to_degree": args.round_pixels_to_degree, + "format_params": args.format_param, + "overwrite": args.overwrite} + + if args.out_dir is None: + cr = conversion_worker(src=args.FILES[0], dst=args.FILES[1], + quiet=False, **common_kwargs) + if cr.status == ConvStatus.Error: + assert cr.msg is not None + error(cr.msg) + error_if(cr.status == ConvStatus.Exists, + f"File '{args.FILES[1]}' already exists. Specify --overwrite " + f"to overwrite it") + assert cr.status == ConvStatus.Success + else: + if not os.path.isdir(args.out_dir): + os.makedirs(args.out_dir) + total_count = len(sources) + completed_count = [0] # Made list to facilitate closure in completer() + skipped_files: List[str] = [] + failed_files: List[str] = [] + + def completer(cr: ConvResult) -> None: + """ Processes completion of a single file """ + completed_count[0] += 1 + msg = f"{completed_count[0]} of {total_count} " \ + f"({completed_count[0] * 100 // total_count}%) {cr.filename}: " + if cr.status == ConvStatus.Exists: + msg += "Destination file exists. Skipped" + skipped_files.append(cr.filename) + elif cr.status == ConvStatus.Error: + failed_files.append(cr.filename) + msg += f"Conversion failed: {cr.msg}" + else: + assert cr.status == ConvStatus.Success + msg += f"Converted in {Durator.duration_to_hms(cr.duration)}" + if cr.msg: + msg += "\n" + cr.msg + print(msg) + + try: + original_sigint_handler = signal.signal(signal.SIGINT, + signal.SIG_IGN) + with multiprocessing.pool.ThreadPool( + processes=threads_arg(args.threads)) as pool: + signal.signal(signal.SIGINT, original_sigint_handler) + for filename in sources: + kwargs = common_kwargs.copy() + kwargs["src"] = filename + kwargs["dst"] = \ + os.path.join(args.out_dir, os.path.basename(filename)) + if args.out_ext is not None: + kwargs["dst"] = \ + os.path.splitext(kwargs["dst"])[0] + args.out_ext + kwargs["quiet"] = True + pool.apply_async(conversion_worker, kwds=kwargs, + callback=completer) + pool.close() + pool.join() + if skipped_files: + print( + f"{len(skipped_files)} previously existing files skipped") + if failed_files: + print("Following files were not converted due to errors:") + for filename in sorted(failed_files): + print(f" {filename}") + except KeyboardInterrupt: + sys.exit(1) + + implicit_conversion_params: List[str] = [] + if scale != args.scale: + implicit_conversion_params.append(f"scale of {scale}") + if no_data != args.no_data: + implicit_conversion_params.append(f"NoData of {no_data}") + if resampling != args.resampling: + implicit_conversion_params.append(f"resampling of {resampling}") + if data_type != args.data_type: + implicit_conversion_params.append(f"data type of {data_type}") + if implicit_conversion_params: + print(f"Implicitly chosen conversion parameters: " + f"{', '.join(implicit_conversion_params)}") + + print( + f"Total duration: " + f"{Durator.duration_to_hms(datetime.datetime.now() - start_time)}") + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tools/geo_converters/to_wgs84.py b/tools/geo_converters/to_wgs84.py new file mode 100755 index 0000000..daf4ed2 --- /dev/null +++ b/tools/geo_converters/to_wgs84.py @@ -0,0 +1,564 @@ +#!/usr/bin/env python3 +# Converts whatever geospatial file to WGS84 (wrapper around gdalwarp) + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=wildcard-import, unused-wildcard-import, invalid-name +# pylint: disable=too-many-statements, too-many-locals, too-many-arguments +# pylint: disable=too-many-branches + +import argparse +from collections.abc import Sequence +import datetime +import enum +import fnmatch +import glob +import multiprocessing.pool +import os +import signal +import sys +from typing import List, NamedTuple, Optional, Set + +from geoutils import * + +_epilog = """This script expects that GDAL utilities are installed and in PATH. +Also it expects that as part of this installation proj/proj.db is somehow +installed as well (if it is, but gdalwarp still complains - set PROJ_LIB +environment variable to point to this proj directory). + +Usage examples: + - Convert Canada DSM 2000 to WGS84 + $ ./to_wgs84.py --pixels_per_degree 3600 --resampling cubic \\ + --format_param BIGTIFF=IF_NEEDED --format_param COMPRESS=LZW \\ + cdsm-canada-dem.tif cdsm-canada-dem_wgs84_cgvd28.tif +""" + +# Boundary detection status +BoundaryStatus = enum.Enum("BoundaryStatus", + ["Success", "NoSrcGeoid", "NoDstGeoid", + "OutOfBounds", "Error"]) + + +# Names of source and destination file +SrcDst = NamedTuple("SrcDst", [("src", str), ("dst", str)]) + + +class BoundaryResult(NamedTuple): + """ Boundary detection result """ + + # Name of source and destination file + src_dst: SrcDst + + # Boundary detection status + status: BoundaryStatus + + # Boundaries (None in case of failure) + boundaries: Optional[Boundaries] = None + + # Error message + msg: Optional[str] = None + + # Data tpes (None in case of failure) + data_types: Optional[Sequence[str]] = None + + +def boundary_worker(src_dst: SrcDst, pixel_size: Optional[float], + pixels_per_degree: Optional[float], top: Optional[float], + bottom: Optional[float], left: Optional[float], + right: Optional[float], round_boundaries_to_degree: bool, + round_pixels_to_degree: bool, src_geoids: Geoids, + dst_geoids: Geoids) -> BoundaryResult: + """ Boundary detection worker + + Arguments: + src_dst -- Contains source file name + pixel_size -- Optional pixel size + pixels_per_degree -- Optional number of pixels per degree + top -- Optional top boundary + bottom -- Optional bottom boundary + left -- Optional left boundary + right -- Optional right boundary + round_boundaries_to_degree -- True to round boundaries to outer degree + round_pixels_to_degree -- True to round number of pixels per degree to + whole number + src_geoids -- Source geoids + dst_geoids -- Destination geoids + Reurns BoundaryResult object + """ + try: + boundaries = \ + Boundaries( + src_dst.src, pixel_size=pixel_size, + pixels_per_degree=pixels_per_degree, top=top, bottom=bottom, + left=left, right=right, + round_boundaries_to_degree=round_boundaries_to_degree, + round_pixels_to_degree=round_pixels_to_degree) + gi = GdalInfo(src_dst.src) + if not boundaries.intersects( + Boundaries(top=gi.top, bottom=gi.bottom, left=gi.left, + right=gi.right)): + return BoundaryResult(src_dst=src_dst, + status=BoundaryStatus.OutOfBounds) + if src_geoids and \ + (not src_geoids.geoid_for(boundaries, + fail_if_not_found=False)): + return BoundaryResult(src_dst=src_dst, + status=BoundaryStatus.NoSrcGeoid, + data_types=gi.data_types) + if dst_geoids and \ + (not dst_geoids.geoid_for(boundaries, + fail_if_not_found=False)): + return BoundaryResult(src_dst=src_dst, + status=BoundaryStatus.NoDstGeoid, + data_types=gi.data_types) + return BoundaryResult(src_dst=src_dst, status=BoundaryStatus.Success, + boundaries=boundaries, data_types=gi.data_types) + except (Exception, KeyboardInterrupt, SystemExit) as ex: + return BoundaryResult(src_dst=src_dst, status=BoundaryStatus.Error, + msg=repr(ex)) + + +# Conversion result status +ConvStatus = enum.Enum("ConvStatus", ["Success", "Exists", "Error"]) + +# Conversion result +ConvResult = \ + NamedTuple( + "ConvResult", + [ + # Name of converted file + ("filename", str), + # Conversion status + ("status", ConvStatus), + # Conversion duration + ("duration", datetime.timedelta), + # Optional error message + ("msg", Optional[str])]) + + +def conversion_worker( + src_dst: SrcDst, boundaries: Boundaries, resampling: str, + src_geoids: Geoids, dst_geoids: Geoids, out_format: Optional[str], + format_params: List[str], overwrite: bool, quiet: bool, + remove_src: bool, keep_ext: List[str]) -> ConvResult: + """ Worker function performing the conversion + + Arguments: + src_dst -- Source ands destination file names + boundaries -- Destination file boundaries + resampling -- Resampling method + src_geoids -- Geoid(s) of source file + dst_geoids -- Geoid(s) for destination file + out_format -- Optional output file format + format_params -- Output format options + overwrite -- True to overwrite file if it exists + quiet -- True to not print anything, returning message + instead + remove_src -- Remove source file + keep_ext -- List of extensions to keep + Returns ConvResult object + """ + stem, ext = os.path.splitext(src_dst.dst) + incomplete_stem = stem + ".incomplete" + temp_file = incomplete_stem + ext + start_time = datetime.datetime.now() + try: + if os.path.isfile(src_dst.dst) and (not overwrite): + return ConvResult(filename=src_dst.src, status=ConvStatus.Exists, + duration=datetime.datetime.now() - start_time, + msg=None) + src_geoid: Optional[str] = None + if src_geoids: + src_geoid = \ + src_geoids.geoid_for(boundaries, fail_if_not_found=False) + assert src_geoid is not None + + dst_geoid: Optional[str] = None + if dst_geoids: + dst_geoid = \ + dst_geoids.geoid_for(boundaries, fail_if_not_found=False) + assert src_geoid is not None + + success, msg = \ + warp(src=src_dst.src, dst=temp_file, resampling=resampling, + top=boundaries.top if boundaries.edges_overridden else None, + bottom=boundaries.bottom + if boundaries.edges_overridden else None, + left=boundaries.left if boundaries.edges_overridden else None, + right=boundaries.right + if boundaries.edges_overridden else None, + pixel_size_lat=boundaries.pixel_size_lat + if boundaries.pixel_size_overridden else None, + pixel_size_lon=boundaries.pixel_size_lon + if boundaries.pixel_size_overridden else None, + src_geoid=src_geoid, dst_geoid=dst_geoid, + center_lon_180=bool(boundaries.cross_180), + out_format=out_format, format_params=format_params, + overwrite=True, quiet=quiet) + if not success: + return ConvResult(filename=src_dst.src, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg=msg) + if os.path.isfile(src_dst.dst): + if not quiet: + print(f"Removing '{src_dst.dst}'") + os.unlink(src_dst.dst) + if not quiet: + print(f"Renaming '{temp_file}' to '{src_dst.dst}'") + os.rename(temp_file, src_dst.dst) + + if remove_src: + os.unlink(src_dst.src) + + for other_generated in glob.glob(incomplete_stem + "*"): + if not os.path.isfile(other_generated): + continue + other_ext = os.path.splitext(other_generated)[1] + if other_ext in keep_ext: + other_target = stem + other_ext + if os.path.isfile(other_target): + if not quiet: + print(f"Removing '{other_target}'") + os.unlink(other_target) + if not quiet: + print(f"Renaming '{other_generated}' to '{other_target}'") + os.rename(other_generated, other_target) + else: + if not quiet: + print(f"Removing '{other_generated}'") + os.unlink(other_generated) + return ConvResult( + filename=src_dst.src, + status=ConvStatus.Success, + duration=datetime.datetime.now() - + start_time, + msg=msg) + except (Exception, KeyboardInterrupt, SystemExit) as ex: + return ConvResult(filename=src_dst.src, status=ConvStatus.Error, + duration=datetime.datetime.now() - start_time, + msg=repr(ex)) + finally: + try: + if os.path.isfile(temp_file): + os.unlink(temp_file) + except OSError: + pass + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + argument_parser = argparse.ArgumentParser( + description="Converts whatever geospatial file to WGS84", + formatter_class=argparse.RawDescriptionHelpFormatter, epilog=_epilog) + argument_parser.add_argument( + "--pixel_size", metavar="DEGREES", type=float, + help="Resulting file resolution in pixel size (expressed in degrees). " + "Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--pixels_per_degree", metavar="NUMBER", type=float, + help="Resulting file resolution in pixels per degree. Default is to " + "use one from 'gdalinfo'") + argument_parser.add_argument( + "--top", metavar="MAX_LATITUDE", type=float, + help="Maximum latitude. Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--bottom", metavar="MIN_LATITUDE", type=float, + help="Minimum latitude. Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--left", metavar="MIN_LONGITUDE", type=float, + help="Minimum longitude. Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--right", metavar="MAX_LONGITUDE", type=float, + help="Maximum longitude. Default is to use one from 'gdalinfo'") + argument_parser.add_argument( + "--round_boundaries_to_degree", action="store_true", + help="Round boundaries (that were not explicitly specified) to next " + "degree in outward direction. Default is to keep boundaries") + argument_parser.add_argument( + "--round_pixels_to_degree", action="store_true", + help="Round pixel sizes to whole number of pixels per degree") + argument_parser.add_argument( + "--resampling", metavar="METHOD", + choices=warp_resamplings(), + help="Resampling method. See " + "https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r for " + "explanations. Default is `near' for byte data, 'cubic' for other " + "data") + argument_parser.add_argument( + "--src_geoid", metavar="GEOID_FILE", action="append", + help="Geoid source file heights specified relative to. Name may " + "include wildcards (to handle multifile geoids). This argument may be " + "specified several times (in order of preference decrease). If " + "directory not specified geoids also looked up in this script's " + "directory. Default is to assume ellipsoidal source heights") + argument_parser.add_argument( + "--dst_geoid", metavar="GEOID_FILE", action="append", + help="Geoid resulting file heights specified relative to. Name may " + "include wildcards (to handle multifile geoids). This argument may be " + "specified several times (in order of preference decrease). If " + "directory not specified geoids also looked up in this script's " + "directory. Default is to assume ellipsoidal resulting heights") + argument_parser.add_argument( + "--extend_geoid_coverage", metavar="DEGREES", type=float, default=0., + help="Artificially extend geoid boundaries when checking for " + "coverage. Useful when geoid and converted file cut at same " + "latitude/longitude degree, but converted file has margin that " + "extends it beyond geoid (this making it not fully covered) - as it " + "is the case for Canada") + argument_parser.add_argument( + "--format", metavar="GDAL_DRIVER_NAME", + help="File format expressed as GDAL driver name (see " + "https://gdal.org/drivers/raster/index.html ). By default derived " + "from target file extension") + argument_parser.add_argument( + "--format_param", metavar="NAME=VALUE", action="append", + default=[], + help="Format option (e.g. COMPRESS=PACKBITS or BIGTIFF=IF_NEEDED)") + argument_parser.add_argument( + "--out_dir", metavar="OUTPUT_DIRECTORY", + help="If present - mass conversion is performed and this is the " + "target directory") + argument_parser.add_argument( + "--recursive", action="store_true", + help="Recreate folder structure in destination directory - see help " + "to FILES. If specified then --out_dir must be specified") + argument_parser.add_argument( + "--out_ext", metavar=".EXT", + help="Extension for output files to use in case of mass conversion. " + "Default is to keep original extension") + argument_parser.add_argument( + "--keep_ext", metavar=".EXT", action="append", default=[], + help="Also keep generated files of given extension. This parameter " + "may be specified several times") + argument_parser.add_argument( + "--overwrite", action="store_true", + help="Overwrite target file if exists. By default if mass conversion " + "is performed (--out_dir provided), already existing files are " + "skipped (making possible a restartable conversion), otherwise " + "already existing file reported as an error") + argument_parser.add_argument( + "--threads", metavar="COUNT_OR_PERCENT%", + help="Number of threads to use. If positive - number of threads, if " + "negative - number of CPU cores NOT to use, if followed by `%%` - " + "percent of CPU cores. Default is total number of CPU cores") + argument_parser.add_argument( + "--nice", action="store_true", + help="Lower priority of this process and its subprocesses") + argument_parser.add_argument( + "--remove_src", action="store_true", + help="Remove source file after successful conversion (e.g. to save " + "space)") + argument_parser.add_argument( + "FILES", nargs="+", + help="If --recursive specified, file specification has form " + "like BASE_DIR/*.EXT (search in subdirectories of BASE_DIR performed, " + "these subdirectories below BASE_DIR replicated in output directory). " + "If --out_dir specified, but --recursive not specified - mass " + "conversion to given directory. If --out_dir not specified - SRC_FILE " + "DST_FILE pair should be specified") + if not argv: + argument_parser.print_help() + sys.exit(1) + args = argument_parser.parse_args(argv) + + setup_logging() + + if args.nice: + nice() + + def change_dst_ext(dst: str) -> str: + """ Changes extension of given given destination file name if it was + requested """ + return (os.path.splitext(dst)[0] + args.out_ext) \ + if args.out_ext is not None else dst + + filenames: List[SrcDst] = [] + if args.out_dir is None: + error_if(len(args.FILES) < 2, + "Destination file name not provided") + error_if(len(args.FILES) > 2, + "Exactly two file names (source and destination) should be " + "provided") + error_if(not os.path.isfile(args.FILES[0]), + f"File '{args.FILES[0]}' not found") + filenames.append( + SrcDst(src=args.FILES[0], + dst=os.path.join(args.FILES[1], + os.path.basename(args.FILES[0])) + if os.path.isdir(args.FILES[1]) else args.FILES[1])) + elif not args.recursive: + for files_arg in args.FILES: + files = glob.glob(files_arg) + error_if(not files, + f"No source files matching '{files_arg}' found") + filenames += \ + [SrcDst(f, os.path.join(args.out_dir, + change_dst_ext(os.path.basename(f)))) + for f in files] + else: + for files_arg in args.FILES: + base_dir, filemask = os.path.split(files_arg) + base_dir = base_dir or "." + error_if(not os.path.isdir(base_dir), + f"Directory '{base_dir}' not found") + found = False + for walk_dirpath, _, walk_filenames in os.walk(base_dir): + for filename in walk_filenames: + if not fnmatch.fnmatch(filename, filemask): + continue + found = True + filenames.append( + SrcDst(src=os.path.join(walk_dirpath, filename), + dst=os.path.join( + args.out_dir, + os.path.relpath(walk_dirpath, base_dir), + change_dst_ext(filename)))) + error_if(not found, + f"No '{filemask}' files were found beneath '{base_dir}'") + + try: + SrcInfo = NamedTuple("SrcInfo", + [("src_dst", SrcDst), ("boundaries", Boundaries)]) + src_infos: List[SrcInfo] = [] + oob_files: List[str] = [] + no_geoid_files: List[str] = [] + src_geoids = \ + Geoids(args.src_geoid, extension=args.extend_geoid_coverage) + dst_geoids = \ + Geoids(args.dst_geoid, extension=args.extend_geoid_coverage) + source_data_types: Set[str] = set() + failed_files: List[str] = [] + + def boundariesCompleter(br: BoundaryResult) -> None: + if br.data_types is not None: + source_data_types.update(br.data_types) + if br.status == BoundaryStatus.Success: + assert br.boundaries is not None + src_infos.append(SrcInfo(src_dst=br.src_dst, + boundaries=br.boundaries)) + return + if br.status == BoundaryStatus.OutOfBounds: + warning(f"'{br.src_dst.src}' lies outside of given " + f"boundaries. It will not be processed") + oob_files.append(br.src_dst.src) + return + if br.status == BoundaryStatus.NoSrcGeoid: + warning(f"'{br.src_dst.src}' not covered by any source geoid. " + f"It will not be processed") + no_geoid_files.append(br.src_dst.src) + return + if br.status == BoundaryStatus.NoDstGeoid: + warning(f"'{br.src_dst.src}' not covered by any target geoid. " + f"It will not be processed") + no_geoid_files.append(br.src_dst.src) + return + if br.status == BoundaryStatus.Error: + warning(f"'{br.src_dst.src}' has a problem: {br.msg}") + failed_files.append(br.src_dst.src) + return + + original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) + with multiprocessing.pool.ThreadPool( + processes=threads_arg(args.threads)) as pool: + signal.signal(signal.SIGINT, original_sigint_handler) + for src_dst in filenames: + pool.apply_async( + boundary_worker, + kwds={"src_dst": src_dst, "pixel_size": args.pixel_size, + "pixels_per_degree": args.pixels_per_degree, + "top": args.top, "bottom": args.bottom, + "left": args.left, "right": args.right, + "round_boundaries_to_degree": + args.round_boundaries_to_degree, + "round_pixels_to_degree": + args.round_pixels_to_degree, + "src_geoids": src_geoids, "dst_geoids": dst_geoids}, + callback=boundariesCompleter) + pool.close() + pool.join() + src_infos.sort() + resampling = get_resampling(arg_resampling=args.resampling, + src_data_types=source_data_types, + dst_data_type=None) + common_kwargs = { + "resampling": resampling, + "src_geoids": src_geoids, + "dst_geoids": dst_geoids, + "out_format": args.format, + "format_params": args.format_param, + "overwrite": args.overwrite, + "remove_src": args.remove_src, + "keep_ext": args.keep_ext} + start_time = datetime.datetime.now() + + total_count = len(src_infos) + completed_count = [0] # Made list to facilitate closure in completer() + skipped_files: List[str] = [] + + def conversionCompleter(cr: ConvResult) -> None: + """ Processes completion of a single file """ + completed_count[0] += 1 + msg = f"{completed_count[0]} of {total_count} " \ + f"({completed_count[0] * 100 // total_count}%) {cr.filename}: " + if cr.status == ConvStatus.Exists: + msg += "Destination file exists. Skipped" + skipped_files.append(cr.filename) + elif cr.status == ConvStatus.Error: + failed_files.append(cr.filename) + msg += f"Conversion failed: {cr.msg}" + else: + assert cr.status == ConvStatus.Success + msg += f"\n{cr.msg}\nConverted in " \ + f"{Durator.duration_to_hms(cr.duration)}" + print(msg) + + original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) + with multiprocessing.pool.ThreadPool( + processes=threads_arg(args.threads)) as pool: + signal.signal(signal.SIGINT, original_sigint_handler) + for src_info in src_infos: + kwargs = common_kwargs.copy() + kwargs["src_dst"] = src_info.src_dst + dest_dir = os.path.dirname(src_info.src_dst.dst) or "." + if not os.path.isdir(dest_dir): + os.makedirs(dest_dir) + kwargs["boundaries"] = src_info.boundaries + kwargs["quiet"] = args.out_dir is not None + pool.apply_async(conversion_worker, kwds=kwargs, + callback=conversionCompleter) + pool.close() + pool.join() + + if skipped_files: + print(f"{len(skipped_files)} previously existing files skipped") + if no_geoid_files: + print(f"{len(no_geoid_files)} files are out of geoid coverage") + if failed_files: + print("Following files were not converted due to errors:") + for filename in sorted(failed_files): + print(f" {filename}") + + implicit_conversion_params: List[str] = [] + if resampling != args.resampling: + implicit_conversion_params.append(f"resampling of {resampling}") + if implicit_conversion_params: + print(f"Implicitly chosen conversion parameters: " + f"{', '.join(implicit_conversion_params)}") + + print( + f"Total duration: " + f"{Durator.duration_to_hms(datetime.datetime.now() - start_time)}") + except KeyboardInterrupt: + sys.exit(1) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tools/load_tool/README.md b/tools/load_tool/README.md new file mode 100644 index 0000000..c4dc808 --- /dev/null +++ b/tools/load_tool/README.md @@ -0,0 +1,276 @@ +Copyright (C) 2022 Broadcom. All rights reserved.\ +The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate that +owns the software below. This work is licensed under the OpenAFC Project +License, a copy of which is included with this software program. + +# `afc_load_tool.py` - script for AFC load test + +## Table of contents + - [Overview](#overview) + - [Requests, messages](#requests) + - [Prerequisites](#prerequisites) + - [Population Database preparation](#population_db) + - [Config file](#config) + - [`afc_load_tool.py`](#tool) + - [`preload` subcommand](#preload) + - [`load` subcommand](#load) + - [`netload` subcommand](#netload) + - [`cache` subcommand](#cache) + - [`afc_config` subcommand](#afc_config) + - [`json_config` subcommand](#json_config) + - [Usage example](#examples) + +## Overview + +`afc_load_tool.py` is a script that sends AFC Requests to AFC Server in several parallel streams. It was designed to be used standalone. + +Several instances of this script can be used simultaneously (e.g. from different locations), however this script does not (yet?) contain means for orchestration and statistics aggregation of this use case. + +The major AFC performance bottleneck is AFC computations, so in order to avoid test results to be determined by this slowest component, this script has an ability to **preload** response cache with (fake) results so that subsequent load test would use these fake results and not be constrained by AFC computations' performance. + +This script requires Python 3.6+. It is desirable to have **pyyaml** module installed, however this requirement may be circumvented (by converting YAML config file to JSON e.g. with `json_config` subcommand or by some online converter - and then using this JSON config file on system without **pyyaml** module installed). + +## Requests, messages + +It is important to distinguish **AFC Request Message** from **AFC Request** + +**AFC Request Message** is a message sent to AFC Server. It may contain several **AFC Requests** - more than one if sent from, say, AFC Aggregator. `afc_load_tool.py` supports putting more than one request to message (by means of **`--batch`** command line parameter). + +Each AFC Request, generated by `afc_load_tool.py`, has AP coordinates and Serial Number determined by **Request Index** (an integer value). Thus it is important for set of Request Indices (**`--idx_range`** command line parameter) used during load test to be the same (or be a subset of) set of Request Indices used during preload. + +## Prerequisites + +`afc_load_tool.py` is a Python 3 script. Successfully tried on Python **3.7** (more is better, as always). + +Besides, it has the following dependencies that are not part of Python distribution: + + - **yaml** module. Required. + Typically it's already installed. If not - it can be installed with **`pip install pyyaml`** (this works even on Alpine) + - **requests** module. Optional (used for `--no-reconnect` switch). + Also typically is already installed. If not - it can be installed with **`pip install requests`** (this works even on Alpine) + +## Population Database preparation + +`load` subcommand may distribute request location proportional to population (to test AFC Engine performance in noncached mode). It does this by means of `--population ` switch, where `DATABASE_NAME` is a name of SQLite3 file. + +This SQLite file (population database) is prepared from some population density GDAL-compatible 'image' file (e.g. for USA from [2020 1X1 km population density map](https://data.worldpop.org/GIS/Population_Density/Global_2000_2020_1km/2020/USA/usa_pd_2020_1km.tif) taken from [WorldPop Hub](https://hub.worldpop.org/geodata/summary?id=39730). YMMV) + +Population database is prepared from source GDAL-compatible image file by means of *tools/geo_converters/make_population_db.py* script. This script requires GDAL libraries and utilities to be installed, so most likely one may want to run it from the container. Image for container may be built with *tools/geo_converters/Dockerfile*. To avoid muddling explanation with file mappings, access rights, etc., following text will assume that this script executed without (or completely within) the container. + +General format: + +`make_population_db.py [OPTIONS] SRC_IMAGE_FILE DST_DATABASE_FILE` + +Where options are: + +|Option|Meaning| +|------|-------| +|--resolution **ARC_SECONDS**|Resolution of resulting database in latitude/longitude seconds. Default is 60, which, as of time of this writing, seems pretty adequate| +|--center_lon_180|Better be set for countries (like USA) that exist in both east and west hemispheres. Should be determined automagically, but does not, sorry| +|--overwrite|Overwrite target database file if it exists| + +Example: + +`make_population_db.py --center_lon_180 usa_pd_2020_1km.tif usa_pd.sqlite3` + + +## Config file + +Some important constants used by script are stored outside, in config file. This is YAML (or JSON) file, by default having same name and location as script, with *.yaml* extension (on systems without **pyyaml** Python module installed - with *.json* extension). + +File content is, hopefully, self-descriptive, so here is just a brief overview of its sections. + +|Section|Content| +|-------|-------| +|defaults|Default values for performance-related script parameters (index range, number of repetitions, etc. - see commends in config file and script help message)| +|region|Region for which requests will be generated: rectangle, grid density, Ruleset ID (AFC's regulatory domain identifier), Certification ID (AFC's manufacturer identifier - must be registered in RAT DB for used Ruleset ID)| +|req_msg_pattern|AFC Request Message pattern| +|resp_msg_pattern|AFC Response Message pattern| +|paths|Points of modifications in Request/Response Message patterns| +|channels_20mhz|List of 20MHz channels, used to make preload AFC Responses unique| +|rest_api|(Semi)default URLs for REST APIs used by script| + +It is possible to use several merged together config files - e.g. have one full config file and several config files with default and/or regional parameters. + +This script is provided with default config file in YAML format (as it is more human-readable and allows for comments). On system without **pyyaml** Python m,odulke installed installed JSON config files are to be used. Conversion may be performed with `json_config` subcommand (that, of course, should be performed on system with pyyaml installed) or with some online tool. + +Since JSON is a subset of YAML (i.e. every valid JSON file is valid YAML file), JSON config files may also be used on system with pyyaml inatalled (however note that default config file name on such systems is having *.yaml* extension). + +## `afc_load_tool.py` + +General invocation format: +`afc_load_tool.py [--config [+]FILENAME] ... SUBCOMMAND [PARAMETERS]` + +`--config` may be specified several times or not at all (in which case default one will be used). If config filename prefixed with `+`, it is used as an addition (not instead of) the default config. E.g. +`afc_load_tool.py --config +brazil.yaml] ... preload` +might be used when *brazil.yaml* defines `region` section for Brazil, whereas the rest of config data comes from default config file. + +Without parameters script prints list of subcommands. Help on specific subcommand may be printed by means of `help` subcommand. E.g. help for `preload` subcommand may be printed as follows: +`afc_load_tool.py help preload` +Note that due to Python idiosyncrasies, `--help` will only print help on `--config` parameter. + +### `preload` subcommand + +Preloads Rcache with fake responses for given range of request indices. +This causes subsequent load test to test only performance of dispatcher+msghnd+rat_db+bulk_postgres, without drowning into slowness of AFC Engine. + +Parameters: + +|Parameter|Default
    in Config|Meaning| +|---------|--------------------|-------| +|--idx_range **FROM**-**TO**|0-1000000|Range of request indices to preload to cache. Note that *TO* is 'afterlast' index, i.e. 0-1000000 means [0-999999]| +|--parallel **N**|20|Number of parallel streams (may speed up operation and entertaining in general)| +|--batch **N**|1|Number of requests per message (may speed up operation and entertaining in general)| +|--backoff **SEC**|0.1|Initial backoff (in seconds) if request failed. Useful in wrestling with DDOS protection| +|--retries **N**|5|Number of retries to make if request fails. Useful in wrestling with DDOS protection| +|--status_period **N**|1000|Once in what number of requests to print intermediate statistics. 0 to not at all| +|--dry||Don't actually send anything to servers. Useful to determine overhead of this script itself| +|--comp_proj **PROJ**||Docker Compose project name (part before service name in container names) to use to determine IPs of services being used (*rat_server* and *rcache* for this subcommand)| +|--rat_server **HOST[:PORT]**|rat_server|IP Address and maybe port of rat_server service. If not specified rat_service of compose project, specified by --comp_proj is used| +|--rcache **HOST:PORT**|rcache:8000|IP Address and port of rcache service. If not specified rcache of compose project, specified by --comp_proj is used| +|--protect_cache||Protect rcache from invalidation (by ULS downloader - not doing so may divert some requests to AFC Engine during `load` and thus degrade performance). Cache may be unprotected with `cache --unprotect` subcommand| +|--no_reconnect||Every sender process establishes permanent connection to Rcache service and sends Rcache update requests over this connection. Requires `requests` Python module to be installed. Default is to establish connection on every update request send| + +### `load` subcommand
    + +Do the load test. Several processes simultaneously send AFC Request messages to dispatcher (front end) or msghnd (back end), waiting for responses. + +Parameters: + +|Parameter|Default
    in Config|Meaning| +|---------|--------------------|-------| +|--idx_range **FROM**-**TO**|0-1000000|Range of request indices to use (supposedly they have been preloaded to rcache with *preload* subcommand). Note that *TO* is 'afterlast' index, i.e. 0-1000000 means [0-999999]| +|--count **N**|1000000|Number of requests to send| +|--parallel **N**|20|Number of parallel streams| +|--batch **N**|1|Number of requests per message| +|--backoff **SEC**|0.1|Initial backoff (in seconds) if request failed. Useful in wrestling with DDOS protection| +|--retries **N**|5|Number of retries to make if request fails. Useful in wrestling with DDOS protection| +|--status_period **N**|1000|Once in what number of requests to print intermediate statistics. 0 to not at all| +|--dry||Don't actually send anything to servers. Useful to determine overhead of this script itself| +|--comp_proj **PROJ**||Docker Compose project name (part before service name in container names) to use to determine IPs of services being used (*msghnd* for this subcommand)| +|--afc **HOST[:PORT]**|msghnd:8000|IP Address andm aybe port of service to send AFC Requests to. If neither it not `--localhost` specified, requests will be sent to msghnd, determined by means of --comp_proj| +|--localhost [http\|https]||Send AFC requests to AFC http (default) or https port of AFC server (dispatcher service), running on localhost with project, specified by --comp_proj| +|--no_reconnect||Every sender process establishes permanent connection to target server and sends AFC Request messages over this connection. Requires `requests` Python module to be installed. Default is to establish connection on every update request send (which is closer to real life, but slows things and lead to different set of artifacts))| +|--no_cache||Forces recomputation of each AFC Request| +|--req **FIELD1=VALUE1[;FIELD2=VALUE2...]**||Modify AFC Request field(s). Top level is request (not message), path to deep fields is dot-separated (e.g. *location.elevation.height*), Several semicolon-separated fields may be specified (don't forget to quote such parameter) and/or this switch may be specified several times. Value may be numeric, string or list (enclosed in [] and formatted per JSON rules)| +|--population **POPULATION_DB_FILE**||Choose positions according to population density. **POPULATION_DB_FILE** is an SQLite3 file, prepared with `make_population_db.py` (see [chapter](#population_db) on it). Note that use of this parameter may lead to positions outside the shore, that AFC Engine treats as incorrect - that's life...| +|--random||Chose points randomly - according to population database (if `--population` specified) or within region rectangle in config file. Useful fro AFC Engine (`--no_cache`) testing| +|--err_dir **DIRECTORY**||Directory to write failed AFC Request messages to. By default errors are reported, but failed requests are not logged| +|--ramp_up **SECONDS**|Gradually increase load (starting streams one by one instead of all at once) for this time| + + +### `netload` subcommand
    + +Makes repeated GET requests on dispatcher, msghnd, or rcache service. +Note that report unit for this subcommand is message, whereas for `preload` and `load` commands unit is request (with a single message containing *batch* number of requests). + +Parameters: + +|Parameter|Default
    in Config|Meaning| +|---------|--------------------|-------| +|--count **N**|1000000|Number of requests to send| +|--parallel **N**|20|Number of parallel streams| +|--backoff **SEC**|0.1|Initial backoff (in seconds) if request failed. Useful in wrestling with DDOS protection| +|--retries **N**|5|Number of retries to make if request fails. Useful in wrestling with DDOS protection| +|--status_period **N**|1000|Once in what number of requests to print intermediate statistics. 0 to not at all| +|--dry||Don't actually send anything to servers. Useful to determine overhead of this script itself| +|--comp_proj **PROJ**||Docker Compose project name (part before service name in container names) to use to determine IPs of services being used| +|--afc **HOST[:PORT]**||IP Address and maybe port of target msghnd or dispatcher service. If neither it not `--localhost` specified, requests will be sent to msghnd, determined by means of --comp_proj| +|--rcache **HOST:PORT**|rcache:8000|IP Address and port of rcache service.. If not specified rat_service of compose project, specified by --comp_proj is used| +|--localhost [http\|https]||Access AFC server (i.e. dispatcher service) using http(default) or https of compose project specified by --comp_proj| +|--no_reconnect||Every sender process establishes permanent connection to target server and sends AFC Request messages over this connection. Requires `requests` Python module to be installed. Default is to establish connection on every update request send (which is closer to real life, but slows things and lead to different set of artifacts))| +|--target dispatcher\|msghnd\|rcache\||Guess attempt|What service to access. If not specified - attempt to guess is made (*dispatcher* if `--localhost`, *msghnd* if `--afc`, *rcache* if `--rcache`| + + +### `cache` subcommand
    + +Small toolset to control over rcache - functional subset of `rcache_tool.py`, put here for better accessibility. + +Parameters: + +|Parameter|Default
    in Config|Meaning| +|---------|--------------------|-------| +|--comp_proj **PROJ**||Docker Compose project name (part before service name in container names) to use to determine IPs of services being used| +|--rcache **HOST:PORT**|rcache:8000|IP Address and port of rcache service.. If not specified rat_service of compose project, specified by --comp_proj is used| +|--protect||Protect rcache from invalidation (by ULS downloader - not doing so may divert some requests to AFC Engine during `load` and thus degrade performance)| +|--unprotect||Remove Rcache protection from invalidation (normal mode of Rcache operation)| +|--invalidate||Invalidate rcache. If one need to force AFC Engine, `--no_cache` option of `load` command looks like better alternative`| + + +### `afc_config` subcommand
    + +Modify field(s) in AFC Config for current region, that will be used in subsequent tests (makes sense for AFC Engine, i.e. no cache, test) + +Parameters: + +|Parameter|Meaning| +|---------|-------| +|--comp_proj **PROJ**|Docker Compose project name (part before service name in container names) to use to determine IPs of services being used| +|--rat_server **HOST[:PORT]**|rat_server|IP Address and maybe port of rat_server service. If not specified rat_service of compose project, specified by --comp_proj is used| +|**FIELD1=VALUE1** [**FIELD2=VALUE2**...]|Fields to modify. For deep fields, path specified as comma-separated sequence of keys (e.g. *freqBands.0.startFreqMHz*)| + + +### `json_config` subcommand + +Convert YAML config file to JSON format. This command should be executed on system with **pyyaml** Python module installed. However resulting JSON file may be used on system without **pyyaml** installed. Of course, conversion may also be performed with some online tool. + +Invocation: +`afc_load_tool.py [--config [+]FILENAME.yaml] ... json_config [JSON_FILE]` + +Here `json_file`, if specified, is resulting file name. By default it has same file name and directory as script, but *.json* extension. + +## Usage example + +``` +$ docker ps +CONTAINER ID IMAGE ... PORTS NAMES +... +acffe87ff12c public.ecr.aw... 0.0.0.0:374->80/tcp, :::374->80/tcp, 0.0.0.0:458->443/tcp, :::452->443/tcp foo_l2_dispatcher_1 +bce6e9629ef1 110738915961.... 80/tcp, 443/tcp foo_l2_rat_server_1 +7f76bdee65d8 110738915961.... 8000/tcp foo_l2_msghnd_1 +152dd3a42ce7 110738915961.... foo_l2_worker_1 +d102db21067d public.ecr.aw... foo_l2_cert_db_1 +987115a71c95 public.ecr.aw... foo_l2_als_siphon_1 +08e1ec3248d2 public.ecr.aw... foo_l2_rcache_1 +803e9b266310 public.ecr.aw... 5432/tcp foo_l2_ratdb_1 +7ba8ce112cae public.ecr.aw... 5432/tcp foo_l2_bulk_postgres_1 +92fd56d4db75 public.ecr.aw... foo_l2_objst_1 +a5c680bb32b6 rcache_tool:f... foo_l2_rcache_tool_1 +871849a6e841 public.ecr.aw... 9092/tcp foo_l2_als_kafka_1 +efea7249498f public.ecr.aw... foo_l2_uls_downloader_1 +b3903f7c2d92 public.ecr.aw... 4369/tcp, 5671-5672/tcp, 15691-15692/tcp, 25672/tcp foo_l2_rmq_1 +... +``` + +Important piece of information here is the **Docker compose project name** - common prefix of container names: *foo_l2* + +Now, preloading rcache, using default parameters from config file (these parameters will be printed) and protecting it from invalidation: +`$ afc_load_tool.py preload --comp_proj foo_l2 --protect_cache` +In this command abovementioned compose project name *foo_l2* was used. + +Now, doing load test with default parameters from config file (these parameters will be printed)): + - Send AFC requests to HTTP port of AFC server on current host (i.e. to *dispatcher* service): + `$ afc_load_tool.py load --comp_proj foo_l2 --localhost` + - Send AFC requests to *msghnd* service (bypassing *dispatcher* service): + `$ afc_load_tool.py load --comp_proj foo_l2` + - Send AFC requests to explicitly specified server *foo.bar*: + `$ afc_load_tool.py load --afc foo.bar:80` + +Upon completion of testing it is recommended to re-enable cache invalidation: +`$ afc_load_tool.py cache --comp_proj foo_l2 --unprotect` + +Test AFC Server (*dispatcher* service) network performance (no `preload` or Rcache protection needed): +`$ afc_load_tool.py netload --comp_proj foo_l2 --target dispatcher` + +Test AFC Engine (no `preload` or Rcache protection needed): + - Set Max Link Distance to 200km: + `$ afc_load_tool.py afc_config --comp_proj foo_l2 maxLinkDistance=200` + - Do population-density-based testing. No batching to evaluate performance of single AFC, small report interval because AFC Engine is slo-o-o-o-ow, multistream to speed up statistics gathering: + `$ afc_load_tool.py load --random --population usa_pd.sqlite3 --comp_proj foo_l2 \` + ` --localhost --status_period 1 --batch 1 --parallel 10 --retries 0 --no_cache` + Note that this test will create some position with 'invalid coordinates' (AFC error code 103) - that's because some positions are generated 'off shore`. This is known problem and it would **not** be dealt with. + - Ditto, but with large uncertainty: + `$ afc_load_tool.py load --random --population usa_pd.sqlite3 --comp_proj foo_l2 \` + ` --localhost --status_period 1 --batch 1 --parallel 10 --retries 0 --no_cache \` + ` --req "location.elevation.verticalUncertainty=30;location.elevation.height=300" \` + ` --req "location.ellipse.minorAxis=150;location.ellipse.majorAxis=150"` diff --git a/tools/load_tool/afc_load_tool.py b/tools/load_tool/afc_load_tool.py new file mode 100755 index 0000000..627f9bf --- /dev/null +++ b/tools/load_tool/afc_load_tool.py @@ -0,0 +1,2287 @@ +#!/usr/bin/env python3 +""" AFC Load Test Tool """ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# pylint: disable=too-many-lines, too-many-arguments, invalid-name +# pylint: disable=consider-using-f-string, wrong-import-order, too-many-locals +# pylint: disable=too-few-public-methods, logging-fstring-interpolation +# pylint: disable=too-many-instance-attributes, broad-exception-caught +# pylint: disable=too-many-branches, too-many-nested-blocks +# pylint: disable=too-many-statements, eval-used + +import argparse +from collections.abc import Iterable, Iterator +import copy +import csv +import datetime +import hashlib +import http +import inspect +import json +import logging +import multiprocessing +import os +import random +try: + import requests +except ImportError: + pass +import re +import signal +import sqlite3 +import subprocess +import sys +import time +import traceback +from typing import Any, Callable, cast, List, Dict, NamedTuple, Optional, \ + Tuple, Union +import urllib.error +import urllib.parse +import urllib.request + +has_yaml = True +try: + import yaml +except ImportError: + has_yaml = False + + +def dp(*args, **kwargs) -> None: # pylint: disable=invalid-name + """Print debug message + + Arguments: + args -- Format and positional arguments. If latter present - formatted + with % + kwargs -- Keyword arguments. If present formatted with format() + """ + msg = args[0] if args else "" + if len(args) > 1: + msg = msg % args[1:] + if args and kwargs: + msg = msg.format(**kwargs) + cur_frame = inspect.currentframe() + assert (cur_frame is not None) and (cur_frame.f_back is not None) + frameinfo = inspect.getframeinfo(cur_frame.f_back) + print(f"DP {frameinfo.function}()@{frameinfo.lineno}: {msg}") + + +def error(msg: str) -> None: + """ Prints given msg as error message and exit abnormally """ + logging.error(msg) + sys.exit(1) + + +def error_if(cond: Any, msg: str) -> None: + """ If condition evaluates to true prints given msg as error message and + exits abnormally """ + if cond: + error(msg) + + +def exc_if(predicate: Any, exc: Exception) -> None: + """ Raise given exception if predicate evaluates to true """ + if predicate: + raise exc + + +def expandpath(path: Optional[str]) -> Optional[str]: + """ Expand ~ and {} in given path. For None/empty returns None/empty """ + return os.path.expandvars(os.path.expanduser(path)) if path else path + + +def unused_argument(arg: Any) -> None: # pylint: disable=unused-argument + """ Sink for all unused arguments """ + return + + +def yaml_load(yaml_s: str) -> Any: + """ YAML/JSON dictionary for given YAML/JSON content """ + kwargs: Dict[str, Any] = {} + if has_yaml: + if hasattr(yaml, "CLoader"): + kwargs["Loader"] = yaml.CLoader + elif hasattr(yaml, "FullLoader"): + kwargs["Loader"] = yaml.FullLoader + return yaml.load(yaml_s, **kwargs) + return json.loads(yaml_s) + + +class Config(Iterable): + """ Node of config structure that provides dot-based access + + Private attributes: + _data -- Data that corresponds to node (list or string-indexed dictionary) + _path -- Path to node ("foo.bar.42.baz") for use in error messages + """ + + JSON_EXT = ".json" + YAML_EXT = ".yaml" + + # Default config file name + DEFAULT_CONFIG = \ + os.path.splitext(__file__)[0] + (YAML_EXT if has_yaml else JSON_EXT) + + def __init__( + self, argv: Optional[List[str]] = None, + arg_name: Optional[str] = None, config_env: Optional[str] = None, + data: Optional[Union[List[Any], Dict[str, Any]]] = None, + path: Optional[str] = None) -> None: + """ Constructor (for both root and nonroot nodes) + + Arguments: + argv -- Argument for root node: Command line arguments + arg_name -- Argument for root node - name of command line parameter + with config + env_config -- Optional argument for root node - name of environment + variable for main config + data -- Argument for nonroot node: data that corresponds to node + (list or string-indexed dictionary) + path -- Argument for nonroot node: path to node + ("foo.bar[42].baz") for use in error messages + """ + self._data: Union[List[Any], Dict[str, Any]] + self._path: str + # Nonroot node + if data is not None: + assert path is not None + assert (argv or arg_name or config_env) is None + self._path = path + self._data = data + return + + # Root node + assert path is None + assert argv is not None + assert arg_name is not None + self._path = "" + argument_parser = argparse.ArgumentParser() + argument_parser.add_argument( + "--" + arg_name, action="append", default=[]) + configs = getattr(argument_parser.parse_known_args(argv)[0], arg_name) + # Finding root config + if (not configs) or any(c.startswith("+") for c in configs): + for config_file in \ + ([expandpath(os.environ.get(config_env))] + if config_env else []) + [self.DEFAULT_CONFIG]: + assert config_file is not None + if os.path.isfile(config_file): + configs = [config_file] + configs + break + else: + error("This script's config file not found") + self._data = {} + # Merging all configs + for config in configs: + if config.startswith("+"): + config = config[1:] + error_if(not os.path.isfile(config), + f"Config file '{config}' not found") + with open(config, encoding="utf-8") as f: + yaml_s = f.read() + try: + config_yaml_dict = yaml_load(yaml_s) + except (yaml.YAMLError if has_yaml else json.JSONDecodeError) \ + as ex: + error(f"Error reading '{config}': {repr(ex)}") + error_if(not isinstance(config_yaml_dict, dict), + f"Content of config file '{config}' is not a dictionary") + assert isinstance(config_yaml_dict, dict) + self._data = {**self._data, **config_yaml_dict} + + def __getattr__(self, attr: str) -> Any: + """ Returns given attribute of node """ + exc_if(not (isinstance(self._data, dict) and (attr in self._data)), + AttributeError(f"Config item '{self._path}' does not have " + f"attribute '{attr}'")) + assert isinstance(self._data, dict) + return self._subitem(value=self._data[attr], attr=attr) + + def __getitem__(self, key: Union[int, str]) -> Any: + """ Access by index - integer or string """ + if isinstance(self._data, list): + exc_if(not isinstance(key, (int, slice)), + IndexError(f"Config item '{self._path}' is a list, it " + f"can't be subscribed with '{key}'")) + assert isinstance(key, (int, slice)) + exc_if(isinstance(key, int) and (key >= len(self._data)), + IndexError(f"Index {key} is out of range for config item " + f"'{self._path}'")) + else: + assert isinstance(self._data, dict) + exc_if(not isinstance(key, str), + KeyError(f"Config item '{self._path}' is a dictionary, it " + f"can't be subscribed with '{key}'")) + assert isinstance(key, str) + exc_if(key not in self._data, + KeyError(f"Config item '{self._path}' does not have " + f"attribute '{key}'")) + return self._subitem(value=self._data[key], attr=key) + + def get(self, attr: str, default: Any = None) -> Any: + """ Keys of dictionary-type node """ + exc_if(not isinstance(self._data, dict), + AttributeError(f"Config item '{self._path}' is not a " + f"dictionary, can't be queried for '{attr}'")) + assert isinstance(self._data, dict) + exc_if(not isinstance(attr, str), + AttributeError(f"Non-string attribute '{attr}' can't be looked " + f"up in config item '{self._path}'")) + return self._subitem(value=self._data.get(attr, default), attr=attr) + + def __len__(self) -> int: + """ Number of subitems in node """ + return len(self._data) + + def keys(self) -> Iterable: + """ Keys of dictionary-type node """ + exc_if(not isinstance(self._data, dict), + AttributeError(f"Config item '{self._path}' is not a " + f"dictionary, it doesn't have keys")) + assert isinstance(self._data, dict) + return self._data.keys() + + def values(self) -> Iterable: + """ Values of dictionary-type node """ + exc_if(not isinstance(self._data, dict), + AttributeError(f"Config item '{self._path}' is not a " + f"dictionary, it doesn't have values")) + assert isinstance(self._data, dict) + for key, value in self._data.items(): + yield self._subitem(value=value, attr=key) + + def items(self) -> Iterable: + """ Items of dictionary-type node """ + exc_if(not isinstance(self._data, dict), + AttributeError(f"Config item '{self._path}' is not a " + f"dictionary, it doesn't have items")) + assert isinstance(self._data, dict) + for key, value in self._data.items(): + yield (key, self._subitem(value=value, attr=key)) + + def data(self) -> Union[List[Any], Dict[str, Any]]: + """ Returns underlying data structure """ + return self._data + + def __iter__(self) -> Iterator: + """ Iterator over node subitems """ + if isinstance(self._data, list): + for idx, value in enumerate(self._data): + yield self._subitem(value=value, attr=idx) + else: + assert isinstance(self._data, dict) + for key in self._data.keys(): + yield key + + def __in__(self, attr: Any) -> bool: + """ True if dictionary node contains given attribute """ + exc_if(not isinstance(self._data, dict), + AttributeError(f"Config item '{self._path}' is not a " + f"dictionary, 'in' check can't be performed")) + exc_if(not isinstance(attr, str), + AttributeError(f"Non-string attribute '{attr}' can't be looked " + f"up in config item '{self._path}'")) + return attr in self._data + + def _subitem(self, value: Any, attr: Union[int, str]) -> Any: + """ Returns given subitem as attribute of given name """ + if not isinstance(value, (list, dict)): + return value + return \ + self.__class__( + data=value, + path=f"{self._path}{'.' if self._path else ''}{attr}" + if isinstance(attr, str) else f"{self._path}[{attr}]") + + +def run_docker(args: List[str]) -> str: + """ Runs docker with given parameters, returns stdout content """ + try: + return \ + subprocess.check_output(["docker"] + args, universal_newlines=True, + encoding="utf-8") + except subprocess.CalledProcessError as ex: + error(f"Failed to run 'docker {' '.join(args)}': {repr(ex)}. Please " + "specify all hosts explicitly") + return "" # Unreachable code to make pylint happy + + +class ServiceDiscovery: + """ Container and IP discovery for services + + Private attributes: + _compose_project -- Compose project name + _containers -- Dictionary of _ContainerInfo object, indexed by service + name. None before first access + """ + class _ContainerInfo: + """ Information about single service + + Public attributes: + name -- Container name + ip -- Container IP (if known) or None + """ + + def __init__(self, name: str) -> None: + self.name = name + self.ip: Optional[str] = None + + def __init__(self, compose_project: str) -> None: + """ Constructor + + Arguments: + compose_project -- Docker compose project name + """ + self._compose_project = compose_project + self._containers: \ + Optional[Dict[str, "ServiceDiscovery._ContainerInfo"]] = None + + def get_container(self, service: str) -> str: + """ Returns container name for given service name """ + ci = self._get_cont_info(service) + return ci.name + + def get_ip(self, service: str) -> str: + """ Returns container IP for given service name """ + ci = self._get_cont_info(service) + if ci.ip: + return ci.ip + try: + inspect_dict = json.loads(run_docker(["inspect", ci.name])) + except json.JSONDecodeError as ex: + error(f"Error parsing 'docker inspect {ci.name}' output: " + f"{repr(ex)}") + try: + for net_name, net_info in \ + inspect_dict[0]["NetworkSettings"]["Networks"].items(): + if net_name.endswith("_default"): + ci.ip = net_info["IPAddress"] + break + else: + error(f"Default network not found in container '{ci.name}'") + except (AttributeError, LookupError) as ex: + error(f"Unsupported structure of 'docker inspect {ci.name}' " + f"output: {repr(ex)}") + assert ci.ip is not None + return ci.ip + + def _get_cont_info(self, service: str) \ + -> "ServiceDiscovery._ContainerInfo": + """ Returns _ContainerInfo object for given service name """ + if self._containers is None: + self._containers = {} + name_offset: Optional[int] = None + for line in run_docker(["ps"]).splitlines(): + if name_offset is None: + name_offset = line.find("NAMES") + error_if(name_offset < 0, + "Unsupported structure of 'docker ps' output") + continue + assert name_offset is not None + for names in re.split(r",\s*", line[name_offset:]): + m = re.search(r"(%s_(.+)_\d+)" % + re.escape(self._compose_project), + names) + if m: + self._containers[m.group(2)] = \ + self._ContainerInfo(name=m.group(1)) + error_if(not self._containers, + f"No running containers found for compose project " + f"'{self._compose_project}'") + ret = self._containers.get(service) + error_if(ret is None, + f"Service name '{service}' not found among containers of " + f"compose project '{self._compose_project}'") + assert ret is not None + return ret + + +def get_url(base_url: str, param_host: Optional[str], + service_discovery: Optional[ServiceDiscovery]) -> str: + """ Construct URL from base URL in config and command line host or compose + project name + + Arguments: + base_url -- Base URL from config + param_host -- Optional host name from command line parameter + service_discovery -- Optional ServiceDiscovery object. None means that + service discovery is not possible (--comp_proj was + not specified) and hostname must be provided + explicitly + Returns actionable URL with host either specified or retrieved from compose + container inspection and tail from base URL + """ + base_parts = urllib.parse.urlparse(base_url) + if param_host: + if "://" not in param_host: + param_host = f"{base_parts.scheme}://{param_host}" + param_parts = urllib.parse.urlparse(param_host) + replacements = {field: getattr(param_parts, field) + for field in base_parts._fields + if getattr(param_parts, field)} + return urllib.parse.urlunparse(base_parts._replace(**replacements)) + if service_discovery is None: + return base_url + service = base_parts.netloc.split(":")[0] + return \ + urllib.parse.urlunparse( + base_parts._replace( + netloc=service_discovery.get_ip(service) + + base_parts.netloc[len(service):])) + + +def ratdb(cfg: Config, command: str, service_discovery: ServiceDiscovery) \ + -> Union[int, List[Dict[str, Any]]]: + """ Executes SQL statement in ratdb + + Arguments: + cfg -- Config object + command -- SQL command to execute + service_discovery -- ServiceDiscovery object + Returns number of affected records for INSERT/UPDATE/DELETE, list of row + dictionaries on SELECT + """ + cont = service_discovery.get_container(cfg.ratdb.service) + result = run_docker(["exec", cont, "psql", "-U", cfg.ratdb.username, + "-d", cfg.ratdb.dbname, "--csv", "-c", command]) + if command.upper().startswith("SELECT "): + try: + return list(csv.DictReader(result.splitlines())) + except csv.Error as ex: + error(f"CSV parse error on output of '{command}': {repr(ex)}") + for begin, pattern in [("INSERT ", r"INSERT\s+\d+\s+(\d+)"), + ("UPDATE ", r"UPDATE\s+(\d+)"), + ("DELETE ", r"DELETE\s+(\d+)")]: + if command.upper().startswith(begin): + m = re.search(pattern, result) + error_if(not m, + f"Can't fetch result of '{command}'") + assert m is not None + return int(m.group(1)) + error(f"Unknown SQL command '{command}'") + return 0 # Unreachable code, appeasing pylint + + +def path_get(obj: Any, path: List[Union[str, int]]) -> Any: + """ Read value from multilevel dictionary/list structure + + Arguments: + obj -- Multilevel dictionary/list structure + path -- Sequence of indices + Returns value at the end of sequence + """ + for idx in path: + obj = obj[idx] + return obj + + +def path_set(obj: Any, path: List[Union[str, int]], value: Any) -> None: + """ Write value to multilevel dictionary/list structure + + Arguments: + obj -- Multilevel dictionary/list structure + path -- Sequence of indices + value -- Value to insert to point at the end of sequence + """ + for idx in path[: -1]: + obj = obj[idx] + obj[path[-1]] = value + + +def path_del(obj: Any, path: List[Union[str, int]]) -> Any: + """ Delete value from multilevel dictionary/list structure + + Arguments: + obj -- Multilevel dictionary/list structure + path -- Sequence of indices, value at end of it is deleted + """ + ret = obj + for idx in path[: -1]: + obj = obj[idx] + if isinstance(path[-1], int): + obj.pop(path[-1]) + else: + del obj[path[-1]] + return ret + + +class AfcReqRespGenerator: + """ Generator of AFC Request/Response messages for given request indices + + Private attributes + _paths -- Copy of cfg.paths + _req_msg_pattern -- AFC Request message pattern as JSON dictionary + _resp_msg_pattern -- AFC Response message pattern as JSON dictionary + _grid_size -- Copy of cfg.region.grid_size + _min_lat -- Copy of cfg.region.min_lat + _max_lat -- Copy of cfg.region.max_lat + _min_lon -- Copy of cfg.region.min_lon + _max_lon -- Copy of cfg.region.max_lon + _default_height -- AP height to use when there is no randomization + _channels_20mhz -- Copy of cfg._channels_20mhz + _randomize -- True to choose AP positions randomly (uniformly or + according to population density). False to use request + index (to make caching possible) + _random_height -- Formula for random height in string form. Evaluated + with 'r' local value, randomly distributed in [0, 1] + _population_db -- Population density database name. None for uniform + _db_conn -- None or SQLite3 connection + _db_cur -- None or SQLite3 cursor + + """ + + def __init__(self, cfg: Config, randomize: bool, + population_db: Optional[str], + req_msg_pattern: Optional[Dict[str, Any]]) -> None: + """ Constructor + + Arguments: + cfg -- Config object + randomize -- True to choose AP positions randomly (uniformly or + according to population density). False to use + request index (to make caching possible) + population_db -- Population density database name. None for uniform + req_msg_pattern -- Optional Request message pattern to use instead of + default + """ + self._paths = cfg.paths + self._req_msg_pattern = \ + req_msg_pattern or json.loads(cfg.req_msg_pattern) + self._resp_msg_pattern = json.loads(cfg.resp_msg_pattern) + path_set(path_get(self._req_msg_pattern, self._paths.reqs_in_msg)[0], + self._paths.ruleset_in_req, cfg.region.rulesest_id) + path_set(path_get(self._req_msg_pattern, self._paths.reqs_in_msg)[0], + self._paths.cert_in_req, cfg.region.cert_id) + path_set(path_get(self._resp_msg_pattern, self._paths.resps_in_msg)[0], + self._paths.ruleset_in_resp, cfg.region.rulesest_id) + self._grid_size = cfg.region.grid_size + self._min_lat = cfg.region.min_lat + self._max_lat = cfg.region.max_lat + self._min_lon = cfg.region.min_lon + self._max_lon = cfg.region.max_lon + self._default_height = \ + path_get( + path_get(self._req_msg_pattern, self._paths.reqs_in_msg)[0], + self._paths.height_in_req) + self._channels_20mhz = cfg.channels_20mhz + self._randomize = randomize + self._random_height = cfg.region.random_height + self._population_db = population_db + self._db_conn: Optional[sqlite3.Connection] = None + self._db_cur: Optional[sqlite3.Cursor] = None + + def request_msg(self, req_indices=Union[int, List[int]]) -> Dict[str, Any]: + """ Generate AFC Request message for given request index range + + Arguments: + req_indices -- Request index or list of request indices + Returns Request message + """ + if isinstance(req_indices, int): + req_indices = [req_indices] + assert not isinstance(req_indices, int) + + # Whole message to be + msg = copy.deepcopy(self._req_msg_pattern) + pattern_req = path_get(msg, self._paths.reqs_in_msg)[0] + # List of requests + reqs = [] + for idx_in_msg, req_idx in enumerate(req_indices): + # Individual request + req = copy.deepcopy(pattern_req) if len(req_indices) > 1 \ + else pattern_req + + path_set(req, self._paths.id_in_req, self._req_id(req_idx, + idx_in_msg)) + path_set(req, self._paths.serial_in_req, self._serial(req_idx)) + lat, lon, height = self._get_position(req_idx=req_idx) + path_set(req, self._paths.lat_in_req, lat) + path_set(req, self._paths.lon_in_req, lon) + path_set(req, self._paths.height_in_req, height) + reqs.append(req) + path_set(msg, self._paths.reqs_in_msg, reqs) + return msg + + def response_msg(self, req_idx: int) -> Dict[str, Any]: + """ Generate (fake) AFC Response message (for rcache - hence + single-item) + + Arguments: + req_idx -- Request index + Returns Fake AFC Response message + """ + # AFC Response message to be + msg = copy.deepcopy(self._resp_msg_pattern) + # Response inside it + resp = path_get(msg, self._paths.resps_in_msg)[0] + path_set(resp, self._paths.id_in_resp, self._req_id(req_idx, 0)) + # To add uniqueness to message - set 20MHz channels according to bits + # in request index binary representation + channels = [] + powers = [] + chan_idx = 0 + while req_idx: + if req_idx & 1: + channels.append(self._channels_20mhz[chan_idx]) + powers.append(30.) + chan_idx += 1 + req_idx //= 2 + path_set(resp, self._paths.var_chans_in_resp, channels) + path_set(resp, self._paths.var_pwrs_in_resp, powers) + return msg + + def _req_id(self, req_idx: int, idx_in_msg: int) -> str: + """ Request ID in message for given request index + + Arguments: + req_idx -- Request index + idx_in_msg -- Index of request in AFC message + Returns Request ID to use + """ + unused_argument(idx_in_msg) + return str(req_idx) + + def _serial(self, req_idx: int) -> str: + """ AP Serial Number for given request index """ + return f"AFC_LOAD_{req_idx:08}" + + def _get_position(self, req_idx: int) -> Tuple[float, float, float]: + """ Returns (lat_deg, lon_deg, height_m) position for given request + index """ + height = \ + eval(self._random_height, None, {"r": random.uniform(0, 1)}) \ + if self._randomize else self._default_height + if self._population_db is None: + if self._randomize: + return (random.uniform(self._min_lat, self._max_lat), + random.uniform(self._min_lon, self._max_lon), + height) + return (self._min_lat + + (req_idx // self._grid_size) * + (self._max_lat - self._min_lat) / self._grid_size, + (req_idx % self._grid_size) * + (self._max_lon - self._min_lon) / self._grid_size, + height) + if self._db_conn is None: + self._db_conn = \ + sqlite3.connect(f"file:{self._population_db}?mode=ro", + uri=True) + self._db_cur = self._db_conn.cursor() + assert self._db_cur is not None + cumulative_density = random.uniform(0, 1) if self._randomize \ + else req_idx / (self._grid_size * self._grid_size) + rows = \ + self._db_cur.execute( + f"SELECT min_lat, max_lat, min_lon, max_lon " + f"FROM population_density " + f"WHERE cumulative_density >= {cumulative_density} " + f"ORDER BY cumulative_density " + f"LIMIT 1") + min_lat, max_lat, min_lon, max_lon = rows.fetchall()[0] + if self._randomize: + return (random.uniform(min_lat, max_lat), + random.uniform(min_lon, max_lon), height) + return ((min_lat + max_lat) / 2, (min_lon + max_lon) / 2, height) + + +class RestDataHandlerBase: + """ Base class for generate/process REST API request/response data payloads + """ + + def __init__(self, cfg: Config, randomize: bool = False, + population_db: Optional[str] = None, + req_msg_pattern: Optional[Dict[str, Any]] = None) -> None: + """ Constructor + + Arguments: + cfg -- Config object + randomize -- True to choose AP positions randomly (uniformly or + according to population density). False to use + request index (to make caching possible) + population_db -- Population density database name. None for uniform + req_msg_pattern -- Optional Request message pattern to use instead of + default + """ + self.cfg = cfg + self.afc_req_resp_gen = \ + AfcReqRespGenerator( + cfg=cfg, randomize=randomize, population_db=population_db, + req_msg_pattern=req_msg_pattern) + + def make_req_data(self, req_indices: List[int]) -> bytes: + """ Abstract method that generates REST API POST data payload + + Arguments: + req_indices -- List of request indices to generate payload for + Returns POST payload as byte string + """ + unused_argument(req_indices) + raise \ + NotImplementedError( + f"{self.__class__}.make_req_data() must be implemented") + + def make_error_map(self, result_data: Optional[bytes]) -> Dict[int, str]: + """ Virtual method for error map generation + + Arguments: + result_data -- Response in form of optional bytes string + Returns error map - dictionary of error messages, indexed by request + indices. This default implementation returns empty dictionary + """ + unused_argument(result_data) + return {} + + def dry_result_data(self, batch: Optional[int]) -> bytes: + """ Virtual method returning bytes string to pass to make_error_map on + dry run. This default implementation returns empty string """ + unused_argument(batch) + return b"" + + +class PreloadRestDataHandler(RestDataHandlerBase): + """ REST API data handler for 'preload' operation - i.e. Rcache update + messages (no response) + + Private attributes: + _hash_base -- MD5 hash computed over AFC Config, awaiting AFC Request tail + """ + + def __init__(self, cfg: Config, afc_config: Dict[str, Any], + req_msg_pattern: Optional[Dict[str, Any]] = None) -> None: + """ Constructor + + Arguments: + cfg -- Config object + afc_config -- AFC Config that will be used in request hash + computation + req_msg_pattern -- Optional Request message pattern to use instead of + default + """ + self._hash_base = hashlib.md5() + self._hash_base.update(json.dumps(afc_config, + sort_keys=True).encode("utf-8")) + super().__init__(cfg=cfg, req_msg_pattern=req_msg_pattern) + + def make_req_data(self, req_indices: List[int]) -> bytes: + """ Generates REST API POST payload (RcacheUpdateReq - see + rcache_models.py, not included here) + + Arguments: + req_indices -- List of request indices to generate payload for + Returns POST payload as byte string + """ + rrks = [] + for req_idx in req_indices: + req_msg = self.afc_req_resp_gen.request_msg(req_indices=[req_idx]) + resp_msg = self.afc_req_resp_gen.response_msg(req_idx=req_idx) + hash_req = \ + copy.deepcopy(path_get(req_msg, self.cfg.paths.reqs_in_msg)[0]) + path_del(hash_req, self.cfg.paths.id_in_req) + h = self._hash_base.copy() + h.update(json.dumps(hash_req, sort_keys=True).encode("utf-8")) + rrks.append({"afc_req": json.dumps(req_msg), + "afc_resp": json.dumps(resp_msg), + "req_cfg_digest": h.hexdigest()}) + return json.dumps({"req_resp_keys": rrks}).encode("utf-8") + + +class LoadRestDataHandler(RestDataHandlerBase): + """ REST API data handler for 'load' operation - i.e. AFC Request/Response + messages """ + + def __init__(self, cfg: Config, randomize: bool, + population_db: Optional[str], + req_msg_pattern: Optional[Dict[str, Any]] = None) -> None: + """ Constructor + + Arguments: + cfg -- Config object + randomize -- True to choose AP positions randomly (uniformly or + according to population density). False to use + request index (to make caching possible) + population_db -- Population density database name. None for uniform + req_msg_pattern -- Optional Request message pattern to use instead of + default + """ + super().__init__(cfg=cfg, randomize=randomize, + population_db=population_db, + req_msg_pattern=req_msg_pattern) + + def make_req_data(self, req_indices: List[int]) -> bytes: + """ Generates REST API POST payload (AFC Request message) + + Arguments: + req_indices -- Request indices to generate payload for + Returns POST payload as byte string + """ + return \ + json.dumps( + self.afc_req_resp_gen.request_msg(req_indices=req_indices)).\ + encode("utf-8") + + def make_error_map(self, result_data: Optional[bytes]) -> Dict[int, str]: + """ Generate error map for given AFC Response message + + Arguments: + result_data -- AFC Response as byte string + Returns error map - dictionary of error messages, indexed by request + indices + """ + paths = self.cfg.paths + ret: Dict[int, str] = {} + for resp in path_get(json.loads(result_data or b""), + paths.resps_in_msg): + if path_get(resp, paths.code_in_resp): + ret[int(path_get(resp, paths.id_in_resp))] = \ + str(path_get(resp, paths.response_in_resp)) + return ret + + def dry_result_data(self, batch: Optional[int]) -> bytes: + """ Returns byte string, containing AFC Response to parse on dry run + """ + assert batch is not None + resp_msg = json.loads(self.cfg.resp_msg_pattern) + path_set(resp_msg, self.cfg.paths.resps_in_msg, + path_get(resp_msg, self.cfg.paths.resps_in_msg) * batch) + return json.dumps(resp_msg).encode("utf-8") + + +# POST Worker request data and supplementary information +PostWorkerReqInfo = \ + NamedTuple("PostWorkerReqInfo", + [ + # Request indices, contained in REST API request data + ("req_indices", List[int]), + # REST API Request data + ("req_data", bytes)]) + + +# GET Worker request data and supplementary information +GetWorkerReqInfo = \ + NamedTuple("GetWorkerReqInfo", + [ + # Number of GET requests to send + ("num_gets", int)]) + + +# REST API request results +class WorkerResultInfo(NamedTuple): + """ Data, returned by REST API workers in result queue """ + # Retries made (0 - from first attempt) + retries: int + # CPU time consumed by worker in nanoseconds. Negative if not available + worker_cpu_consumed_ns: int + # Time consumed in processing of current request in second + req_time_spent_sec: float + # Request indices. None for netload + req_indices: Optional[List[int]] = None + # REST API Response data (None if error or N/A) + result_data: Optional[bytes] = None + # Error message for failed requests, None for succeeded + error_msg: Optional[str] = None + # Optional request data + req_data: Optional[bytes] = None + + +# Message from Tick worker for EMA rate computation +TickInfo = NamedTuple("TickInfo", [("tick", int)]) + +# Type for result queue items +ResultQueueDataType = Optional[Union[WorkerResultInfo, TickInfo]] + + +class Ticker: + """ Second ticker (used for EMA computations). Puts TickInfo to result + queue + + Private attributes: + _worker -- Tick worker process (generates TickInfo once per second) + """ + + def __init__(self, + result_queue: "multiprocessing.Queue[ResultQueueDataType]") \ + -> None: + """ Constructor + + Arguments: + result_queue -- Queue for tick worker to put TickInfo objects + """ + self._worker = \ + multiprocessing.Process(target=Ticker._tick_worker, + kwargs={"result_queue": result_queue}) + self._worker.start() + + @classmethod + def _tick_worker( + cls, result_queue: "multiprocessing.Queue[ResultQueueDataType]") \ + -> None: + """ Tick worker process + + Arguments: + result_queue -- Queue to put TickInfo to + """ + count = 0 + while True: + time.sleep(1) + count += 1 + result_queue.put(TickInfo(tick=count)) + + def stop(self) -> None: + """ Stops tick worker """ + self._worker.terminate() + + +class RateEma: + """ Exponential Moving Average for rate of change + + Public attributes: + rate_ema -- Rate Average computed on last tick + + Private attributes: + _weight -- Weight for EMA computation + _prev_value -- Value on previous tick + """ + + def __init__(self, win_size_sec: float = 20) -> None: + """ Constructor + + Arguments: + result_queue -- Queue for tick worker to put TickInfo objects + win_size_sec -- Averaging window size in seconds + """ + self._weight = 2 / (win_size_sec + 1) + self.rate_ema: float = 0 + self._prev_value: float = 0 + + def on_tick(self, new_value: float) -> None: + """ Call on arrival of TickInfo message + + Arguments: + new_value - Measured data value on this tick + """ + increment = new_value - self._prev_value + self._prev_value = new_value + self.rate_ema += self._weight * (increment - self.rate_ema) + + +class StatusPrinter: + """ Prints status on a single line: + + Private attributes: + _prev_len -- Length of previously printed line + """ + + def __init__(self) -> None: + """ Constructor + + Arguments: + enabled -- True if status print is enabled + """ + self._prev_len = 0 + + def pr(self, s: Optional[str] = None) -> None: + """ Print string (if given) or cleans up after previous print """ + if s is None: + if self._prev_len: + print(" " * self._prev_len, flush=True) + self._prev_len = 0 + else: + print(s + " " * (max(0, self._prev_len - len(s))), end="\r", + flush=True) + self._prev_len = len(s) + + +class ResultsProcessor: + """ Prints important summary of request execution results + + Private attributes: + _netload -- True for netload testing, False for preload/load + testing + _total_requests -- Total number of individual requests (not batches) + that will be executed + _result_queue -- Queue with execution results, second ticks, Nones + for completed workers + _status_period -- Period of status printing (in terms of request + count), e.g. 1000 for once in 1000 requests, 0 to + not print status (except in the end) + _rest_data_handler -- Generator/interpreter of REST request/response + data. None for netload test + _num_workers -- Number of worker processes + _err_dir -- None or directory for failed requests + _status_printer -- StatusPrinter + """ + + def __init__( + self, netload: bool, total_requests: int, + result_queue: "multiprocessing.Queue[ResultQueueDataType]", + num_workers: int, status_period: int, + rest_data_handler: Optional[RestDataHandlerBase], + err_dir: Optional[str]) -> None: + """ Constructor + + Arguments: + netload -- True for netload testing, False for preload/load + testing + total_requests -- Total number of individual requests (not batches) + that will be executed + result_queue -- Queue with execution results, Nones for completed + workers + num_workers -- Number of worker processes + status_period -- Period of status printing (in terms of request + count), e.g. 1000 for once in 1000 requests, 0 to + not print status (except in the end) + rest_data_handler -- Generator/interpreter of REST request/response + data + err_dir -- None or directory for failed requests + """ + self._netload = netload + self._total_requests = total_requests + self._result_queue = result_queue + self._status_period = status_period + self._num_workers = num_workers + self._rest_data_handler = rest_data_handler + self._err_dir = err_dir + self._status_printer = StatusPrinter() + + def process(self) -> None: + """ Keep processing results until all worker will stop """ + start_time = datetime.datetime.now() + requests_sent: int = 0 + requests_failed: int = 0 + retries: int = 0 + cpu_consumed_ns: int = 0 + time_spent_sec: float = 0 + req_rate_ema = RateEma() + cpu_consumption_ema = RateEma() + + def status_message(intermediate: bool) -> str: + """ Returns status message (intermediate or final) """ + now = datetime.datetime.now() + elapsed = now - start_time + elapsed_sec = elapsed.total_seconds() + elapsed_str = re.sub(r"\.\d*$", "", str(elapsed)) + global_req_rate = requests_sent / elapsed_sec if elapsed_sec else 0 + + cpu_consumption: float + if cpu_consumed_ns < 0: + cpu_consumption = -1 + elif intermediate: + cpu_consumption = cpu_consumption_ema.rate_ema + elif elapsed_sec: + cpu_consumption = cpu_consumed_ns * 1e-9 / elapsed_sec + else: + cpu_consumption = 0 + + if requests_sent: + req_duration = time_spent_sec / requests_sent + req_duration_str = \ + f"{req_duration:.3g} sec" if req_duration > 0.1 \ + else f"{req_duration * 1000:.3g} ms" + else: + req_duration_str = "unknown" + ret = \ + f"{'Progress: ' if intermediate else ''}" \ + f"{requests_sent} requests completed " \ + f"({requests_sent * 100 / self._total_requests:.1f}%), " \ + f"{requests_failed} failed " \ + f"({requests_failed * 100 / (requests_sent or 1):.3f}%), " \ + f"{retries} retries made. " \ + f"{elapsed_str} elapsed, " \ + f"rate is {global_req_rate:.3f} req/sec " \ + f"(avg req proc time {req_duration_str})" + if cpu_consumption >= 0: + ret += f", CPU consumption is {cpu_consumption:.3f}" + if intermediate and elapsed_sec and requests_sent: + total_duration = \ + datetime.timedelta( + seconds=self._total_requests * elapsed_sec / + requests_sent) + eta_dt = start_time + total_duration + tta_sec = int((eta_dt - now).total_seconds()) + tta: str + if tta_sec < 60: + tta = f"{tta_sec} seconds" + elif tta_sec < 3600: + tta = f"{tta_sec // 60} minutes" + else: + tta_minutes = tta_sec // 60 + tta = \ + f"{tta_minutes // 60} hours {tta_minutes % 60} minutes" + ret += f", current rate is " \ + f"{req_rate_ema.rate_ema:.3f} req/sec. " \ + f"ETA: {eta_dt.strftime('%X')} (in {tta})" + return ret + + try: + while True: + result_info = self._result_queue.get() + if result_info is None: + self._num_workers -= 1 + if self._num_workers == 0: + break + continue + if isinstance(result_info, TickInfo): + req_rate_ema.on_tick(requests_sent) + cpu_consumption_ema.on_tick(cpu_consumed_ns * 1e-9) + continue + error_msg = result_info.error_msg + if error_msg: + self._status_printer.pr() + if self._netload: + indices_clause = "" + else: + assert result_info.req_indices is not None + indices = \ + ", ".join(str(i) for i in result_info.req_indices) + indices_clause = f" with indices ({indices})" + logging.error( + f"Request{indices_clause} failed: {error_msg}") + + error_map: Dict[int, str] = {} + if self._rest_data_handler is not None: + if error_msg is None: + try: + error_map = \ + self._rest_data_handler.make_error_map( + result_data=result_info.result_data) + except Exception as ex: + error_msg = f"Error decoding message " \ + f"{result_info.result_data!r}: {repr(ex)}" + for idx, error_msg in error_map.items(): + self._status_printer.pr() + logging.error(f"Request {idx} failed: {error_msg}") + + prev_sent = requests_sent + num_requests = \ + 1 if self._netload \ + else len(cast(List[int], result_info.req_indices)) + requests_sent += num_requests + retries += result_info.retries + if result_info.error_msg: + requests_failed += num_requests + else: + requests_failed += len(error_map) + if self._err_dir and result_info.req_data and \ + (result_info.error_msg or error_map): + try: + filename = \ + os.path.join( + self._err_dir, + datetime.datetime.now().strftime( + "err_req_%y%m%d_%H%M%S_%f.json")) + with open(filename, "wb") as f: + f.write(result_info.req_data) + except OSError as ex: + error(f"Failed to write failed request file " + f"'{filename}': {repr(ex)}") + cpu_consumed_ns += result_info.worker_cpu_consumed_ns + time_spent_sec += result_info.req_time_spent_sec + + if self._status_period and \ + ((prev_sent // self._status_period) != + (requests_sent // self._status_period)): + self._status_printer.pr(status_message(intermediate=True)) + finally: + self._status_printer.pr() + logging.info(status_message(intermediate=False)) + + def _print(self, s: str, newline: bool, is_error: bool) -> None: + """ Print message + + Arguments: + s -- Message to print + newline -- True to go to new line, False to remain on same line + """ + if newline: + self._status_printer.pr() + (logging.error if is_error else logging.info)(s) + else: + self._status_printer.pr(s) + + +def post_req_worker( + url: str, retries: int, backoff: float, dry: bool, + post_req_queue: multiprocessing.Queue, + result_queue: "multiprocessing.Queue[ResultQueueDataType]", + dry_result_data: Optional[bytes], use_requests: bool, + return_requests: bool = False, delay_sec: float = 0) -> None: + """ REST API POST worker + + Arguments: + url -- REST API URL to send POSTs to + retries -- Number of retries + backoff -- Initial backoff window in seconds + dry -- True to dry run + post_req_queue -- Request queue. Elements are PostWorkerReqInfo objects + corresponding to single REST API post or None to stop + operation + result_queue -- Result queue. Elements added are WorkerResultInfo for + operation results, None to signal that worker finished + use_requests -- True to use requests, False to use urllib.request + return_requests -- True to return requests in WorkerResultInfo + delay_sec -- Delay start by this number of seconds + """ + try: + time.sleep(delay_sec) + session: Optional[requests.Session] = \ + requests.Session() if use_requests and (not dry) else None + has_proc_time = hasattr(time, "process_time_ns") + prev_proc_time_ns = time.process_time_ns() if has_proc_time else 0 + while True: + req_info: PostWorkerReqInfo = post_req_queue.get() + if req_info is None: + result_queue.put(None) + return + + start_time = datetime.datetime.now() + error_msg = None + if dry: + result_data = dry_result_data + attempts = 1 + else: + result_data = None + last_error: Optional[str] = None + for attempt in range(retries + 1): + if use_requests: + assert session is not None + try: + resp = \ + session.post( + url=url, data=req_info.req_data, + headers={ + "Content-Type": + "application/json; charset=utf-8", + "Content-Length": + str(len(req_info.req_data))}, + timeout=180) + if not resp.ok: + last_error = \ + f"{resp.status_code}: {resp.reason}" + continue + result_data = resp.content + break + except requests.RequestException as ex: + last_error = repr(ex) + else: + req = urllib.request.Request(url) + req.add_header("Content-Type", + "application/json; charset=utf-8") + req.add_header("Content-Length", + str(len(req_info.req_data))) + try: + with urllib.request.urlopen( + req, req_info.req_data, timeout=180) as f: + result_data = f.read() + break + except (urllib.error.HTTPError, urllib.error.URLError, + urllib.error.ContentTooShortError, OSError) \ + as ex: + last_error = repr(ex) + time.sleep( + random.uniform(0, (1 << attempt)) * backoff) + else: + error_msg = last_error + attempts = attempt + 1 + new_proc_time_ns = time.process_time_ns() if has_proc_time else 0 + result_queue.put( + WorkerResultInfo( + req_indices=req_info.req_indices, retries=attempts - 1, + result_data=result_data, error_msg=error_msg, + worker_cpu_consumed_ns=(new_proc_time_ns - + prev_proc_time_ns) + if has_proc_time else -1, + req_time_spent_sec=(datetime.datetime.now() - start_time). + total_seconds(), + req_data=req_info.req_data if return_requests and (not dry) + else None)) + prev_proc_time_ns = new_proc_time_ns + except Exception as ex: + logging.error(f"Worker failed: {repr(ex)}\n" + f"{traceback.format_exc()}") + result_queue.put(None) + + +def get_req_worker( + url: str, expected_code: Optional[int], retries: int, backoff: float, + dry: bool, get_req_queue: multiprocessing.Queue, + result_queue: "multiprocessing.Queue[ResultQueueDataType]", + use_requests: bool) -> None: + """ REST API GET worker + + Arguments: + url -- REST API URL to send POSTs to + expected_code -- None or expected non-200 status code + retries -- Number of retries + backoff -- Initial backoff window in seconds + dry -- True to dry run + get_req_queue -- Request queue. Elements are GetWorkerReqInfo objects + corresponding to bunch of single REST API GETs or None to + stop operation + result_queue -- Result queue. Elements added are WorkerResultInfo for + operation results, None to signal that worker finished + use_requests -- True to use requests, False to use urllib.request + """ + try: + if expected_code is None: + expected_code = http.HTTPStatus.OK.value + has_proc_time = hasattr(time, "process_time_ns") + prev_proc_time_ns = time.process_time_ns() if has_proc_time else 0 + session: Optional[requests.Session] = \ + requests.Session() if use_requests and (not dry) else None + while True: + req_info: GetWorkerReqInfo = get_req_queue.get() + if req_info is None: + result_queue.put(None) + return + start_time = datetime.datetime.now() + for _ in range(req_info.num_gets): + error_msg = None + if dry: + attempts = 1 + else: + last_error: Optional[str] = None + for attempt in range(retries + 1): + if use_requests: + assert session is not None + try: + resp = session.get(url=url, timeout=30) + if resp.status_code != expected_code: + last_error = \ + f"{resp.status_code}: {resp.reason}" + continue + break + except requests.RequestException as ex: + last_error = repr(ex) + else: + req = urllib.request.Request(url) + status_code: Optional[int] = None + status_reason: str = "" + try: + with urllib.request.urlopen(req, timeout=30): + pass + status_code = http.HTTPStatus.OK.value + status_reason = http.HTTPStatus.OK.name + except urllib.error.HTTPError as http_ex: + status_code = http_ex.code + status_reason = http_ex.reason + except (urllib.error.URLError, + urllib.error.ContentTooShortError) as ex: + last_error = repr(ex) + if status_code is not None: + if status_code == expected_code: + break + last_error = f"{status_code}: {status_reason}" + time.sleep( + random.uniform(0, (1 << attempt)) * backoff) + else: + error_msg = last_error + attempts = attempt + 1 + new_proc_time_ns = \ + time.process_time_ns() if has_proc_time else 0 + result_queue.put( + WorkerResultInfo( + retries=attempts - 1, error_msg=error_msg, + worker_cpu_consumed_ns=(new_proc_time_ns - + prev_proc_time_ns) + if has_proc_time else -1, + req_time_spent_sec=(datetime.datetime.now() - + start_time). + total_seconds(),)) + prev_proc_time_ns = new_proc_time_ns + except Exception as ex: + logging.error(f"Worker failed: {repr(ex)}\n" + f"{traceback.format_exc()}") + result_queue.put(None) + + +def get_idx_range(idx_range_arg: str) -> Tuple[int, int]: + """ Parses --idx_range command line parameter to index range tuple """ + parts = idx_range_arg.split("-", maxsplit=1) + try: + ret = (0, int(parts[0])) if len(parts) == 1 \ + else (int(parts[0]), int(parts[1])) + error_if(ret[0] >= ret[1], f"Invalid index range: {idx_range_arg}") + return ret + except ValueError as ex: + error(f"Invalid index range syntax: {repr(ex)}") + return (0, 0) # Appeasing pylint, will never happen + + +def producer_worker( + count: Optional[int], batch: int, parallel: int, + req_queue: multiprocessing.Queue, netload: bool = False, + min_idx: Optional[int] = None, max_idx: Optional[int] = None, + rest_data_handler: Optional[RestDataHandlerBase] = None) -> None: + """ POST Producer (request queue filler) + + Arguments: + batch -- Batch size (number of requests per queue element) + min_idx -- Minimum request index. None for netload + max_idx -- Aftremaximum request index. None for netload + count -- Optional count of requests to send. None means that + requests will be sent sequentially according to range. + If specified - request indices will be randomized + parallel -- Number of worker processes to use + dry -- Dry run + rest_data_handler -- Generator/interpreter of REST request/response data + req_queue -- Requests queue to fill + """ + try: + if netload: + assert count is not None + for min_count in range(0, count, batch): + req_queue.put( + GetWorkerReqInfo(num_gets=min(count - min_count, batch))) + elif count is not None: + assert (min_idx is not None) and (max_idx is not None) and \ + (rest_data_handler is not None) + for min_count in range(0, count, batch): + req_indices = \ + [random.randrange(min_idx, max_idx) + for _ in range(min(count - min_count, batch))] + req_queue.put( + PostWorkerReqInfo( + req_indices=req_indices, + req_data=rest_data_handler.make_req_data(req_indices))) + else: + assert (min_idx is not None) and (max_idx is not None) and \ + (rest_data_handler is not None) + for min_req_idx in range(min_idx, max_idx, batch): + req_indices = list(range(min_req_idx, + min(min_req_idx + batch, max_idx))) + req_queue.put( + PostWorkerReqInfo( + req_indices=req_indices, + req_data=rest_data_handler.make_req_data(req_indices))) + except Exception as ex: + error(f"Producer terminated: {repr(ex)}") + finally: + for _ in range(parallel): + req_queue.put(None) + + +def run(url: str, parallel: int, backoff: int, retries: int, dry: bool, + status_period: int, batch: int, + rest_data_handler: Optional[RestDataHandlerBase] = None, + netload_target: Optional[str] = None, + expected_code: Optional[int] = None, min_idx: Optional[int] = None, + max_idx: Optional[int] = None, count: Optional[int] = None, + use_requests: bool = False, err_dir: Optional[str] = None, + ramp_up: Optional[float] = None, randomize: Optional[bool] = None, + population_db: Optional[str] = None) -> None: + """ Run the POST operation + + Arguments: + rest_data_handler -- REST API payload data generator/interpreter. None for + netload + url -- REST API URL to send POSTs to (GET in case of netload) + parallel -- Number of worker processes to use + backoff -- Initial size of backoff windows in seconds + retries -- Number of retries + dry -- True to dry run + status_period -- Period (in terms of count) of status message prints (0 + to not at all) + batch -- Batch size (number of requests per element of request + queue) + netload_target -- None for POST test, tested destination for netload + test + expected_code -- None or non-200 netload test HTTP status code + min_idx -- Minimum request index. None for netload test + max_idx -- Aftermaximum request index. None for netload test + count -- Optional count of requests to send. None means that + requests will be sent sequentially according to range. + If specified - request indices will be randomized + use_requests -- Use requests to send requests (default is to use + urllib.request) + err_dir -- None or directory for failed requests + ramp_up -- Ramp up parallel streams for this number of seconds + randomize -- True to random points, False for predefined points, + None if irrelevant. Only used in banner printing + population_db -- Population database file name or None. Only used for + banner printing + """ + error_if(use_requests and ("requests" not in sys.modules), + "'requests' Python3 module have to be installed to use " + "'--no_reconnect' option") + total_requests = count if count is not None \ + else (cast(int, max_idx) - cast(int, min_idx)) + if netload_target: + logging.info(f"Netload test of {netload_target}") + logging.info(f"URL: {'N/A' if dry else url}") + logging.info(f"Streams: {parallel}") + logging.info(f"Backoff: {backoff} sec, {retries} retries") + if not netload_target: + logging.info(f"Index range: {min_idx:_} - {max_idx:_}") + logging.info(f"Batch size: {batch}") + logging.info(f"Requests to send: {total_requests:_}") + logging.info(f"Nonreconnect mode: {use_requests}") + if ramp_up is not None: + logging.info(f"Ramp up for: {ramp_up} seconds") + if status_period: + logging.info(f"Intermediate status is printed every " + f"{status_period} requests completed") + else: + logging.info("Intermediate status is not printed") + if err_dir: + logging.info(f"Directory for failed requests: {err_dir}") + if randomize is not None: + logging.info( + f"Point selection is {'random' if randomize else 'predefined'}") + if population_db: + logging.info(f"Point density chosen according to population database: " + f"{population_db}") + if dry: + logging.info("Dry mode") + + req_queue: multiprocessing.Queue = multiprocessing.Queue() + result_queue: "multiprocessing.Queue[ResultQueueDataType]" = \ + multiprocessing.Queue() + workers: List[multiprocessing.Process] = [] + original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) + ticker: Optional[Ticker] = None + if err_dir: + try: + os.makedirs(err_dir, exist_ok=True) + except OSError as ex: + error(f"Failed to create directory '{err_dir}': {repr(ex)}") + try: + req_worker_kwargs: Dict[str, Any] = { + "url": url, "backoff": backoff, "retries": retries, + "dry": dry, "result_queue": result_queue, + "use_requests": use_requests} + req_worker: Callable + if netload_target: + req_worker = get_req_worker + req_worker_kwargs["get_req_queue"] = req_queue + req_worker_kwargs["expected_code"] = expected_code + else: + assert rest_data_handler is not None + req_worker = post_req_worker + req_worker_kwargs["post_req_queue"] = req_queue + req_worker_kwargs["dry_result_data"] = \ + rest_data_handler.dry_result_data(batch) + req_worker_kwargs["return_requests"] = err_dir is not None + for idx in range(parallel): + kwargs = dict(req_worker_kwargs) + if ramp_up is not None: + kwargs["delay_sec"] = \ + (idx * ramp_up / (parallel - 1)) if idx else 0 + + workers.append( + multiprocessing.Process(target=req_worker, kwargs=kwargs)) + workers[-1].start() + workers.append( + multiprocessing.Process( + target=producer_worker, + kwargs={"netload": netload_target is not None, + "min_idx": min_idx, "max_idx": max_idx, + "count": count, "batch": batch, + "parallel": parallel, + "rest_data_handler": rest_data_handler, + "req_queue": req_queue})) + workers[-1].start() + ticker = Ticker(result_queue) + signal.signal(signal.SIGINT, original_sigint_handler) + results_processor = \ + ResultsProcessor( + netload=netload_target is not None, + total_requests=total_requests, result_queue=result_queue, + num_workers=parallel, status_period=status_period, + rest_data_handler=rest_data_handler, err_dir=err_dir) + results_processor.process() + for worker in workers: + worker.join() + finally: + for worker in workers: + if worker.is_alive(): + worker.terminate() + if ticker: + ticker.stop() + + +def wait_rcache_flush(cfg: Config, args: Any, + service_discovery: Optional[ServiceDiscovery]) -> None: + """ Waiting for rcache preload stuff flushing to DB + + Arguments: + cfg -- Config object + args -- Parsed command line arguments + service_discovery -- Optional ServiceDiscovery object + """ + logging.info("Waiting for updates to flush to DB") + start_time = datetime.datetime.now() + start_queue_len: Optional[int] = None + prev_queue_len: Optional[int] = None + status_printer = StatusPrinter() + rcache_status_url = \ + get_url(base_url=cfg.rest_api.rcache_status.url, + param_host=args.rcache, service_discovery=service_discovery) + while True: + try: + with urllib.request.urlopen(rcache_status_url, timeout=30) as f: + rcache_status = json.loads(f.read()) + except (urllib.error.HTTPError, urllib.error.URLError) as ex: + error(f"Error retrieving Rcache status '{cfg.region.rulesest_id}' " + f"using URL get_config.url : {repr(ex)}") + queue_len: int = rcache_status["update_queue_len"] + if not rcache_status["update_queue_len"]: + status_printer.pr() + break + if start_queue_len is None: + start_queue_len = prev_queue_len = queue_len + elif (args.status_period is not None) and \ + ((cast(int, prev_queue_len) - queue_len) >= + args.status_period): + now = datetime.datetime.now() + elapsed_sec = (now - start_time).total_seconds() + written = start_queue_len - queue_len + total_duration = \ + datetime.timedelta( + seconds=start_queue_len * elapsed_sec / written) + eta_dt = start_time + total_duration + status_printer.pr( + f"{queue_len} records not yet written. " + f"Write rate {written / elapsed_sec:.2f} rec/sec. " + f"ETA {eta_dt.strftime('%X')} (in " + f"{int((eta_dt - now).total_seconds()) // 60} minutes)") + time.sleep(1) + status_printer.pr() + now = datetime.datetime.now() + elapsed_sec = (now - start_time).total_seconds() + elapsed_str = re.sub(r"\.\d*$", "", str((now - start_time))) + msg = f"Flushing took {elapsed_str}" + if start_queue_len is not None: + msg += f". Flush rate {start_queue_len / elapsed_sec: .2f} rec/sec" + logging.info(msg) + + +def get_afc_config(cfg: Config, args: Any, + service_discovery: Optional[ServiceDiscovery]) \ + -> Dict[str, Any]: + """ Retrieve AFC Config for configured Ruleset ID + + Arguments: + cfg -- Config object + args -- Parsed command line arguments + service_discovery -- Optional ServiceDiscovery object + Returns AFC Config as dictionary + """ + get_config_url = \ + get_url(base_url=cfg.rest_api.get_config.url, + param_host=getattr(args, "rat_server", None), + service_discovery=service_discovery) + try: + get_config_url += cfg.region.rulesest_id + with urllib.request.urlopen(get_config_url, timeout=30) as f: + afc_config_str = f.read().decode('utf-8') + except (urllib.error.HTTPError, urllib.error.URLError) as ex: + error(f"Error retrieving AFC Config for Ruleset ID " + f"'{cfg.region.rulesest_id}' using URL get_config.url: " + f"{repr(ex)}") + try: + return json.loads(afc_config_str) + except json.JSONDecodeError as ex: + error(f"Error decoding AFC Config JSON: {repr(ex)}") + return {} # Unreachable code to appease pylint + + +def patch_json(patch_arg: Optional[List[str]], json_dict: Dict[str, Any], + data_type: str, new_type: Optional[str] = None) \ + -> Dict[str, Any]: + """ Modify JSON object with patches from command line + + Arguments: + patch_arg -- Optional list of FIELD1=VALUE1[,FIELD2=VALUE2...] patches + json_dict -- JSON dictionary to modify + data_type -- Patch of what - to be used in error messages + new_type -- None or type of new values for previously nonexisted keys + returns modified dictionary + """ + if not patch_arg: + return json_dict + ret = copy.deepcopy(json_dict) + for patches in patch_arg: + for patch in patches.split(";"): + error_if("=" not in patch, + f"Invalid syntax: {data_type} setting '{patch}' doesn't " + f"have '='") + field, value = patch.split("=", 1) + new_key = False + super_obj: Any = None + obj: Any = ret + last_idx: Any = None + idx: Any + for idx in field.split("."): + super_obj = obj + if re.match(r"^\d+$", idx): + int_idx = int(idx) + error_if(not isinstance(obj, list), + f"Integer index '{idx}' in {data_type} setting " + f"'{patch}' is applied to nonlist entity") + error_if(not (0 <= int_idx < len(obj)), + f"Integer index '{idx}' in {data_type} setting " + f"'{patch}' is outside of [0, {len(obj)}[ valid " + f"range") + idx = int_idx + else: + error_if(not isinstance(obj, dict), + f"Key '{idx}' in {data_type} setting '{patch}' " + f"can't be applied to nondictionary entity") + if idx not in obj: + error_if(not new_type, + f"Key '{idx}' of setting '{patch}' not found " + f"in {data_type}") + obj[idx] = {} + new_key = True + obj = obj[idx] + last_idx = idx + error_if( + (isinstance(obj, dict) and (not new_key)) or + (isinstance(obj, list) and (not value.startswith("["))), + f"'{field}' of {data_type} setting '{patch}' does not address " + f"scalar value") + try: + if isinstance(obj, int) or (new_key and (new_type == "int")): + try: + super_obj[last_idx] = int(value) + except ValueError: + if new_key and (new_type == "int"): + raise + super_obj[last_idx] = float(value) + elif isinstance(obj, float) or \ + (new_key and (new_type == "float")): + super_obj[last_idx] = float(value) + elif isinstance(obj, bool) or \ + (new_key and (new_type == "bool")): + if value.lower() in ("1", "y", "t", "yes", "true", "+"): + super_obj[last_idx] = True + elif value.lower() in ("0", "n", "f", "no", "false", "-"): + super_obj[last_idx] = False + else: + raise TypeError(f"'{value}' is bot a valid boolean " + "representation") + elif isinstance(obj, list) or \ + (new_key and (new_type == "list")): + super_obj[last_idx] = json.loads(value) + else: + super_obj[last_idx] = value + except (TypeError, ValueError, json.JSONDecodeError) as ex: + error(f"'{value}' of {data_type} setting '{patch}' has " + f"invalid type/formatting: {repr(ex)}") + return ret + + +def patch_req(cfg: Config, args_req: Optional[List[str]]) -> Dict[str, Any]: + """ Get AFC Request pattern, patched according to --req switch + + Arguments: + cfg -- Config object + args_req -- Optional --req switch value + Returns patched AFC Request pattern + """ + req_msg_dict = json.loads(cfg.req_msg_pattern) + req_dict = path_get(obj=req_msg_dict, path=cfg.paths.reqs_in_msg)[0] + req_dict = patch_json(patch_arg=args_req, json_dict=req_dict, + data_type="AFC Request") + path_set(obj=req_msg_dict, path=cfg.paths.reqs_in_msg, value=[req_dict]) + return req_msg_dict + + +def do_preload(cfg: Config, args: Any) -> None: + """ Execute "preload" command. + + Arguments: + cfg -- Config object + args -- Parsed command line arguments + """ + if args.dry: + afc_config = {} + worker_url = "" + else: + service_discovery = None if args.comp_proj is None \ + else ServiceDiscovery(compose_project=args.comp_proj) + if args.protect_cache: + try: + with urllib.request.urlopen( + urllib.request.Request( + get_url(base_url=cfg.rest_api.protect_rcache.url, + param_host=args.rcache, + service_discovery=service_discovery), + method="POST"), + timeout=30): + pass + except (urllib.error.HTTPError, urllib.error.URLError) as ex: + error(f"Error attempting to protect Rcache from invalidation " + f"using protect_rcache.url: {repr(ex)}") + + worker_url = \ + get_url(base_url=cfg.rest_api.update_rcache.url, + param_host=args.rcache, + service_discovery=service_discovery) + + afc_config = get_afc_config(cfg=cfg, args=args, + service_discovery=service_discovery) + + min_idx, max_idx = get_idx_range(args.idx_range) + + run(rest_data_handler=PreloadRestDataHandler( + cfg=cfg, afc_config=afc_config, + req_msg_pattern=patch_req(cfg=cfg, args_req=args.req)), + url=worker_url, parallel=args.parallel, backoff=args.backoff, + retries=args.retries, dry=args.dry, batch=args.batch, min_idx=min_idx, + max_idx=max_idx, status_period=args.status_period, + use_requests=args.no_reconnect) + + if not args.dry: + wait_rcache_flush(cfg=cfg, args=args, + service_discovery=service_discovery) + + +def get_afc_worker_url(args: Any, base_url: str, + service_discovery: Optional[ServiceDiscovery]) -> str: + """ REST API URL to AFC server + + Arguments: + cfg -- Config object + base_url -- Base URL from config + service_discovery -- Optional ServiceDiscovery object + Returns REST API URL + """ + if args.localhost: + error_if(not args.comp_proj, + "--comp_proj parameter must be specified") + m = re.search(r"0\.0\.0\.0:(\d+)->%d/tcp.*%s_dispatcher_\d+" % + (80 if args.localhost == "http" else 443, + re.escape(args.comp_proj)), + run_docker(["ps"])) + error_if(not m, + "AFC port not found. Please specify AFC server address " + "explicitly and remove --localhost switch") + assert m is not None + ret = get_url(base_url=base_url, + param_host=f"localhost:{m.group(1)}", + service_discovery=service_discovery) + else: + ret = get_url(base_url=base_url, param_host=args.afc, + service_discovery=service_discovery) + return ret + + +def do_load(cfg: Config, args: Any) -> None: + """ Execute "load" command + + Arguments: + cfg -- Config object + args -- Parsed command line arguments + """ + if args.dry: + worker_url = "" + else: + service_discovery = None if args.comp_proj is None \ + else ServiceDiscovery(compose_project=args.comp_proj) + worker_url = \ + get_afc_worker_url(args=args, base_url=cfg.rest_api.afc_req.url, + service_discovery=service_discovery) + if args.no_cache: + parsed_worker_url = urllib.parse.urlparse(worker_url) + worker_url = \ + parsed_worker_url._replace( + query="&".join( + p for p in [parsed_worker_url.query, "nocache=True"] + if p)).geturl() + + min_idx, max_idx = get_idx_range(args.idx_range) + run(rest_data_handler=LoadRestDataHandler( + cfg=cfg, randomize=args.random, population_db=args.population, + req_msg_pattern=patch_req(cfg=cfg, args_req=args.req)), + url=worker_url, parallel=args.parallel, backoff=args.backoff, + retries=args.retries, dry=args.dry, batch=args.batch, min_idx=min_idx, + max_idx=max_idx, status_period=args.status_period, count=args.count, + use_requests=args.no_reconnect, err_dir=args.err_dir, + ramp_up=args.ramp_up, randomize=args.random, + population_db=args.population) + + +def do_netload(cfg: Config, args: Any) -> None: + """ Execute "netload" command. + + Arguments: + cfg -- Config object + args -- Parsed command line arguments + """ + expected_code: Optional[int] = None + worker_url = "" + if not args.dry: + service_discovery = None if args.comp_proj is None \ + else ServiceDiscovery(compose_project=args.comp_proj) + if args.target is None: + if args.localhost and (not args.rcache): + args.target = "dispatcher" + elif args.afc and (not (args.localhost or args.rcache)): + args.target = "msghnd" + elif args.target and (not (args.localhost or args.afc)): + args.target = "rcache" + else: + error("'--target' argument must be explicitly specified") + if args.target == "rcache": + worker_url = \ + get_url(base_url=cfg.rest_api.rcache_get.url, + param_host=args.rcache, + service_discovery=service_discovery) + expected_code = cfg.rest_api.rcache_get.get("code") + elif args.target == "msghnd": + worker_url = \ + get_url(base_url=cfg.rest_api.msghnd_get.url, + param_host=args.afc, + service_discovery=service_discovery) + expected_code = cfg.rest_api.msghnd_get.get("code") + else: + assert args.target == "dispatcher" + worker_url = \ + get_afc_worker_url( + args=args, base_url=cfg.rest_api.dispatcher_get.url, + service_discovery=service_discovery) + expected_code = cfg.rest_api.dispatcher_get.get("code") + run(netload_target=args.target, url=worker_url, + parallel=args.parallel, backoff=args.backoff, retries=args.retries, + dry=args.dry, batch=1000, expected_code=expected_code, + status_period=args.status_period, count=args.count, + use_requests=args.no_reconnect) + + +def do_cache(cfg: Config, args: Any) -> None: + """ Execute "cache" command. + + Arguments: + cfg -- Config object + args -- Parsed command line arguments + """ + error_if(args.protect and args.unprotect, + "--protect and --unprotect are mutually exclusive") + error_if(not (args.protect or args.unprotect or args.invalidate), + "Nothing to do") + service_discovery = None if args.comp_proj is None \ + else ServiceDiscovery(compose_project=args.comp_proj) + json_data: Any + for arg, attr, json_data in \ + [("protect", "protect_rcache", None), + ("invalidate", "invalidate_rcache", {}), + ("unprotect", "unprotect_rcache", None)]: + try: + if not getattr(args, arg): + continue + data: Optional[bytes] = None + url = get_url(base_url=getattr(cfg.rest_api, attr).url, + param_host=args.rcache, + service_discovery=service_discovery) + req = urllib.request.Request(url, method="POST") + if json_data is not None: + data = json.dumps(json_data).encode(encoding="ascii") + req.add_header("Content-Type", "application/json") + urllib.request.urlopen(req, data, timeout=30) + except (urllib.error.HTTPError, urllib.error.URLError) as ex: + error(f"Error attempting to perform cache {arg} using " + f"rest_api.{attr}.url: {repr(ex)}") + + +def do_afc_config(cfg: Config, args: Any) -> None: + """ Execute "afc_config" command. + + Arguments: + cfg -- Config object + args -- Parsed command line arguments + """ + service_discovery = ServiceDiscovery(compose_project=args.comp_proj) + afc_config = get_afc_config(cfg=cfg, args=args, + service_discovery=service_discovery) + afc_config_str = \ + json.dumps( + patch_json(patch_arg=args.FIELD_VALUE, json_dict=afc_config, + new_type=args.new, data_type="AFC Config")) + result = \ + ratdb( + cfg=cfg, + command=cfg.ratdb.update_config_by_id.format( + afc_config=afc_config_str, region_str=afc_config["regionStr"]), + service_discovery=service_discovery) + error_if(not (isinstance(result, int) and result > 0), + "AFC Config update failed") + + +def do_json_config(cfg: Config, args: Any) -> None: + """ Execute "afc_config" command. + + Arguments: + cfg -- Config object + args -- Parsed command line arguments + """ + s = json.dumps(cfg.data(), indent=2) + filename = args.JSON_CONFIG if args.JSON_CONFIG else \ + (os.path.splitext(Config.DEFAULT_CONFIG)[0] + Config.JSON_EXT) + with open(filename, "w", encoding="utf-8") as f: + f.write(s) + + +def do_help(cfg: Config, args: Any) -> None: + """ Execute "help" command. + + Arguments: + cfg -- Config object (not used) + args -- Parsed command line arguments (also contains 'argument_parser' and + 'subparsers' fields) + """ + unused_argument(cfg) + if args.subcommand is None: + args.argument_parser.print_help() + else: + args.subparsers.choices[args.subcommand].print_help() + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + + cfg = Config(argv=argv, arg_name="config") + + switches_common = argparse.ArgumentParser(add_help=False) + switches_common.add_argument( + "--parallel", metavar="N", type=int, default=cfg.defaults.parallel, + help=f"Number of requests to execute in parallel. Default is " + f"{cfg.defaults.parallel}") + switches_common.add_argument( + "--backoff", metavar="SECONDS", type=float, + default=cfg.defaults.backoff, + help=f"Initial backoff window (in seconds) to use on request failure. " + f"It is doubled on each retry. Default is {cfg.defaults.backoff} " + f"seconds") + switches_common.add_argument( + "--retries", metavar="N", type=int, default=cfg.defaults.retries, + help=f"Maximum number of retries before giving up. Default is " + f"{cfg.defaults.retries}") + switches_common.add_argument( + "--dry", action="store_true", + help="Dry run (no communication with server) to estimate overhead") + switches_common.add_argument( + "--status_period", metavar="N", type=int, + default=cfg.defaults.status_period, + help=f"How often to print status information. Default is once in " + f"{cfg.defaults.status_period} requests. 0 means no status print") + switches_common.add_argument( + "--no_reconnect", action="store_true", + help="Do not reconnect on each request (requires 'requests' Python3 " + "library to be installed: 'pip install requests'") + + switches_req = argparse.ArgumentParser(add_help=False) + switches_req.add_argument( + "--idx_range", metavar="[FROM-]TO", default=cfg.defaults.idx_range, + help=f"Range of AP indices. FROM is initial index (0 if omitted), TO " + f"is 'afterlast' index. Default is '{cfg.defaults.idx_range}'") + switches_req.add_argument( + "--batch", metavar="N", type=int, default=cfg.defaults.batch, + help=f"Number of requests in one REST API call. Default is " + f"{cfg.defaults.batch}") + switches_req.add_argument( + "--req", metavar="FIELD1=VALUE1[;FIELD2=VALUE2...]", action="append", + default=[], + help="Change field(s) in request body (compared to req_msg_pattern " + "in config file). FIELD is dot-separated path to field inside request " + "(e.g. 'location.ellipse.majorAxis'), VALUE is a field value, if " + "field value is list - it should be surrounded by [] and formatted as " + "in JSON. Several semicolon-separated settings may be specified, also " + "this parameter may be specified several times") + + switches_count = argparse.ArgumentParser(add_help=False) + switches_count.add_argument( + "--count", metavar="NUM_REQS", type=int, default=cfg.defaults.count, + help=f"Number of requests to send. Default is {cfg.defaults.count}") + + switches_afc = argparse.ArgumentParser(add_help=False) + switches_afc.add_argument( + "--afc", metavar="[PROTOCOL://]HOST[:port][path][params]", + help="AFC Server to send requests to. Unspecified parts are taken " + "from 'rest_api.afc_req' of config file. By default determined by " + "means of '--comp_proj'") + switches_afc.add_argument( + "--localhost", nargs="?", choices=["http", "https"], const="http", + help="If --afc not specified, default is to send requests to msghnd " + "container (bypassing Nginx container). This flag causes requests to " + "be sent to external http/https AFC port on localhost. If protocol " + "not specified http is assumed") + + switches_rat = argparse.ArgumentParser(add_help=False) + switches_rat.add_argument( + "--rat_server", metavar="[PROTOCOL://]HOST[:port][path][params]", + help="Server to request config from. Unspecified parts are taken " + "from 'rest_api.get_config' of config file. By default determined by " + "means of '--comp_proj'") + + switches_compose = argparse.ArgumentParser(add_help=False) + switches_compose.add_argument( + "--comp_proj", metavar="PROJ_NAME", + help="Docker compose project name. Used to determine hosts to send " + "API calls to. If not specified hostnames should be specified " + "explicitly") + + switches_rcache = argparse.ArgumentParser(add_help=False) + switches_rcache.add_argument( + "--rcache", metavar="[PROTOCOL://]HOST[:port]", + help="Rcache server. May also be determined by means of '--comp_proj'") + + # Top level parser + argument_parser = argparse.ArgumentParser( + description="AFC Load Test Tool") + argument_parser.add_argument( + "--config", metavar="[+]CONFIG_FILE", action="append", default=[], + help=f"Config file. Default has same name and directory as this " + f"script, but has " + f"'{Config.YAML_EXT if has_yaml else Config.JSON_EXT}' extension " + f"(i.e. {Config.DEFAULT_CONFIG}). May be specified several times (in " + f"which case values are merged together). If prefixed with '+' - " + "joined to the default config. Note that this script is accompanied " + f"with default YAML config. On YAML-less Python it should be " + f"converted to JSON (with 'json_config' subcommand) on some YAML-ed " + f"system and copied to YAML-less one") + + subparsers = argument_parser.add_subparsers(dest="subcommand", + metavar="SUBCOMMAND") + + parser_preload = subparsers.add_parser( + "preload", + parents=[switches_common, switches_rat, switches_req, switches_compose, + switches_rcache], + help="Fill rcache with (fake) responses") + parser_preload.add_argument( + "--protect_cache", action="store_true", + help="Protect Rcache from invalidation (e.g. by ULS downloader). " + "Protection persists in rcache database, see 'rcache' subcommand on " + "how to unprotect") + parser_preload.set_defaults(func=do_preload) + + parser_load = subparsers.add_parser( + "load", + parents=[switches_common, switches_req, switches_count, + switches_compose, switches_afc], + help="Do load test") + parser_load.add_argument( + "--no_cache", action="store_true", + help="Don't use rcache, force each request to be computed") + parser_load.add_argument( + "--population", metavar="POPULATION_DB_FILE", + help="Select AP positions proportionally to population density from " + "given database (prepared with " + "tools/geo_converters/make_population_db.py). Positions are random, " + "so no rcache will help") + parser_load.add_argument( + "--err_dir", metavar="DIRECTORY", + help="Directory for offending JSON AFC Requests") + parser_load.add_argument( + "--random", action="store_true", + help="Choose AP positions randomly (makes sense only in noncached " + "mode)") + parser_load.add_argument( + "--ramp_up", metavar="SECONDS", type=float, default=0, + help="Ramp up streams for this number of seconds. Default is to start " + "all at once") + parser_load.set_defaults(func=do_load) + + parser_network = subparsers.add_parser( + "netload", + parents=[switches_common, switches_count, switches_compose, + switches_afc, switches_rcache], + help="Network load test by repeatedly querying health endpoints") + parser_network.add_argument( + "--target", choices=["dispatcher", "msghnd", "rcache"], + help="What to test. If omitted then guess is attempted: 'dispatcher' " + "if --localhost specified, 'msghnd' if --afc without --localhost is " + "specified, 'rcache' if --rcache is specified") + parser_network.set_defaults(func=do_netload) + + parser_cache = subparsers.add_parser( + "cache", parents=[switches_compose, switches_rcache], + help="Do something with response cache") + parser_cache.add_argument( + "--protect", action="store_true", + help="Protect rcache from invalidation (e.g. by background ULS " + "downloader). This action persists in rcache database, it need to be " + "explicitly undone with --unprotect") + parser_cache.add_argument( + "--unprotect", action="store_true", + help="Allows rcache invalidation") + parser_cache.add_argument( + "--invalidate", action="store_true", + help="Invalidate cache (invalidation must be enabled)") + parser_cache.set_defaults(func=do_cache) + + parser_afc_config = subparsers.add_parser( + "afc_config", parents=[switches_rat], help="Modify AFC Config") + parser_afc_config.add_argument( + "--comp_proj", metavar="PROJ_NAME", required=True, + help="Docker compose project name. This parameter is mandatory") + parser_afc_config.add_argument( + "--new", metavar="VALUE_TYPE", + choices=["str", "int", "float", "bool", "list"], + help="Allow creation of new AFC Config keys (requires respective " + "changes in AFC Engine). Created keys will be of given type") + parser_afc_config.add_argument( + "FIELD_VALUE", nargs="+", + help="One or more FIELD=VALUE clauses, where FIELD is a field name in " + "AFC Config, deep field may be specified in dot-separated for m (e.g. " + "'freqBands.0.startFreqMHz'). VALUE is new field value, if field " + "value is list - it should be surrounded by [] and formatted as in " + "JSON") + parser_afc_config.set_defaults(func=do_afc_config) + + parser_json_config = subparsers.add_parser( + "json_config", + help="Convert config file from YAML to JSON for use on YAML-less " + "systems") + parser_json_config.add_argument( + "JSON_CONFIG", nargs="?", + help=f"JSON file to create. By default - same as source YAML file, " + f"but with {Config.JSON_EXT} extension") + parser_json_config.set_defaults(func=do_json_config) + + parser_help = subparsers.add_parser( + "help", add_help=False, + help="Prints help on given subcommand") + parser_help.add_argument( + "subcommand", metavar="SUBCOMMAND", nargs="?", + choices=subparsers.choices, + help="Name of subcommand to print help about (use " + + "\"%(prog)s --help\" to get list of all subcommands)") + parser_help.set_defaults(func=do_help, subparsers=subparsers, + argument_parser=argument_parser, + supports_unknown_args=True) + + if not argv: + argument_parser.print_help() + sys.exit(1) + args = argument_parser.parse_known_args(argv)[0] + if not getattr(args, "supports_unknown_args", False): + args = argument_parser.parse_args(argv) + + # Set up logging + console_handler = logging.StreamHandler() + console_handler.setFormatter( + logging.Formatter( + f"{os.path.basename(__file__)}. %(levelname)s: %(message)s")) + logging.getLogger().addHandler(console_handler) + logging.getLogger().setLevel(logging.INFO) + + # Do the needful + try: + args.func(cfg, args) + except KeyboardInterrupt: + print("^C") + sys.exit(1) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tools/load_tool/afc_load_tool.yaml b/tools/load_tool/afc_load_tool.yaml new file mode 100644 index 0000000..ce7b506 --- /dev/null +++ b/tools/load_tool/afc_load_tool.yaml @@ -0,0 +1,482 @@ +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program + +# Parameter file for afc_load_tool.py. +# Contains request/response pattern, region coordinates, REST APi, etc. +--- +# Defaults for various parameters +defaults: + # Range of point indices + idx_range: "0-1000000" + + # Number of AFC requests per message + batch: 1 + + # Number of simultaneous requests + parallel: 20 + + # Initial backoff window in seconds + backoff: 0.1 + + # Number of retries + retries: 5 + + # Status printing period (in number of requests) + status_period: 1000 + + # Number of individual requests to send by `load` subcommand + count: 1000000 + + + +# Request position generation stuff +# By default (in non-population mode) requests are made in rectangle, defined +# by min/max_lat/lon. In nonrandom (cache testing) mode positions are selected +# in nodes of grid that is grid_size X grid_size +region: + # Minimum latitude in north-positive degrees + min_lat: 33 + + # Maximum latitude in north-positive degrees + max_lat: 48 + + # Minimum longitude in east-positive degrees + min_lon: -116 + + # Maximum longitude in east-positive degrees + max_lon: -95 + + # Grid (number of points in one direction) of generated coordinates + grid_size: 10000 + + # Formula for random height. May use 'r' value that is random, uniformly + # distributed in [0, 1] segment + random_height: 2 + r * r * 100 + + # Ruleset ID (identifies region) + rulesest_id: US_47_CFR_PART_15_SUBPART_E + + # Certification ID (must be registered with given Rulsest ID) + cert_id: FCCID-FSP43 + +# Paths inside requests, responses and other dictionaries +paths: + # Request list in AFC Request message + reqs_in_msg: ["availableSpectrumInquiryRequests"] + + # Request ID inside AFC Request + id_in_req: ["requestId"] + + # AP serial number inside AFC Request + serial_in_req: ["deviceDescriptor", "serialNumber"] + + # Ruleset (region definition) inside AFC Request + ruleset_in_req: ["deviceDescriptor", "certificationId", 0, "rulesetId"] + + # Certification ID inside AFC Request + cert_in_req: ["deviceDescriptor", "certificationId", 0, "id"] + + # Latitude inside AFC Request + lat_in_req: ["location", "ellipse", "center", "latitude"] + + # Longitude inside AFC Request + lon_in_req: ["location", "ellipse", "center", "longitude"] + + # Height inside AFC Request + height_in_req: ["location", "elevation", "height"] + + # List of responses inside AFC Response message + resps_in_msg: ["availableSpectrumInquiryResponses"] + + # Request ID inside AFC Response + id_in_resp: ["requestId"] + + # Ruleset (region definition) inside AFC Request + ruleset_in_resp: ["rulesetId"] + + # Response code insire AFC Response + response_in_resp: ["response"] + + # Response code insire AFC Response + code_in_resp: ["response", "responseCode"] + + # Variable channels inside AFC Response + var_chans_in_resp: ["availableChannelInfo", 0, "channelCfi"] + + # Variable powers inside ASFC Response + var_pwrs_in_resp: ["availableChannelInfo", 0, "maxEirp"] + + # Maximum link distance (distance to FSs to take into account) inside + # AFC Config + link_distance_in_cfg: ["maxLinkDistance"] + + +# Pattern for AFC messages +req_msg_pattern: | + { + "availableSpectrumInquiryRequests": [ + { + "inquiredChannels": [ + { "globalOperatingClass": 131 }, + { "globalOperatingClass": 132 }, + { "globalOperatingClass": 133 }, + { "globalOperatingClass": 134 }, + { "globalOperatingClass": 136 } + ], + "deviceDescriptor": { + "serialNumber": "XXX", + "certificationId": [ + { + "rulesetId": "XXX", + "id": "XXX" + } + ] + }, + "inquiredFrequencyRange": [ + {"lowFrequency": 5925, "highFrequency": 6425}, + {"lowFrequency": 6525, "highFrequency": 6875} + ], + "location": { + "indoorDeployment": 2, + "elevation": { + "verticalUncertainty": 10, + "heightType": "AGL", + "height": 83 + }, + "ellipse": { + "center": { + "latitude": 39.792935, + "longitude": -105.018517 + }, + "orientation": 45, + "minorAxis": 50, + "majorAxis": 50 + } + }, + "requestId": "XXX" + } + ], + "version": "1.4" + } + +# Pattern for AFC Responses used to Rcache prefill +resp_msg_pattern: | + { + "availableSpectrumInquiryResponses": [ + { + "availabilityExpireTime": "2043-08-11T16:45:44Z", + "availableChannelInfo": [ + { + "channelCfi": [], + "globalOperatingClass": 131, + "maxEirp": [] + }, + { + "channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67], + "globalOperatingClass": 132, + "maxEirp": [-2.3, -2.3, 19.7, 34.8, 34.4, 34.5, 17.1, + 31.2, 25.8] + }, + { + "channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167 ], + "globalOperatingClass": 133, + "maxEirp": [0.7, 21.8, 36, 20.2, 28.9, 36, 27.3, 32.5, + 20.1] + }, + { + "channelCfi": [15, 47, 79, 143], + "globalOperatingClass": 134, + "maxEirp": [3.8, 23.1, 32, 30.4] + }, + { + "channelCfi": [2], + "globalOperatingClass": 136, + "maxEirp": [22.4] + } + ], + "availableFrequencyInfo": [ + { + "frequencyRange": { + "highFrequency": 5945, + "lowFrequency": 5925 + }, + "maxPsd": 9.4 + }, + { + "frequencyRange": { + "highFrequency": 5965, + "lowFrequency": 5945 + }, + "maxPsd": -18.4 + }, + { + "frequencyRange": { + "highFrequency": 6025, + "lowFrequency": 5965 + }, + "maxPsd": -18.3 + }, + { + "frequencyRange": { + "highFrequency": 6045, + "lowFrequency": 6025 + }, + "maxPsd": 7.9 + }, + { + "frequencyRange": { + "highFrequency": 6065, + "lowFrequency": 6045 + }, + "maxPsd": 8 + }, + { + "frequencyRange": { + "highFrequency": 6085, + "lowFrequency": 6065 + }, + "maxPsd": 18.8 + }, + { + "frequencyRange": { + "highFrequency": 6105, + "lowFrequency": 6085 + }, + "maxPsd": 22.9 + }, + { + "frequencyRange": { + "highFrequency": 6125, + "lowFrequency": 6105 + }, + "maxPsd": 18.3 + }, + { + "frequencyRange": { + "highFrequency": 6165, + "lowFrequency": 6125 + }, + "maxPsd": 18.4 + }, + { + "frequencyRange": { + "highFrequency": 6185, + "lowFrequency": 6165 + }, + "maxPsd": 18.5 + }, + { + "frequencyRange": { + "highFrequency": 6205, + "lowFrequency": 6185 + }, + "maxPsd": 1.1 + }, + { + "frequencyRange": { + "highFrequency": 6245, + "lowFrequency": 6205 + }, + "maxPsd": 15.1 + }, + { + "frequencyRange": { + "highFrequency": 6265, + "lowFrequency": 6245 + }, + "maxPsd": 15.2 + }, + { + "frequencyRange": { + "highFrequency": 6305, + "lowFrequency": 6265 + }, + "maxPsd": 9.8 + }, + { + "frequencyRange": { + "highFrequency": 6325, + "lowFrequency": 6305 + }, + "maxPsd": 20.9 + }, + { + "frequencyRange": { + "highFrequency": 6345, + "lowFrequency": 6325 + }, + "maxPsd": 21 + }, + { + "frequencyRange": { + "highFrequency": 6425, + "lowFrequency": 6345 + }, + "maxPsd": 22.9 + }, + { + "frequencyRange": { + "highFrequency": 6565, + "lowFrequency": 6525 + }, + "maxPsd": 22.9 + }, + { + "frequencyRange": { + "highFrequency": 6585, + "lowFrequency": 6565 + }, + "maxPsd": 21.4 + }, + { + "frequencyRange": { + "highFrequency": 6605, + "lowFrequency": 6585 + }, + "maxPsd": 21.5 + }, + { + "frequencyRange": { + "highFrequency": 6625, + "lowFrequency": 6605 + }, + "maxPsd": 8.2 + }, + { + "frequencyRange": { + "highFrequency": 6645, + "lowFrequency": 6625 + }, + "maxPsd": 8.3 + }, + { + "frequencyRange": { + "highFrequency": 6665, + "lowFrequency": 6645 + }, + "maxPsd": 11.2 + }, + { + "frequencyRange": { + "highFrequency": 6685, + "lowFrequency": 6665 + }, + "maxPsd": 13.4 + }, + { + "frequencyRange": { + "highFrequency": 6705, + "lowFrequency": 6685 + }, + "maxPsd": 22.9 + }, + { + "frequencyRange": { + "highFrequency": 6725, + "lowFrequency": 6705 + }, + "maxPsd": 19.3 + }, + { + "frequencyRange": { + "highFrequency": 6765, + "lowFrequency": 6725 + }, + "maxPsd": 15.6 + }, + { + "frequencyRange": { + "highFrequency": 6805, + "lowFrequency": 6765 + }, + "maxPsd": 12.5 + }, + { + "frequencyRange": { + "highFrequency": 6845, + "lowFrequency": 6805 + }, + "maxPsd": 1.2 + }, + { + "frequencyRange": { + "highFrequency": 6865, + "lowFrequency": 6845 + }, + "maxPsd": 22.9 + } + ], + "requestId": "XXX", + "response": {"responseCode": 0, "shortDescription": "Success"}, + "rulesetId": "XXX" + } + ], + "version": "1.4" + } + +# List of 20MHz channels, used for generation of variable part of AFC Response +# used for Rcache prefill +channels_20mhz: [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, + 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, + 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181] + +# Various necessary REST APIs +rest_api: + # Get config by RulestID (thgat passed as part of URL) + get_config: + url: http://rat_server/fbrat/ratapi/v1/GetAfcConfigByRulesetID/ + + # Add records to Rcache + update_rcache: + url: http://rcache:8000/update + + # Protect Rcache + protect_rcache: + url: http://rcache:8000/invalidation_state/false + + # Unprotect Rcache + unprotect_rcache: + url: http://rcache:8000/invalidation_state/true + + # Invalidate Rcache + invalidate_rcache: + url: http://rcache:8000/invalidate + + # Rcache status + rcache_status: + url: http://rcache:8000/status + + # Make AFC Request + afc_req: + url: http://msghnd:8000/fbrat/ap-afc/availableSpectrumInquiry + + # Msghnd endpoint to use for network load test + msghnd_get: + url: http://msghnd:8000/fbrat/ap-afc/healthy + + # Rcache endpoint to use for network load test + rcache_get: + url: http://rcache:8000/healthcheck + + # Nginx endpoint to use for network load test + dispatcher_get: + url: http://dispatcher:80/fbrat/ratapi/v1/GetAfcConfigByRulesetID + code: 404 + +# Stuff for working with ratdb +ratdb: + # Database service + service: ratdb + + # Database name + dbname: fbrat + + # Database username + username: postgres + + # Update AFC Config by regionStr + update_config_by_id: >- + UPDATE "AFCConfig" SET config = '{afc_config}' + WHERE config->>'regionStr' = '{region_str}' diff --git a/tools/rcache/Dockerfile b/tools/rcache/Dockerfile new file mode 100644 index 0000000..9d662a0 --- /dev/null +++ b/tools/rcache/Dockerfile @@ -0,0 +1,36 @@ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +FROM alpine:3.18 + +# Default Rcache service port +ENV RCACHE_CLIENT_PORT=8000 + +# Default Rcache service root URL +ENV RCACHE_SERVICE_URL=http://rcache:${RCACHE_CLIENT_PORT} + +# Default Rcache database connection string +ENV RCACHE_POSTGRES_DSN=postgresql://postgres:postgres@bulk_postgres/rcache + +WORKDIR /wd + +RUN apk add --update --no-cache python3=~3.11 py3-sqlalchemy=~1.4 py3-pip \ + py3-requests py3-pydantic=~1.10 py3-psycopg2 py3-greenlet py3-aiohttp curl + +COPY tools/rcache/requirements.txt /wd +RUN pip3 install --root-user-action=ignore -r /wd/requirements.txt + +COPY src/afc-packages /wd/afc-packages +RUN pip3 install --use-pep517 --root-user-action=ignore \ + -r /wd/afc-packages/pkgs.rcache \ + && rm -rf /wd/afc-packages + +COPY tools/rcache/rcache_tool.py /wd +COPY tools/load_tool/afc_load_tool.* /wd/ +RUN chmod a+x /wd/rcache_tool.py /wd/afc_load_tool.py + +CMD sleep infinity \ No newline at end of file diff --git a/tools/rcache/compose_rcache.yaml b/tools/rcache/compose_rcache.yaml new file mode 100644 index 0000000..ce31e14 --- /dev/null +++ b/tools/rcache/compose_rcache.yaml @@ -0,0 +1,17 @@ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# This is an addendum to main docker-compose.yaml that adds rcache_tool serice +# that contains rcache_tool.py + +services: + rcache_tool: + image: rcache_tool:${TAG:-latest} + environment: + - RCACHE_POSTGRES_DSN=postgresql://postgres:postgres@bulk_postgres/rcache + - RCACHE_SERVICE_URL=http://rcache:${RCACHE_PORT} + dns_search: [.] diff --git a/tools/rcache/rcache_tool.py b/tools/rcache/rcache_tool.py new file mode 100644 index 0000000..33ac7ae --- /dev/null +++ b/tools/rcache/rcache_tool.py @@ -0,0 +1,840 @@ +#!/usr/bin/env python3 +""" Tool for testing Rcache """ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +# pylint: disable=wrong-import-order, invalid-name, too-many-arguments +# pylint: disable=too-many-instance-attributes, too-many-statements +# pylint: disable=too-many-locals, too-many-branches + +import aiohttp +import asyncio +import argparse +import copy +import datetime +import hashlib +import json +import os +import random +import re +import sqlalchemy as sa +import sqlalchemy.ext.asyncio as sa_async +import sys +import tabulate +from typing import Any, Dict, List, Optional, Set, Tuple, Union +import urllib.parse + +from rcache_common import dp, error, error_if +from rcache_models import AfcReqRespKey, ApDbRecord, ApDbRespState, \ + LatLonRect, RcacheInvalidateReq, RcacheSpatialInvalidateReq, \ + RcacheStatus, RcacheUpdateReq + +# Environment variable with connection string to Postgres DB +POSTGRES_DSN_ENV = "RCACHE_POSTGRES_DSN" + +# Environment variable with URL to Rcache service +RCACHE_URL_ENV = "RCACHE_SERVICE_URL" + +# Environment variable for localhost's port of Rcache service +RCACHE_PORT_ENV = "RCACHE_CLIENT_PORT" + +# Default number of simultaneous streams in mas operations +DEFAULT_MASS_THREADS = 10 + +# Default number of request in one AFC message +DEFAULT_BATCH_SIZE = 1 + +# Default number of lookups +DEFAULT_LOOKUP_COUNT = 1000000 + +# Default periodicity of status reports +DEFAULT_PERIODICITY = 1000 + +# SqlAlchemy asynchronous driver name +ASYNC_DRIVER_NAME = "asyncpg" + +# Name of table in RCache +TABLE_NAME = "aps" + +# Number of retries +RETRIES = 6 + + +class RrkGen: + """ Generator of request/response/key triplets """ + + # Minimum latitude for generated coordinates + MIN_LAT = 33 + + # Maximum latitude for generated coordinates + MAX_LAT = 48 + + # Minimum longitude for generated coordinates + MIN_LON = -116 + + # Maximum longitude for generated coordinates + MAX_LON = -95 + + # Grid (number of points in one direction) of generated coordinates + GRID_SIZE = 100000 + + # AFC Request template + REQUEST_TEMPLATE = json.loads("""{ + "availableSpectrumInquiryRequests": [ + { + "inquiredChannels": [ + { "globalOperatingClass": 131 }, + { "globalOperatingClass": 132 }, + { "globalOperatingClass": 133 }, + { "globalOperatingClass": 134 }, + { "globalOperatingClass": 136 } + ], + "deviceDescriptor": { + "serialNumber": "FSP43", + "certificationId": [ + {"rulesetId": "US_47_CFR_PART_15_SUBPART_E", "id": "FCCID-FSP43"} + ] + }, + "inquiredFrequencyRange": [ + {"lowFrequency": 5925, "highFrequency": 6425}, + {"lowFrequency": 6525, "highFrequency": 6875} + ], + "location": { + "indoorDeployment": 2, + "elevation": { + "verticalUncertainty": 10, + "heightType": "AGL", + "height": 83 + }, + "ellipse": { + "center": {"latitude": 39.792935, "longitude": -105.018517}, + "orientation": 45, + "minorAxis": 50, + "majorAxis": 50 + } + }, + "requestId": "0" + } + ], + "version": "1.4" +}""") + + # AFC Response template + RESPONSE_TEMPLATE = json.loads("""{ + "availableSpectrumInquiryResponses": [ + { + "availabilityExpireTime": "2023-08-11T16:45:44Z", + "availableChannelInfo": [ + {"channelCfi": [], + "globalOperatingClass": 131, + "maxEirp": []}, + {"channelCfi": [3, 11, 19, 27, 35, 43, 51, 59, 67], + "globalOperatingClass": 132, + "maxEirp": [-2.3, -2.3, 19.7, 34.8, 34.4, 34.5, 17.1, 31.2, 25.8]}, + {"channelCfi": [7, 23, 39, 55, 71, 87, 135, 151, 167 ], + "globalOperatingClass": 133, + "maxEirp": [0.7, 21.8, 36, 20.2, 28.9, 36, 27.3, 32.5, 20.1]}, + {"channelCfi": [15, 47, 79, 143], + "globalOperatingClass": 134, + "maxEirp": [3.8, 23.1, 32, 30.4]}, + {"channelCfi": [2], + "globalOperatingClass": 136, + "maxEirp": [22.4]} + ], + "availableFrequencyInfo": [ + {"frequencyRange": {"highFrequency": 5945, "lowFrequency": 5925}, + "maxPsd": 9.4}, + {"frequencyRange": {"highFrequency": 5965, "lowFrequency": 5945}, + "maxPsd": -18.4}, + {"frequencyRange": {"highFrequency": 6025, "lowFrequency": 5965}, + "maxPsd": -18.3}, + {"frequencyRange": {"highFrequency": 6045, "lowFrequency": 6025}, + "maxPsd": 7.9}, + {"frequencyRange": {"highFrequency": 6065, "lowFrequency": 6045}, + "maxPsd": 8}, + {"frequencyRange": {"highFrequency": 6085, "lowFrequency": 6065}, + "maxPsd": 18.8}, + {"frequencyRange": {"highFrequency": 6105, "lowFrequency": 6085}, + "maxPsd": 22.9}, + {"frequencyRange": {"highFrequency": 6125, "lowFrequency": 6105}, + "maxPsd": 18.3}, + {"frequencyRange": {"highFrequency": 6165, "lowFrequency": 6125}, + "maxPsd": 18.4}, + {"frequencyRange": {"highFrequency": 6185, "lowFrequency": 6165}, + "maxPsd": 18.5}, + {"frequencyRange": {"highFrequency": 6205, "lowFrequency": 6185}, + "maxPsd": 1.1}, + {"frequencyRange": {"highFrequency": 6245, "lowFrequency": 6205}, + "maxPsd": 15.1}, + {"frequencyRange": {"highFrequency": 6265, "lowFrequency": 6245}, + "maxPsd": 15.2}, + {"frequencyRange": {"highFrequency": 6305, "lowFrequency": 6265}, + "maxPsd": 9.8}, + {"frequencyRange": {"highFrequency": 6325, "lowFrequency": 6305}, + "maxPsd": 20.9}, + {"frequencyRange": {"highFrequency": 6345, "lowFrequency": 6325}, + "maxPsd": 21}, + {"frequencyRange": {"highFrequency": 6425, "lowFrequency": 6345}, + "maxPsd": 22.9}, + {"frequencyRange": {"highFrequency": 6565, "lowFrequency": 6525}, + "maxPsd": 22.9}, + {"frequencyRange": {"highFrequency": 6585, "lowFrequency": 6565}, + "maxPsd": 21.4}, + {"frequencyRange": {"highFrequency": 6605, "lowFrequency": 6585}, + "maxPsd": 21.5}, + {"frequencyRange": {"highFrequency": 6625, "lowFrequency": 6605}, + "maxPsd": 8.2}, + {"frequencyRange": {"highFrequency": 6645, "lowFrequency": 6625}, + "maxPsd": 8.3}, + {"frequencyRange": {"highFrequency": 6665, "lowFrequency": 6645}, + "maxPsd": 11.2}, + {"frequencyRange": {"highFrequency": 6685, "lowFrequency": 6665}, + "maxPsd": 13.4}, + {"frequencyRange": {"highFrequency": 6705, "lowFrequency": 6685}, + "maxPsd": 22.9}, + {"frequencyRange": {"highFrequency": 6725, "lowFrequency": 6705}, + "maxPsd": 19.3}, + {"frequencyRange": {"highFrequency": 6765, "lowFrequency": 6725}, + "maxPsd": 15.6}, + {"frequencyRange": {"highFrequency": 6805, "lowFrequency": 6765}, + "maxPsd": 12.5}, + {"frequencyRange": {"highFrequency": 6845, "lowFrequency": 6805}, + "maxPsd": 1.2}, + {"frequencyRange": {"highFrequency": 6865, "lowFrequency": 6845}, + "maxPsd": 22.9} + ], + "requestId": "0", + "response": {"responseCode": 0, "shortDescription": "Success"}, + "rulesetId": "US_47_CFR_PART_15_SUBPART_E" + } + ], + "version": "1.4" +}""") + + # Response template stripped of variable fields (filled on first use) + _STRIPPED_RESPONSE: Optional[Dict[str, Any]] = None + + # 20MHz channels to chose from + CHANNELS = [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, + 65, 69, 73, 77, 81, 85, 89, 93, 117, 121, 125, 129, 133, 137, + 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181] + + @classmethod + def rrk(cls, idx: int) -> AfcReqRespKey: + """ For given request index generates correspondent AfcReqRespKey + object """ + req_msg = copy.deepcopy(cls.REQUEST_TEMPLATE) + req = req_msg["availableSpectrumInquiryRequests"][0] + req["deviceDescriptor"]["serialNumber"] = "RCACHE_TOOL" + str(idx) + req["location"]["ellipse"]["center"] = \ + {"latitude": + (cls.MIN_LAT + + (idx // cls.GRID_SIZE) * (cls.MAX_LAT - cls.MIN_LAT) / + cls.GRID_SIZE), + "longitude": + (cls.MIN_LON + + (idx % cls.GRID_SIZE) * (cls.MAX_LON - cls.MIN_LON) / + cls.GRID_SIZE)} + resp_msg = copy.deepcopy(cls.RESPONSE_TEMPLATE) + resp = resp_msg["availableSpectrumInquiryResponses"][0] + resp["availableChannelInfo"][0]["channelCfi"], \ + resp["availableChannelInfo"][0]["maxEirp"] = \ + cls._resp_channels(idx) + return AfcReqRespKey(afc_req=json.dumps(req_msg), + afc_resp=json.dumps(resp_msg), + req_cfg_digest=cls.lookup_key(idx)) + + @classmethod + def lookup_key(cls, idx: int) -> str: + """ For given request index generates search key """ + md5 = hashlib.md5() + md5.update(str(idx).encode("ascii")) + return md5.hexdigest() + + @classmethod + def validate_response(cls, idx: int, resp: str) -> bool: + """ True if given response matches given request index """ + resp_dict = json.loads(resp) + if (resp_dict["availableSpectrumInquiryResponses"]["channelCfi"], + resp_dict["availableSpectrumInquiryResponses"]["maxEirp"]) != \ + cls._resp_channels(idx): + return False + if cls._STRIPPED_RESPONSE is None: + cls._STRIPPED_RESPONSE = copy.deepcopy(cls.RESPONSE_TEMPLATE) + cls._resp_strip(cls._STRIPPED_RESPONSE) + if cls._resp_strip(resp_dict) != cls._STRIPPED_RESPONSE: + return False + return True + + @classmethod + def _resp_channels(cls, idx: int) -> Tuple[List[int], List[float]]: + """ Response channels for given request index """ + channels: List[int] = [] + bit_idx = 0 + while idx: + if idx & 1: + channels.append(cls.CHANNELS[bit_idx]) + idx //= 2 + bit_idx += 1 + return (channels, [32.] * len(channels)) + + @classmethod + def _resp_strip(cls, resp_msg_dict: Dict[str, Any]) -> Dict[str, Any]: + """ Strips variable fields of response object """ + resp_dict = resp_msg_dict["availableSpectrumInquiryResponses"][0] + del resp_dict["availableChannelInfo"][0]["channelCfi"] + del resp_dict["requestId"] + return resp_msg_dict + + +class Reporter: + """ Progress reporter + + Public attributes: + start_time -- Start datetime.datetime + success_count -- Number of successful requests + fail_count -- Number of failed requests + + Private attributes: + _total_count -- Total count of requests that will be performed + _periodicity -- Report periodicity (e.g. 1000 - once in 1000 bumps) + _last_print_len -- Length of last single-line print + """ + + def __init__(self, total_count: Optional[int] = None, + periodicity: int = DEFAULT_PERIODICITY) -> None: + """ Constructor + + total_count -- Total count of requests that will be performed + periodicity -- Report periodicity (e.g. 1000 - once in 1000 bumps) + """ + self.start_time = datetime.datetime.now() + self.success_count = 0 + self.fail_count = 0 + self._total_count = total_count + self._periodicity = periodicity + self._last_print_len = 0 + + def bump(self, success: bool = True) -> None: + """ Increment success or fail count """ + if success: + self.success_count += 1 + else: + self.fail_count += 1 + if ((self.success_count + self.fail_count) % self._periodicity) == 0: + self.report(newline=False) + + def report(self, newline: bool = True) -> None: + """ Make a report print + + Arguments: + newline -- True to go to next line, False to print same line as before + """ + completed = self.success_count + self.fail_count + msg = f"{completed} completed " + if self.fail_count: + msg += f"(of them {self.fail_count} " \ + f"({self.fail_count * 100 / completed:0.2f}%) failed). " + if self._total_count is not None: + msg += f"{completed * 100 / self._total_count:.2f}%. " + ts: Union[int, float] = \ + (datetime.datetime.now() - self.start_time).total_seconds() + rate = completed / ts + seconds = ts % 60 + ts = int(ts) // 60 + minutes = ts % 60 + hours = ts // 60 + if hours: + msg += f"{hours}:" + if hours or minutes: + msg += f"{minutes:02}:" + msg += f"{seconds:05.2f} elapsed ({rate:.2f} requests per second)" + print(msg + (" " * min(0, self._last_print_len - len(msg))), + end="\n" if newline else "\r", flush=True) + self._last_print_len = 0 if newline else len(msg) + + +def rcache_url(args: Any, path: str) -> str: + """ Returns Rcache REST URL + + Arguments: + args -- Parsed command line arguments + path -- Path inside teh service + Returns URL + """ + return urllib.parse.urljoin(args.rcache, path) + + +async def fill_worker(args: Any, reporter: Reporter, + req_queue: asyncio.Queue[Optional[int]]) -> None: + """ Database fill worker + + Arguments: + args -- Parsed command line arguments + reporter -- Progress reporter object + req_queue -- Queue with requests + """ + end = False + batch: List[AfcReqRespKey] = [] + while not end: + item = await req_queue.get() + if item is None: + end = True + else: + batch.append(RrkGen.rrk(item)) + if len(batch) < (1 if end else args.batch): + continue + update_req = RcacheUpdateReq(req_resp_keys=batch).dict() + errmsg: Optional[str] = None + if (not args.dry) or args.dry_remote: + backoff_window = 1 + for _ in range(RETRIES): + errmsg = None + try: + async with aiohttp.ClientSession(trust_env=True) \ + as session: + if args.dry_remote: + async with session.get(rcache_url(args, + "healthcheck")) \ + as resp: + if resp.ok: + break + errmsg = f"{await resp.text()}. " \ + f"Status={resp.status}. " \ + f"Reason={resp.reason}" + else: + async with session.post(rcache_url(args, "update"), + json=update_req) as resp: + if resp.ok: + break + errmsg = f"{await resp.text()}. " \ + f"Status={resp.status}. " \ + f"Reason={resp.reason}" + except aiohttp.ClientError as ex: + errmsg = str(ex) + await asyncio.sleep(random.random() * backoff_window) + backoff_window *= 2 + for _ in range(len(batch)): + reporter.bump(success=errmsg is None) + batch = [] + if errmsg: + print(errmsg) + + +async def do_mass_fill(args: Any) -> None: + """ Execute "mass_fill" command. + + Arguments: + args -- Parsed command line arguments + """ + reporter = Reporter(total_count=args.max_idx - args.min_idx) + req_queue: asyncio.Queue[Optional[int]] = asyncio.Queue() + async with asyncio.TaskGroup() as tg: + for _ in range(args.threads): + tg.create_task( + fill_worker(args=args, reporter=reporter, req_queue=req_queue)) + for idx in range(args.min_idx, args.max_idx): + await req_queue.put(idx) + for _ in range(args.threads): + await req_queue.put(None) + reporter.report() + + +async def lookup_worker(postgres_dsn: str, metadata: Optional[sa.MetaData], + min_idx: int, max_idx: int, batch_size: int, + count: int, dry: bool, reporter: Reporter) -> None: + """ Lookup worker + + Arguments: + postgres_dsn -- Postgres connection string + min_idx -- Minimum request index + max_idx -- Aftermaximum request index + batch_size -- Number of requests per message + count -- Number of lookups to perform + dry -- Don't do actual database operations + reporter -- Progress reporter object + """ + dsn_parts = urllib.parse.urlsplit(postgres_dsn) + async_dsn = \ + urllib.parse.urlunsplit( + dsn_parts._replace( + scheme=f"{dsn_parts.scheme}+{ASYNC_DRIVER_NAME}")) + async_engine = None if dry else sa_async.create_async_engine(async_dsn) + dry_result = \ + [ApDbRecord.from_req_resp_key(RrkGen.rrk(idx)).dict() + for idx in range(batch_size)] if dry else None + done = 0 + while done < count: + bs = min(batch_size, count - done) + batch: Set[str] = set() + while len(batch) < bs: + batch.add(RrkGen.lookup_key(random.randrange(min_idx, max_idx))) + errmsg: Optional[str] = None + if dry: + assert dry_result is not None + result = \ + [ApDbRecord.parse_obj(dry_result[idx]).get_patched_response() + for idx in range(len(batch))] + else: + assert async_engine is not None + assert metadata is not None + table = metadata.tables[TABLE_NAME] + backoff_window = 1 + for _ in range(RETRIES): + errmsg = None + try: + s = sa.select(table).\ + where((table.c.req_cfg_digest.in_(list(batch))) & + (table.c.state == ApDbRespState.Valid.name)) + async with async_engine.connect() as conn: + rp = await conn.execute(s) + result = [ApDbRecord.parse_obj(rec).get_patched_response() + for rec in rp] + break + except sa.exc.SQLAlchemyError as ex: + errmsg = str(ex) + result = [] + await asyncio.sleep(random.random() * backoff_window) + backoff_window *= 2 + for _ in range(len(result)): + reporter.bump() + for _ in range(len(batch) - len(result)): + reporter.bump(success=False) + if errmsg: + print(errmsg) + done += len(batch) + + +async def do_mass_lookup(args: Any) -> None: + """ Execute "mass_lookup" command. + + Arguments: + args -- Parsed command line arguments + """ + per_worker_count = args.count // args.threads + reporter = Reporter(total_count=per_worker_count * args.threads) + metadata: Optional[sa.MetaData] = None + if not args.dry: + engine = sa.create_engine(args.postgres) + metadata = sa.MetaData() + metadata.reflect(bind=engine) + engine.dispose() + async with asyncio.TaskGroup() as tg: + for _ in range(args.threads): + tg.create_task( + lookup_worker( + postgres_dsn=args.postgres, metadata=metadata, + dry=args.dry, min_idx=args.min_idx, max_idx=args.max_idx, + batch_size=args.batch, count=per_worker_count, + reporter=reporter)) + reporter.report() + + +async def do_invalidate(args: Any) -> None: + """ Execute "invalidate" command. + + Arguments: + args -- Parsed command line arguments + """ + invalidate_req: Dict[str, Any] = {} + path: str = "" + if args.enable or args.disable: + error_if(args.all or args.tile or args.ruleset or + (args.enable and args.disable), + "Incompatible parameters") + path = f"invalidation_state/{json.dumps(bool(args.enable))}" + elif args.all: + error_if(args.tile or args.ruleset, "Incompatible parameters") + invalidate_req = RcacheInvalidateReq().dict() + path = "invalidate" + elif args.ruleset: + error_if(args.tile, "Incompatible parameters") + invalidate_req = RcacheInvalidateReq(ruleset_ids=args.ruleset).dict() + path = "invalidate" + elif args.tile: + tiles: List[LatLonRect] = [] + for s in args.tile: + m = \ + re.search( + r"^(?P[0-9.+-]+),(?P[0-9.+-]+)" + r"(,(?P[0-9.+-]+)(,(?P[0-9.+-]+))?)?$", + s) + error_if(not m, f"Tile specification '{s}' has invalid format") + assert m is not None + try: + min_lat = float(m.group("min_lat")) + min_lon = float(m.group("min_lon")) + max_lat = float(m.group("max_lat")) if m.group("max_lat") \ + else (min_lat + 1) + max_lon = float(m.group("max_lon")) if m.group("max_lon") \ + else (min_lon + 1) + except ValueError: + error(not m, f"Tile specification '{s}' has invalid format") + tiles.append( + LatLonRect(min_lat=min_lat, min_lon=min_lon, max_lat=max_lat, + max_lon=max_lon)) + invalidate_req = RcacheSpatialInvalidateReq(tiles=tiles).dict() + path = "spatial_invalidate" + else: + error("No invalidation type parameters specified") + async with aiohttp.ClientSession() as session: + kwargs = {} + if invalidate_req: + kwargs["json"] = invalidate_req + async with session.post(rcache_url(args, path), **kwargs) as resp: + error_if(not resp.ok, + f"Operation failed: {await resp.text()}") + + +async def do_precompute(args: Any) -> None: + """ Execute "precompute" command. + + Arguments: + args -- Parsed command line arguments + """ + if args.enable or args.disable: + error_if((args.quota is not None) or (args.enable and args.disable), + "Only one parameter should be specified") + path = "precomputation_state" + value = bool(args.enable) + elif args.quota is not None: + path = "precomputation_quota" + value = args.quota + else: + error("At least one parameter should be specified") + async with aiohttp.ClientSession() as session: + async with session.post(rcache_url(args, f"{path}/{value}")) as resp: + error_if(not resp.ok, f"Operation failed: {await resp.text()}") + + +async def do_update(args: Any) -> None: + """ Execute "update" command. + + Arguments: + args -- Parsed command line arguments + """ + error_if(args.enable == args.disable, + "Exactly one parameter should be specified") + async with aiohttp.ClientSession() as session: + async with session.post( + rcache_url(args, f"update_state/{bool(args.enable)}")) as resp: + error_if(not resp.ok, f"Operation failed: {await resp.text()}") + + +async def do_status(args: Any) -> None: + """ Execute "status" command. + + Arguments: + args -- Parsed command line arguments + """ + while True: + async with aiohttp.ClientSession() as session: + async with session.get(rcache_url(args, "status")) as resp: + status = RcacheStatus.parse_obj(await resp.json()) + print(tabulate.tabulate(status.dict().items(), + tablefmt="plain", colalign=("left", "right"))) + if args.interval is None: + break + await asyncio.sleep(args.interval) + + +def do_help(args: Any) -> None: + """ Execute "help" command. + + Arguments: + args -- Parsed command line arguments (also contains 'argument_parser' and + 'subparsers' fields) + """ + if args.subcommand is None: + args.argument_parser.print_help() + else: + args.subparsers.choices[args.subcommand].print_help() + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + default_postgres = os.environ.get(POSTGRES_DSN_ENV) + + default_rcache = os.environ.get(RCACHE_URL_ENV) + if (default_rcache is None) and (RCACHE_PORT_ENV in os.environ): + default_rcache = f"http://localhost:{os.environ[RCACHE_PORT_ENV]}" + + switches_mass = argparse.ArgumentParser(add_help=False) + switches_mass.add_argument( + "--min_idx", metavar="MIN_IDX", default=0, type=int, + help="Minimum request index for mass fill/lookup operation. Default " + "is 0") + switches_mass.add_argument( + "--max_idx", metavar="MAX_IDX", required=True, type=int, + help="Post-maximum request index for mass fill/lookup operation. This " + "parameter is mandatory") + switches_mass.add_argument( + "--threads", metavar="NUM_THREADS", default=DEFAULT_MASS_THREADS, + type=int, + help=f"Number of parallel threads during mass operation. Default is " + f"{DEFAULT_MASS_THREADS}") + switches_mass.add_argument( + "--batch", metavar="BATCH_SIZE", default=DEFAULT_BATCH_SIZE, type=int, + help=f"Batch length (corresponds to number of requests per message). " + f"Default is {DEFAULT_BATCH_SIZE}") + switches_mass.add_argument( + "--dry", action="store_true", + help="Dry run (don't do any database/service requests) to estimate " + "client-side overhead") + + switches_postgres = argparse.ArgumentParser(add_help=False) + switches_postgres.add_argument( + "--postgres", metavar="CONN_STR", required=default_postgres is None, + default=default_postgres, + help="Connection string to Rcache Postgres database. " + + (f"Default is {default_postgres}" if default_postgres is not None + else "This parameter is mandatory")) + + switches_rcache = argparse.ArgumentParser(add_help=False) + switches_rcache.add_argument( + "--rcache", metavar="URL", required=default_rcache is None, + default=default_rcache, + help="URL to rcache service. " + + (f"Default is {default_rcache}" if default_rcache is not None + else "This parameter is mandatory")) + + argument_parser = argparse.ArgumentParser( + description="Response cache test and manipulation tool") + + subparsers = argument_parser.add_subparsers(dest="subcommand", + metavar="SUBCOMMAND") + + parser_mass_fill = subparsers.add_parser( + "mass_fill", parents=[switches_mass, switches_rcache], + help="Mass cache fill") + parser_mass_fill.add_argument( + "--dry_remote", action="store_true", + help="Similar to --dry, but also makes a trivial 'get'. Useful for " + "network performance estimation") + parser_mass_fill.set_defaults(func=do_mass_fill) + parser_mass_fill.set_defaults(is_async=True) + + parser_mass_lookup = subparsers.add_parser( + "mass_lookup", parents=[switches_mass, switches_postgres], + help="Mass cache lookup") + parser_mass_lookup.add_argument( + "--count", metavar="MUMBER_OF_LOOKUPS", default=DEFAULT_LOOKUP_COUNT, + type=int, + help=f"How many lookups to perform. Default is " + f"{DEFAULT_LOOKUP_COUNT}") + parser_mass_lookup.add_argument( + "--check_response", action="store_true", + help="Check response content. Note that response content might change " + "because of invalidation/precomputation, thus they should be somehow " + "disabled") + parser_mass_lookup.set_defaults(func=do_mass_lookup) + parser_mass_lookup.set_defaults(is_async=True) + + parser_invalidate = subparsers.add_parser( + "invalidate", parents=[switches_rcache], + help="Cache invalidation (all parameters are mutually exclusive)") + parser_invalidate.add_argument( + "--enable", action="store_true", + help="Signal rcache service to enable cache invalidation (after it " + "was previously disabled). All invalidation requests accumulated " + "while disabled are fulfilled") + parser_invalidate.add_argument( + "--disable", action="store_true", + help="Signal rcache service to disable cache invalidation") + parser_invalidate.add_argument( + "--all", action="store_true", + help="Invalidate all cache") + parser_invalidate.add_argument( + "--tile", metavar="MIN_LAT,MIN_LON[,MAX_LAT,MAX_LON]", action="append", + help="Tile to invalidate. Latitude/longitude are north/east positive " + "degrees. Maximums, if not specified, are 'plus one degree' of " + "minimums. This parameter may be specified several times") + parser_invalidate.add_argument( + "--ruleset", metavar="RULESET_ID", action="append", + help="Config ruleset ID for entries to invalidate. This parameter may " + "be specified several times") + parser_invalidate.set_defaults(func=do_invalidate) + parser_invalidate.set_defaults(is_async=True) + + parser_precompute = subparsers.add_parser( + "precompute", parents=[switches_rcache], + help="Set precomputation parameters") + parser_precompute.add_argument( + "--enable", action="store_true", + help="Enable precomputation after it was previously disabled") + parser_precompute.add_argument( + "--disable", action="store_true", + help="Disable precomputation (e.g. for development purposes)") + parser_precompute.add_argument( + "--quota", metavar="N", type=int, + help="Set precompute quota - maximum number of simultaneous " + "precomputation requests") + parser_precompute.set_defaults(func=do_precompute) + parser_precompute.set_defaults(is_async=True) + + parser_update = subparsers.add_parser( + "update", parents=[switches_rcache], + help="Enables/disables cache update") + parser_update.add_argument( + "--enable", action="store_true", + help="Enable update after it was previously disabled") + parser_update.add_argument( + "--disable", action="store_true", + help="Disable update. All update requests are dropped until emable") + parser_update.set_defaults(func=do_update) + parser_update.set_defaults(is_async=True) + + parser_status = subparsers.add_parser( + "status", parents=[switches_rcache], + help="Print service status") + parser_status.add_argument( + "--interval", metavar="SECONDS", type=float, + help="Report status periodically with given interval (in seconds). " + "Default is to report status once") + parser_status.set_defaults(func=do_status) + parser_status.set_defaults(is_async=True) + + # Subparser for 'help' command + parser_help = subparsers.add_parser( + "help", add_help=False, + help="Prints help on given subcommand") + parser_help.add_argument( + "subcommand", metavar="SUBCOMMAND", nargs="?", + choices=subparsers.choices, + help="Name of subcommand to print help about (use " + + "\"%(prog)s --help\" to get list of all subcommands)") + parser_help.set_defaults(func=do_help, subparsers=subparsers, + argument_parser=argument_parser) + + if not argv: + argument_parser.print_help() + sys.exit(1) + args = argument_parser.parse_args(argv) + + for predicate, argument in [("require_rcache", "rcache"), + ("require_postgres", "postgres")]: + error_if( + getattr(args, predicate, False) and (not getattr(args, argument)), + f"--{argument} must be specified") + try: + if getattr(args, "is_async", False): + asyncio.run(args.func(args)) + else: + args.func(args) + except SystemExit as ex: + sys.exit(1 if isinstance(ex.code, str) else ex.code) + except KeyboardInterrupt: + print("^C") + sys.exit(1) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tools/rcache/requirements.txt b/tools/rcache/requirements.txt new file mode 100644 index 0000000..4e722ad --- /dev/null +++ b/tools/rcache/requirements.txt @@ -0,0 +1,10 @@ +# +# Copyright (C) 2023 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +asyncpg +tabulate +PyYAML==6.0.1 \ No newline at end of file diff --git a/tools/secrets/README.md b/tools/secrets/README.md new file mode 100644 index 0000000..cc815cb --- /dev/null +++ b/tools/secrets/README.md @@ -0,0 +1,255 @@ +Copyright (C) 2022 Broadcom. All rights reserved.\ +The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate that +owns the software below. This work is licensed under the OpenAFC Project +License, a copy of which is included with this software program. + +# AFC System secrets and their handling + +## Table of contents +- [Secrets](#secrets) +- [Kubernetes secrets manifest](#manifest) +- [Secrets template](#template) +- [Secrets handling in production (Kubernetes) environment](#kubernetes) +- [Secrets handling in development (Compose) environment](#compose) +- [`secret_tool.py` - secrets handling tool](#secret_tool) + - [`check` subcommand (check manifest/template validity)](#check) + - [`list` subcommand (list secrets)](#list) + - [`export` subcommand (make Compose secrets)](#export) + - [`compare_template` subcommand (verify template consistency)](#compare_template) + - [Sample template](#sample_template) + +## Secrets + +**Secret** is a configuration data that should remain, well, secret fully or partially - such as passwords, certificates, API keys, etc. +Example of partially secret data is email configuration that includes secret username and password and, say, public SMTP server name. +If it is desired for such a data to be stored together - it become a secret, even though not all its parts are secret. + +Secrets are secret (not stored in public repositories, protected at runtime, etc.), but secret nomenclature and inner structure (if secret contains more than one field) should remain public. + +Secrets should be available to AFC applications in similar way both in development (**Compose**) and in production (**Kubernetes**) environments. +It is not expected from secrets in development environment to be secret from developers. +In production environment, however, secrets should be kept secret from everybody but applications that use them and The Supreme Administrators, endowed with The Great Round Seal. + +Secrets are named entities. System configuration determines what secrets will be available to what container. +For AFC Applications running in containers secrets will be files (named per secret names, directory in Kubernetes may very, in Compose - always `/run/secrets`). + +## Kubernetes secrets manifest +In Kubernetes the simplest (by no means safest) form of storing secrets is **secrets manifest file**. It is YAML file of the following general structure: +``` +apiVersion: v1 +kind: Secret +metadata: + name: SECRETS_GROUP_NAME +data: + # Comment1 + SECRET_NAME1: + # Comment2 + SECRET_NAME2: + ... +stringData: + # Comment3 + SECRET_NAME3: + # Comment4 + SECRET_NAME4: + ... + + # Comment5 + JSON_SECRET1.json: | + { + "FIELD1": "VALUE1", + ... + } + + # Comment6 + YAML_SECRET1.yaml: | + --- + FIELD1: VALUE1 + ... + +``` +Note how YAML format allows to store JSON and YAML files in verbatim secrets section (`JSON_SECRET1.json`, `YAML_SECRET1.yam`). + +This Kubernetes secret manifest format is used to store secrets (at least in Compose environment - Kubernetes storage mechanism may evolve towards something safer/more appropriate). + +## Secrets template +Secrets values (passwords, certificates, keys, etc.) are secret, but secrets structure (set of secret names, inner structure of secrets) is public. + +This structure is documented in secret template file. Secret template file is secret manifest in which all values are cleared. Like this: +``` +apiVersion: v1 +kind: Secret +metadata: + name: SECRETS_GROUP_NAME +data: + # Comment1 + SECRET_NAME1: + # Comment2 + SECRET_NAME2: + ... +stringData: + # Comment3 + SECRET_NAME3: + # Comment4 + SECRET_NAME4: + ... + + # Comment5 + JSON_SECRET1.json: | + { + "FIELD1": null, + ... + } + + # Comment6 + YAML_SECRET1.yaml: | + --- + FIELD1: + ... + +``` +Note that verbatim values for YAML/JSON-formatted secrets are not empty - instead fields in them are empty. This allows to maintain proper documenting of secrets' structure. + +## Secrets handling in production (Kubernetes) environment +The specific mechanism of secrets' handling in production (Kubernetes) environment is out of scope of this document. +If secret manifest file will be used as is - fine. If something more secure and it will require some support - `secret_tool.py`, described later in this document will need to be upgraded to provide such a support. + +Mechanism of distributing secrets to containers is also out of scope of this document (at least for now). + +## Secrets handling in development (Compose) environment + +In Compose (development) environment secrets are stored in files (one file per secret). In the `docker-compose.yaml` file it looks like this: +``` +version: '3.2' +services: + service1: + image: image1 + secrets: + - SECRET_NAME1 + - SECRET_NAME2 + service2: + image: image2 + secrets: + - JSON_SECRET1.json: + +secrets: + SECRET_NAME1: + file: + SECRET_NAME2: + file: + JSON_SECRET1.json: + file: +``` + +Here `secrets` clause inside service definitions declares what services receive what secrets. +`services` top-level element declares in what file each secret is stored outside of container. + +There is nothing safe about this scheme (albeit files may be protected with access rights), but no safety should be expected of development environment. + +## `secret_tool.py` - secrets handling tool + +**`secret_rool.py`** is a script for maintaining secret manifest files and templates. +It can work with many manifests/templates at once, albeit it is not quite clear yet if we'll have *so* many secrets that will justify use of several manifests. + +General usage format is: +`secret_tool.py SUBCOMMAND [OPTIONS] MANIFEST_FILE(S)` + +Options are subcommand-specific, will be reviewed together with subcommands. + +### `check` subcommand (check manifest/template validity) +Checks validity of secret manifest/template files: valid YAML, values are strings (Nones in template), no duplicates, etc. + +Options: + +|Option|Meaning| +|------|-------| +|--template|File is a template, hence all secret values should be empty (for YAML/JSON verbatim secrets - scalar field values should be empty/null). By default - all secret values should be strings (valid base64 strings if in `data` section)| +|--local|If several manifests/templates specified - they allowed to have same secret name in different files (but not in same file)| + +Examples: +`secret_tool.py check afc_secrets.yaml` +`secret_tool.py check --template templates/*.yaml` + +### `list` subcommand (list secrets) +List secret names in given manifest/template files. + +### `export` subcommand (make Compose secrets) +Export secrets from manifest file to directory (one file per secret), generate `secrets` section for `docker-compose.yaml`. + +Options: + +|Option|Meaning| +|------|-------| +|--secrets_dir **DIRECTORY**|Directory to write secrets to| +|--empty|Secrets written to directory specified by `--secrets_dir` will be empty (i.e. set of empty files will be written). Template(s) may be used instead of manifest(s) as source(s) for this case| +|--compose_file **COMPOSE_FILE**|Compose file to appends `secrets` block to (file created if not existing)| +|--base_dir **DIR_OR_MACRO**|Directory prefix to use in `secrets` block. If not specified directory from `--secrets_dir` is used if it is specified, '.' otherwise| + +Example: +Export secrets from manifest file `afc_secrets.yaml` to `/opt/afc/secrets` directory, while also append `secrets` block to `docker-compose.yaml`, using value of `SECRETS_DIR` variable (e.g. defined in `.env`) as directory prefix in `secrets` block: +``` +secret_tool.py export --secrets_dir /opt/afc/secrets \ + --compose_file docker-compose.yaml --base_dir '${SECRETS_DIR} \ + afc_secrets.yaml +``` + +### `compare_template` subcommand (verify template consistency) +Compare manifest(s) with correspondent template(s). Template files should have same names as correspondent manifest files. + +Options: + +|Option|Meaning| +|------|-------| +|--template_dir **DIRECTORY**|Directory containing template files| + +Example: +Compare content of `afc_secrets.yaml` manifest with `templates/afc_secrets.yaml` template: +`secret_tool.py compare_template --template_dir templates afc_secrets.yaml` + +## Sample template +As of time of this writing the template for AFC Secrets looked like this: +``` +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. +--- +apiVersion: v1 +kind: Secret +metadata: + name: afc_secrets +stringData: + OIDC.json: | + { + "LOGIN": null, + "CLIENT_ID": null, + "CLIENT_SECRET": null, + "DISCOVERY_URL": null + } + + REGISTRATION_CAPTCHA.json: | + { + "USE_CAPTCHA": null, + "SECRET": null, + "SITEKEY": null, + "VERIFY": null + } + + NOTIFIER_MAIL.json: | + { + "SERVER": null, + "PORT": null, + "USE_TLS": null, + "USE_SSL": null, + "USERNAME": null, + "PASSWORD": null + } + + REGISTRATION.json: | + { + "DEST_EMAIL": null, + "DEST_PDL_EMAIL": null, + "SRC_EMAIL": null, + "APPROVE_LINK": null, + } +``` diff --git a/tools/secrets/empty_secrets/NOTIFIER_MAIL.json b/tools/secrets/empty_secrets/NOTIFIER_MAIL.json new file mode 100644 index 0000000..e69de29 diff --git a/tools/secrets/empty_secrets/OIDC.json b/tools/secrets/empty_secrets/OIDC.json new file mode 100644 index 0000000..e69de29 diff --git a/tools/secrets/empty_secrets/REGISTRATION.json b/tools/secrets/empty_secrets/REGISTRATION.json new file mode 100644 index 0000000..e69de29 diff --git a/tools/secrets/empty_secrets/REGISTRATION_CAPTCHA.json b/tools/secrets/empty_secrets/REGISTRATION_CAPTCHA.json new file mode 100644 index 0000000..e69de29 diff --git a/tools/secrets/secret_tool.py b/tools/secrets/secret_tool.py new file mode 100755 index 0000000..172cb78 --- /dev/null +++ b/tools/secrets/secret_tool.py @@ -0,0 +1,547 @@ +#!/usr/bin/env python3 +# Operations around Kubernetes secret manifest files + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=logging-fstring-interpolation, invalid-name, too-many-locals +# pylint: disable=too-many-branches, too-many-nested-blocks +# pylint: disable=too-many-statements + +import argparse +import base64 +import binascii +from collections.abc import Iterator +import json +import logging +import os +import sys +from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple, Union +import yaml + +# Extensions for JSON-formatted secrets +JSON_EXTENSIONS = [".json"] +# Extensions for YAML-formatted secrets +YAML_EXTENSIONS = [".yaml", ".yml"] + + +def fatal(msg: str) -> None: + """ Prints given msg as fatal message and exits with an error """ + logging.fatal(msg) + sys.exit(1) + + +def fatal_if(cond: Any, msg: str) -> None: + """ If condition evaluates to true prints given msg as fatal message and + exits with an error """ + if cond: + fatal(msg) + + +def get_yaml(filename: str, secret_name: str, secret_value: Optional[str]) \ + -> Tuple[bool, Optional[Union[List[Any], Dict[str, Any]]]]: + """ Tries to decode secret as YAML-formatted + + Arguments: + filename -- Name of secret manifest/template file + secret_name -- Name of secret + secret_value -- Value of secret + + Returns (success, optional_yaml_dictionary) tuple. 'success' is true if no + decode was attempted (secret name does not have YAML extension) or if + decode was successful """ + if (not secret_value) or \ + (os.path.splitext(secret_name)[1] not in YAML_EXTENSIONS): + return (True, None) + try: + return (True, + yaml.load(secret_value, + Loader=yaml.CLoader if hasattr(yaml, "CLoader") + else yaml.Loader)) + except yaml.YAMLError as ex: + logging.error(f"File '{filename}' has incorrectly formatted YAML " + f"secret '{secret_name}': {ex}") + return (False, None) + + +def get_json(filename: str, secret_name: str, secret_value: Optional[str]) \ + -> Tuple[bool, Optional[Union[List[Any], Dict[str, Any]]]]: + """ Tries to decode secret as JSON-formatted + + Arguments: + filename -- Name of secret manifest/template file + secret_name -- Name of secret + secret_value -- Value of secret + + Returns (success, optional_json_dictionary) tuple. 'success' is true if no + decode was attempted (secret name does not have JSON extension) or if + decode was successful """ + if (not secret_value) or \ + (os.path.splitext(secret_name)[1] not in JSON_EXTENSIONS): + return (True, None) + try: + return (True, json.loads(secret_value)) + except json.JSONDecodeError as ex: + logging.error(f"File '{filename}' has incorrectly formatted JSON " + f"secret '{secret_name}': {ex}") + return (False, None) + + +def clean_secret(secret_value: Union[List[Any], Dict[str, Any], Any]) \ + -> Optional[Union[List[Any], Dict[str, Any], Any]]: + """ Recursive function that replaces all scalar values in given + scalar/list/dictionary object to Nones + """ + if isinstance(secret_value, list): + return [clean_secret(e) for e in secret_value] + if isinstance(secret_value, dict): + return {k: clean_secret(v) for k, v in secret_value.items()} + return None + + +class ManifestInfo(NamedTuple): + """ Information about secret manifest/template file """ + + # File name + filename: str + + # File formatting is valid + valid: bool = True + + # Metadata name + name: Optional[str] = None + + # Dictionary of file(base64) - formatted secrets + data: Dict[str, Optional[bytes]] = {} + + # Dictionary of string-formatted secrets + string_data: Dict[str, Optional[str]] = {} + + +def manifests(files: List[str], is_template: Optional[bool] = None) \ + -> "Iterator[ManifestInfo]": + """ Reads and validates list of manifests + + Arguments: + files -- List of filenames + is_template -- True for template, False for manifest, None if not known + Returns sequence of ManifestInfo objects + """ + assert files + for filename in files: + if not os.path.isfile(filename): + logging.error(f"Manifest file '{filename}' not found") + yield ManifestInfo(filename=filename, valid=False) + try: + with open(filename, encoding="utf-8") as f: + yaml_dict = \ + yaml.load(f, + Loader=yaml.CLoader if hasattr(yaml, "CLoader") + else yaml.Loader) + except OSError as ex: + logging.error(f"Error reading manifest file '{filename}': {ex}") + yield ManifestInfo(filename=filename, valid=False) + continue + except yaml.YAMLError as ex: + logging.error( + f"YAML parsing failed reading manifest file '{filename}': " + f"{ex}") + yield ManifestInfo(filename=filename, valid=False) + continue + valid = True + if not yaml_dict.get("apiVersion"): + logging.error( + f"'apiVersion' field not found in manifest file '{filename}'") + valid = False + if yaml_dict.get("kind") != "Secret": + logging.error( + f"'Manifest file '{filename}' is not of a 'kind: Secret'") + valid = False + name = yaml_dict.get("metadata", {}).get("name") + if not name: + logging.error( + f"'metadata: name:' not found in manifest file '{filename}'") + valid = False + elif not isinstance(name, str): + logging.error(f"'metadata: name:' is no a string in manifest file " + f"'{filename}'") + name = None + valid = False + data: Dict[str, Any] = yaml_dict.get("data", {}) + if not isinstance(data, dict): + logging.error( + f"'data' field of manifest file '{filename}' is not a " + f"dictionary") + data = {} + valid = False + string_data: Dict[str, Any] = yaml_dict.get("stringData", {}) + if not isinstance(string_data, dict): + logging.error( + f"'stringData' field of manifest file '{filename}' is not a " + f"dictionary") + string_data = {} + valid = False + valid = True + decoded_data: Dict[str, Optional[bytes]] = {} + for secret_name, secret_value in data.items(): + decoded_data[secret_name] = None + if is_template is not None: + if is_template: + if secret_value is not None: + logging.error( + f"Manifest template file '{filename}' has " + f"nonempty value for secret '{secret_name}'") + valid = False + continue + else: + if not isinstance(secret_value, str): + logging.error( + f"Manifest file '{filename}' has nonstring value " + f"for secret '{secret_name}'") + valid = False + else: + try: + decoded_data[secret_name] = \ + base64.b64decode(secret_value) + except binascii.Error as ex: + logging.error( + f"Manifest file '{filename}' has incorrectly " + f"encoded Base64 value for secret " + f"'{secret_name}'") + valid = False + for secret_name, secret_value in string_data.items(): + if secret_name in data: + logging.error(f"Manifest {'template ' if is_template else ''} " + f"file '{filename}' has duplicate secret " + f"'{secret_name}'") + valid = False + if is_template: + if secret_value is None: + continue + if not isinstance(secret_value, str): + logging.error(f"Manifest template file '{filename}' has " + f"nonempty nonstring value for secret " + f"'{secret_name}'") + valid = False + continue + for retrieve in (get_yaml, get_json): + v, content = retrieve(filename=filename, + secret_name=secret_name, + secret_value=secret_value) + valid &= v + if content is not None: + if content != clean_secret(content): + logging.error( + f"Manifest template file '{filename}' has " + f"nonempty field values in secret " + f"'{secret_name}'") + valid = False + break + else: + logging.error(f"Manifest template file '{filename}' has " + "nonempty value for secret '{secret_name}'") + valid = False + else: + if not isinstance(secret_value, str): + logging.error(f"Manifest file '{filename}' has nonstring " + f"value for secret '{secret_name}'") + valid = False + for retrieve in (get_yaml, get_json): + v, _ = retrieve(filename=filename, secret_name=secret_name, + secret_value=secret_value) + valid &= v + yield ManifestInfo( + filename=filename, valid=valid, name=name, data=decoded_data, + string_data=string_data) + + +def check_manifests(files: List[str], is_template: Optional[bool], + is_local: bool) -> bool: + """ Check list of manifest/template files for validity and mutual + consistency + + Arguments: + files -- List of manifest/template files + is_template -- True if files are templates, False if manifests, None if can + be any + is_local -- True if secret names may duplicate across files + True if everything valid + """ + manifest_names: Set[str] = set() + secret_names: Set[str] = set() + ret = True + for mi in manifests(files=files, is_template=is_template): + ret &= mi.valid + if mi.name: + if mi.name in manifest_names: + logging.error(f"Duplicate manifest name '{mi.name}'") + ret = False + manifest_names.add(mi.name) + if not is_local: + for secret_name in (list(mi.data.keys() or []) + + list(mi.string_data.keys() or [])): + if secret_name in secret_names: + logging.error(f"Secret '{secret_name}' found in more " + f"than one file") + ret = False + return ret + + +def do_check(args: Any) -> None: + """Execute "check" command. + + Arguments: + args -- Parsed command line arguments + """ + if not check_manifests(files=args.MANIFEST, is_template=args.template, + is_local=args.local_secrets): + sys.exit(1) + + +def do_list(args: Any) -> None: + """Execute "list" command. + + Arguments: + args -- Parsed command line arguments + """ + manifest_list = list(manifests(files=args.MANIFEST)) + for mi in manifest_list: + print(f"{mi.filename}{(' (' + mi.name + ')') if mi.name else ''}" + f"{' - has errors' if not mi.valid else ''}") + for secret_name in \ + sorted(set(mi.data.keys()) | set(mi.string_data.keys())): + print(f" {secret_name}") + + +def do_export(args: Any) -> None: + """Execute "export" command. + + Arguments: + args -- Parsed command line arguments + """ + fatal_if(not check_manifests(files=args.MANIFEST, + is_template=None if args.empty else False, + is_local=False), + "Errors in manifest(s), export not done") + prefix = args.base_dir if args.base_dir is not None else args.secrets_dir + fatal_if(args.compose_file and (prefix is None), + "Neither --secrets_dir nor --base_dir specified") + prefix = prefix or "." + if args.secrets_dir: + os.makedirs(args.secrets_dir, exist_ok=True) + secrets: Dict[str, str] = {} + for mi in manifests(files=args.MANIFEST): + d: Dict[str, Any] + for d, mode, encoding in [(mi.data, "wb", "ascii"), + (mi.string_data, "w", "utf-8")]: + for secret_name, secret_value in d.items(): + secrets[secret_name] = {"file": f"{prefix}/{secret_name}"} + if args.secrets_dir is None: + continue + try: + filename = os.path.join(args.secrets_dir, secret_name) + with open(filename, mode=mode, encoding=encoding) as f: + if not args.empty: + f.write(secret_value) + except OSError as ex: + fatal(f"Error writing '{filename}': {ex}") + if args.compose_file: + try: + with open(args.compose_file, mode="a", encoding="utf-8") as f: + f.write(yaml.dump({"secrets": secrets}, indent=4)) + except OSError as ex: + fatal(f"Error writing '{args.compose_file}': {ex}") + + +def do_compare_template(args: Any) -> None: + """Execute "compare_template" command. + + Arguments: + args -- Parsed command line arguments + """ + fatal_if(not os.path.isdir(args.template_dir), + f"Template directory '{args.template_dir}' not found") + success = True + manifest_filenames: Set[str] = set() + for mi in manifests(files=args.MANIFEST, is_template=False): + success &= mi.valid + b = os.path.basename(mi.filename) + if b in manifest_filenames: + logging.error(f"More than one manifest file with name '{b}'") + success = False + manifest_filenames.add(b) + template_filename = os.path.join(args.template_dir, b) + if not os.path.isfile(template_filename): + logging.error(f"Template file '{template_filename}' not found") + success = False + continue + ti = list(manifests(files=[template_filename], is_template=True))[0] + success &= ti.valid + if mi.name and ti.name and (mi.name != ti.name): + logging.error(f"Manifest and template files '{b}' have different " + f"'metadata: name:'") + success = False + this_secrets: Dict[str, Any] + other_secrets: Dict[str, Any] + for secret_type, this_name, other_name, this_secrets, other_secrets \ + in [("file secrets", "manifest", "template", mi.data, ti.data), + ("file secrets", "template", "manifest", ti.data, mi.data), + ("string secrets", "manifest", "template", + mi.string_data, ti.string_data), + ("string secrets", "template", "manifest", + ti.string_data, mi.string_data)]: + d = set(this_secrets.keys()) - set(other_secrets.keys()) + if not d: + continue + logging.error(f"Manifest and template files '{b}' have different " + f"set of {secret_type}. Following secrets present " + f"in {this_name}, but absent in {other_name}: " + f"{', '.join(sorted(d))}.") + success = False + for s in sorted(mi.string_data.keys()): + if s not in ti.string_data: + continue + for retrieve in (get_yaml, get_json): + v, ms = retrieve(filename=mi.filename, secret_name=s, + secret_value=mi.string_data[s]) + success &= v + if not ms: + continue + ms = clean_secret(ms) + if s not in ti.string_data: + logging.error(f"Template file '{b}' does not contain " + f"string secret '{s}'") + success = False + break + v, ts = retrieve(filename=ti.filename, secret_name=s, + secret_value=ti.string_data[s]) + success &= v + if not ts: + logging.error(f"Template file '{b}' does not contain " + f"inner structure for secret '{s}'") + success = False + break + ts = clean_secret(ts) + if ms != ts: + logging.error( + f"Manifest and template file '{b}' have different " + f"inner structures for secret '{s}'") + success = False + break + fatal_if(not success, "Comparison failed") + + +def do_help(args: Any) -> None: + """Execute "help" command. + + Arguments: + args -- Parsed command line arguments (also contains 'argument_parser' and + 'subparsers' fields) + """ + if args.subcommand is None: + args.argument_parser.print_help() + else: + args.subparsers.choices[args.subcommand].print_help() + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + switches_files = argparse.ArgumentParser(add_help=False) + switches_files.add_argument( + "MANIFEST", nargs="+", + help="Kubernetes secret manifest file. Several may be specified") + + switches_template_dir = argparse.ArgumentParser(add_help=False) + switches_template_dir.add_argument( + "--template_dir", metavar="DIRECTORY", required=True, + help="Directory for/with template secret files") + + argument_parser = argparse.ArgumentParser( + description="Operations with AFC Kubernetes secret manifest files") + subparsers = argument_parser.add_subparsers(dest="subcommand", + metavar="SUBCOMMAND") + + parser_check = subparsers.add_parser( + "check", parents=[switches_files], + help="Check syntactical validity of secret manifest file() or " + "template(s))") + parser_check.add_argument( + "--template", action="store_true", + help="Manifest templates, should not have secret values " + "(only secret names)") + parser_check.add_argument( + "--local_secrets", action="store_true", + help="Allow different manifests have secrets with same name") + parser_check.set_defaults(func=do_check) + + parser_list = subparsers.add_parser( + "list", parents=[switches_files], + help="List secret names") + parser_list.set_defaults(func=do_list) + + parser_export = subparsers.add_parser( + "export", parents=[switches_files], + help="Write secrets to files, generate 'secrets' block for " + "docker-compose.yaml") + parser_export.add_argument( + "--secrets_dir", metavar="DIRECTORY", + help="Directory to write secrets to as individual files") + parser_export.add_argument( + "--empty", action="store_true", + help="Make empty secret files. Template(s) may be used instead of " + "manifest(s)") + parser_export.add_argument( + "--compose_file", metavar="FILENAME", + help="File to write 'secrets' block. If file exists - block appended " + "to it") + parser_export.add_argument( + "--base_dir", metavar="DIR_OR_MACRO", + help="What to use as name of base directory in 'secrets' block. " + "Default is to use --secrets_dir value") + parser_export.set_defaults(func=do_export) + + parser_compare_template = subparsers.add_parser( + "compare_template", parents=[switches_files], + help="Verify that template(s) correspond to secret manifests") + parser_compare_template.add_argument( + "--template_dir", metavar="DIRECTORY", required=True, + help="Directory containing template files. Template file(s) should " + "have same name as manifest file(s). This parameter is mandatory") + parser_compare_template.set_defaults(func=do_compare_template) + + parser_help = subparsers.add_parser( + "help", add_help=False, + help="Prints help on given subcommand") + parser_help.add_argument( + "subcommand", metavar="SUBCOMMAND", nargs="?", + choices=subparsers.choices, + help="Name of subcommand to print help about (use " + + "\"%(prog)s --help\" to get list of all subcommands)") + parser_help.set_defaults(func=do_help, subparsers=subparsers, + argument_parser=argument_parser) + + if not argv: + argument_parser.print_help() + sys.exit(1) + + # Set up logging + console_handler = logging.StreamHandler() + console_handler.setFormatter( + logging.Formatter( + f"{os.path.basename(__file__)}. %(levelname)s: %(message)s")) + logging.getLogger().addHandler(console_handler) + logging.getLogger().setLevel(logging.INFO) + + args = argument_parser.parse_args(argv) + args.func(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tools/secrets/templates/afc_secrets.yaml b/tools/secrets/templates/afc_secrets.yaml new file mode 100644 index 0000000..8fc19d7 --- /dev/null +++ b/tools/secrets/templates/afc_secrets.yaml @@ -0,0 +1,44 @@ +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. +--- +apiVersion: v1 +kind: Secret +metadata: + name: afc_secrets +stringData: + OIDC.json: | + { + "LOGIN": null, + "CLIENT_ID": null, + "CLIENT_SECRET": null, + "DISCOVERY_URL": null + } + + REGISTRATION_CAPTCHA.json: | + { + "USE_CAPTCHA": null, + "SECRET": null, + "SITEKEY": null, + "VERIFY": null + } + + NOTIFIER_MAIL.json: | + { + "SERVER": null, + "PORT": null, + "USE_TLS": null, + "USE_SSL": null, + "USERNAME": null, + "PASSWORD": null + } + + REGISTRATION.json: | + { + "DEST_EMAIL": null, + "DEST_PDL_EMAIL": null, + "SRC_EMAIL": null, + "APPROVE_LINK": null, + } diff --git a/tools/trial/ReadMe.md b/tools/trial/ReadMe.md new file mode 100755 index 0000000..cbeaa50 --- /dev/null +++ b/tools/trial/ReadMe.md @@ -0,0 +1,52 @@ +# Prepare and Run test: +1. Install Python3 and python module dependencies +2. Copy certificate and key to cert/ (dummy files are in git). These are needed for mtls. +3. Create your token and credentials json files. Instructions are found here: _https://developers.google.com/sheets/api/quickstart/python_ + - Note: credentials represent you app. Token is generated when you login when running the script + for the first time. Token has your private info from the login, so keep it private. + Due to need for popping up a browser and login, first time you run this script should be on + a setup when a browser can start, e.g. your laptop +4. Populate proper values in config.json [see below] +5. ./runAllTests.sh + - csvfile: downloaded google sheet in csv format + - results-dir: where the result logs are kept. + - dbfile is the sqlite3 db to track already run requests. This prevents the same requests from + being run again next time + - configfile: json file which has among configurations, + the gmail account information + key/certificates. [see below] + - email (optional): "To" email. If not provided, the email will be sent to the requester. + + The requests are recorded in the db, and will be skipped next time the command is run. + You need to clear the sqlite3 db if you want to run everything all over again: rm + +6. Optional: The user can choose to pick which request and response file to send: +python3 send_mail.py --req --resp --recvr --conf config.json +Optional: --cc_email can be used for cc + +# Config file format: +``` +{ +"sender_email":"sender@your-inc","password":"your-email-app-password","port":"465", +"cert":"path-to-cert", "key":"path-to-key","cc_email":"your-cc@your-inc", "afc_query":"query uri","start_row":"somerow","dry_run":"true/false" +} +``` +query_uri is the uri for the query, eg. _https://afc.broadcom.com/fbrat/ap-afc/availableSpectrumInquiry_ +Password for your email can be set up differently depending on which email service. For gmail, you will need to log into your google account to set it up +Optional fields: cc_email, start_row + cc_email: cc email + start_row: start requesting from this row in the spread sheet + dry_run: "true" or "false". If true, the completed requests won't be put in the database, and will be repeated in next run + + +# Prune test database +requests that are completed won't be repeated in next run. This is done by insert the UID in a database. To re-run all queries, delete the database. To prune selectively some UID in the database, use prune_db.py e.g. +python3 prune_db.py --csv prune.csv --db test.db +The test.db is the database +prune csv is the file listing the UID to remove +Format is as follows: +``` +"UID" +first_uid +second_uid +... +``` diff --git a/tools/trial/cert/test_cli_crt.pem b/tools/trial/cert/test_cli_crt.pem new file mode 100755 index 0000000..c412709 --- /dev/null +++ b/tools/trial/cert/test_cli_crt.pem @@ -0,0 +1,2 @@ +-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- diff --git a/tools/trial/cert/test_cli_key.pem b/tools/trial/cert/test_cli_key.pem new file mode 100755 index 0000000..f5518e2 --- /dev/null +++ b/tools/trial/cert/test_cli_key.pem @@ -0,0 +1,2 @@ +-----BEGIN PRIVATE KEY----- +-----END PRIVATE KEY----- diff --git a/tools/trial/config.json b/tools/trial/config.json new file mode 100644 index 0000000..5265b45 --- /dev/null +++ b/tools/trial/config.json @@ -0,0 +1,4 @@ +{ +"sender_email":"sender email","password":"password","port":"465", +"cert":"cert/test_cli_crt.pem", "key":"cert/test_cli_key.pem","cc_email":"cc address","afc_query":"query uri" +} diff --git a/tools/trial/csv_downloader.py b/tools/trial/csv_downloader.py new file mode 100755 index 0000000..68d8cd1 --- /dev/null +++ b/tools/trial/csv_downloader.py @@ -0,0 +1,72 @@ +from __future__ import print_function +import os.path +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError +import argparse + +# If modifying these scopes, delete the file token.json. +SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly'] + +# The ID and range of a sample spreadsheet. +SPREADSHEET_ID = '1c6ZUrbWgOq66u53IjeeCCzyYUrCwcwBDkqT4Wepr6Os' +RANGE_NAME = 'Form Responses 1!A1:V' + + +def main(): + """Shows basic usage of the Sheets API. + Prints values from a sample spreadsheet. + """ + parser = argparse.ArgumentParser( + description='Download google sheet into csv') + parser.add_argument('--out', type=str, required=True, + help='output csv file name') + args = parser.parse_args() + + creds = None + # The file token.json stores the user's access and refresh tokens, and is + # created automatically when the authorization flow completes for the first + # time. + if os.path.exists('token.json'): + creds = Credentials.from_authorized_user_file('token.json', SCOPES) + # If there are no (valid) credentials available, let the user log in. + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file( + 'credentials.json', SCOPES) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open('token.json', 'w') as token: + token.write(creds.to_json()) + + try: + service = build('sheets', 'v4', credentials=creds) + + # Call the Sheets API + sheet = service.spreadsheets() + result = sheet.values().get(spreadsheetId=SPREADSHEET_ID, + range=RANGE_NAME).execute() + values = result.get('values', []) + + if not values: + print('No data found.') + return + + with open(args.out, 'w', encoding='utf-8') as csv_file: + for row in values: + length = len(row) + if length > 0: + csv_file.write('\"%s\"' % (row[0])) + for i in range(1, length): + csv_file.write(',\"%s\"' % (row[i])) + csv_file.write('\n') + except HttpError as err: + print(err) + + +if __name__ == '__main__': + main() diff --git a/tools/trial/csv_to_request_json.py b/tools/trial/csv_to_request_json.py new file mode 100644 index 0000000..f39f2e7 --- /dev/null +++ b/tools/trial/csv_to_request_json.py @@ -0,0 +1,218 @@ +import sys +import csv +import json +import os.path +from enum import Enum + +version = '1.4' +cert_rulesetId = 'US_47_CFR_PART_15_SUBPART_E' +cert_id = 'TestCertificationId' +indoor_deployment = 0 +global_operating_class = [131, 132, 133, 134, 136, 137] + + +class LocationType(Enum): + """ Enum class that defines the location type """ + ELLIPSE = 'Ellipse' + LINEAR_POLYGON = 'A polygon with specified vertices' + RADIAL_POLYGON = 'A polygon identified by its center and array of vectors' + POINT_RADIUS = 'Point/radius' + + +class ColumnName(Enum): + TIMESTAMP = 'Timestamp' + LONTITUDE = 'Longitude of the center of the ellipse in decimal degrees' + LATITUDE = 'Latitude of the center of the ellipse in decimal degrees ' + HEIGHT = 'Unlicensed device transmit antenna height in meters above ground level (AGL)' + VERTICAL_UNCERTAINTY = ( + 'Unlicensed device transmit antenna vertical location uncertainty in meters. ' + 'Must be a positive integer (no decimal point).') + MAJORAXIS = 'Semi-major axis of the ellipse in meters' + MINORAXIS = 'Semi-minor axis of the ellipse in meters' + ORIENTATION = 'Orientation of the major axis of the ellipse, in degrees clockwise from true north' + POINT_LONTITUDE = 'Unlicensed device transmit antenna longitude in decimal degrees' + POINT_LATITUDE = 'Unlicensed device transmit antenna latitude in decimal degrees ' + POINT_RADIUS_AXIS = 'Unlicensed device transmit antenna horizontal location uncertainty radius in meters' + LINEAR_POLYGON_VERTICES = 'Vertices in decimal degrees' + CENTER_LONTITUDE = 'Center longitude in decimal degrees' + CENTER_LATITUDE = 'Center latitude in decimal degrees ' + VECTORS_FROM_CENTER = 'Vectors from Center' + LOCATION_TYPE = 'Specify the method you choose to provide the horizontal location and horizontal location uncertainty of the unlicensed device.' + + +def return_int_or_float(str): + if str == '': + return '' + + if str.isdigit(): + num = int(str) + else: + num = float(str) + return num + + +def get_outerboundary_list(str): + ob_list = [] + + ob_str = str.replace(" ", "") + ob_str = ob_str.strip("[]") + # print(ob_str) + + ob_split = ob_str.split("),") + for i in ob_split: + i = i.strip("()\n\t") + entry = i.split(",") + ob_list.append([entry[0], entry[1]]) + # print(i) + # print(ob_list) + return ob_list + + +def generate_request_json(csv_dict, file_index, cert_id): + data_dict = {} + asir_list = [] + request_dict = {} + device_desc_dict = {} + cert_id_list = [] + location_dict = {} + freq_list = [] + channel_list = [] + + # prepare device descriptor + timestamp = csv_dict[ColumnName.TIMESTAMP.value].split() + device_desc_dict['serialNumber'] = 'TestSerialNumber' + cert_id_list.append(dict(rulesetId=cert_rulesetId, id=cert_id)) + device_desc_dict['certificationId'] = cert_id_list + + # prepare location data + location_dict['elevation'] = dict( + height=return_int_or_float(csv_dict[ColumnName.HEIGHT.value]), + heightType='AGL', + verticalUncertainty=return_int_or_float( + csv_dict[ColumnName.VERTICAL_UNCERTAINTY.value]), + ) + + location_type = csv_dict[ColumnName.LOCATION_TYPE.value] + + if location_type == LocationType.ELLIPSE.value: + center_dict = dict( + longitude=return_int_or_float( + csv_dict[ColumnName.LONTITUDE.value]), + latitude=return_int_or_float(csv_dict[ColumnName.LATITUDE.value]), + ) + location_dict['ellipse'] = dict( + center=center_dict, + majorAxis=return_int_or_float( + csv_dict[ColumnName.MAJORAXIS.value]), + minorAxis=return_int_or_float( + csv_dict[ColumnName.MINORAXIS.value]), + orientation=return_int_or_float( + csv_dict[ColumnName.ORIENTATION.value]), + ) + elif location_type == LocationType.POINT_RADIUS.value: + center_dict = dict( + longitude=return_int_or_float( + csv_dict[ColumnName.POINT_LONTITUDE.value]), + latitude=return_int_or_float( + csv_dict[ColumnName.POINT_LATITUDE.value]), + ) + location_dict['ellipse'] = dict( + center=center_dict, + majorAxis=return_int_or_float( + csv_dict[ColumnName.POINT_RADIUS_AXIS.value]), + minorAxis=return_int_or_float( + csv_dict[ColumnName.POINT_RADIUS_AXIS.value]), + orientation=0 + ) + elif location_type == LocationType.LINEAR_POLYGON.value: + ob_entry_list = get_outerboundary_list( + csv_dict[ColumnName.LINEAR_POLYGON_VERTICES.value]) + ob_list = [] + for i in ob_entry_list: + ob_list.append( + dict(longitude=return_int_or_float( + i[0]), latitude=return_int_or_float(i[1])) + ) + + location_dict['linearPolygon'] = dict( + outerBoundary=ob_list, + ) + elif location_type == LocationType.RADIAL_POLYGON.value: + center_dict = dict( + longitude=return_int_or_float( + csv_dict[ColumnName.CENTER_LONTITUDE.value]), + latitude=return_int_or_float( + csv_dict[ColumnName.CENTER_LATITUDE.value]), + ) + ob_entry_list = get_outerboundary_list( + csv_dict[ColumnName.VECTORS_FROM_CENTER.value]) + ob_list = [] + for i in ob_entry_list: + ob_list.append( + dict(length=return_int_or_float( + i[0]), angle=return_int_or_float(i[1])) + ) + location_dict['radialPolygon'] = dict( + center=center_dict, + outerBoundary=ob_list, + ) + location_dict['indoorDeployment'] = indoor_deployment + + # prepare frequency + freq_list.append(dict(lowFrequency=5925, highFrequency=6425)) + freq_list.append(dict(lowFrequency=6525, highFrequency=6875)) + + # prepare channel + for op_class in global_operating_class: + channel_list.append(dict(globalOperatingClass=op_class)) + + request_dict['requestId'] = csv_dict['Unique request ID'] + request_dict['deviceDescriptor'] = device_desc_dict + request_dict['location'] = location_dict + request_dict['inquiredFrequencyRange'] = freq_list + request_dict['inquiredChannels'] = channel_list + + asir_list.append(request_dict) + + data_dict['version'] = version + data_dict['availableSpectrumInquiryRequests'] = asir_list + return data_dict + + +def csv_to_json(csv_file_path, cert_id): + if os.path.exists(csv_file_path) is False: + print('input file({}) is not existed'.format(csv_file_path)) + return + + dir = os.path.dirname(csv_file_path) + + if not dir: + dir = os.getcwd() + + print('Start converting the CSV file to request JSON dir %s => %s' % + (csv_file_path, dir)) + with open(csv_file_path, encoding='utf-8') as csv_file_handler: + # convert each row into a dictionary + csv_reader = csv.DictReader(csv_file_handler) + i = 2 + for rows in csv_reader: + file_index = f'UID_' + rows['Unique request ID'] + f'_R{i}' + i = i + 1 + req_json = generate_request_json(rows, file_index, cert_id) + + json_file_path = '{}/{}.json'.format(dir, file_index) + with open(json_file_path, 'w', encoding='utf-8') as json_file_handler: + json_file_handler.write(json.dumps(req_json, indent=4)) + print('Converted row {} to {}'.format( + file_index, json_file_path)) + + print('Finished converting the CSV file to request JSON') + + +if __name__ == "__main__": + if len(sys.argv) <= 1: + sys.exit("Usage: csv_to_request_json.py [csv_file_relative_path]") + if len(sys.argv) > 2: + cert_id = sys.argv[2] + + csv_to_json(sys.argv[1], cert_id) diff --git a/tools/trial/prune.csv b/tools/trial/prune.csv new file mode 100644 index 0000000..c218dc6 --- /dev/null +++ b/tools/trial/prune.csv @@ -0,0 +1 @@ +"UID" diff --git a/tools/trial/prune_db.py b/tools/trial/prune_db.py new file mode 100644 index 0000000..5ca69bd --- /dev/null +++ b/tools/trial/prune_db.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +# Portions copyright © 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate +# affiliate that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy +# of which is included with this software program. +import csv +import argparse +import sqlite3 + +parser = argparse.ArgumentParser(description='Read and parse request json') +parser.add_argument('--csv', type=str, required=True, + help='request csv file name') +parser.add_argument('--db', type=str, required=True, + help='result directory') + +args = parser.parse_args() + +csv_file = args.csv + +conn = sqlite3.connect(args.db) +print("Opened database %s successfully" % args.db) + +with open(csv_file, 'r') as csv_file: + reader = csv.DictReader(csv_file) + for row in reader: + uid = row['UID'] + list = conn.execute( + "SELECT * from REQUESTS WHERE UID=?", (uid,)).fetchall() + if not list == []: + print("Found and prunning %s" % uid) + conn.execute("DELETE from REQUESTS WHERE UID=?", (uid,)) + conn.commit() diff --git a/tools/trial/runAllTests.sh b/tools/trial/runAllTests.sh new file mode 100755 index 0000000..f4243b3 --- /dev/null +++ b/tools/trial/runAllTests.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Portions copyright © 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate +# affiliate that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy +# of which is included with this software program. +# +# run: runAllTests.sh [] +# db_file will be created automatically if not exist. db_file tracks previous already run +# requests to avoid running again next time. +# If optional argument is not provided, the email containing results will be sent to +# the requester. + +python3 csv_downloader.py --out $1 +python3 csv_to_request_json.py $1 +if [ -z "$5" ]; then + python3 send_requests.py --csv $1 --res $2 --db $3 --config $4 +else + python3 send_requests.py --csv $1 --res $2 --db $3 --config $4 --email $5 +fi + + diff --git a/tools/trial/send_mail.py b/tools/trial/send_mail.py new file mode 100644 index 0000000..ce6d6e9 --- /dev/null +++ b/tools/trial/send_mail.py @@ -0,0 +1,62 @@ +#!/usr/bin/python +# Portions copyright © 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate +# affiliate that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy +# of which is included with this software program. + +import smtplib +import ssl +from email import encoders +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.message import EmailMessage +import json +import argparse +from send_requests import send_mail +##### +# configurations +# config file is in json of this format: +# {"sender_email":"sender email here","password":"password here","port":"465"} +#### + +parser = argparse.ArgumentParser(description='Read and parse request json') +parser.add_argument('--req', type=str, required=True, + help='request json file name') +parser.add_argument('--resp', type=str, required=True, + help='response json file name') +parser.add_argument('--recvr', type=str, required=True, + help='receiver email address') +parser.add_argument('--cc_email', type=str, required=False, + help='cc email (group) address') +parser.add_argument('--config', type=str, required=True, + help='config file in json format') +args = parser.parse_args() + +receiver_email = args.recvr +req_file = args.req +resp_file = args.resp +conf_file = args.config +cc_email = args.cc_email +if not cc_email: + cc_email = "" +f = open(req_file) +req_data = json.load(f)['availableSpectrumInquiryRequests'][0] +req_id = req_data['requestId'] +f.close() + +f = open(conf_file) +config_data = json.load(f) +f.close() +password = config_data['password'] +sender_email = config_data['sender_email'] +port = int(config_data['port']) + +# Create a secure SSL context +context = ssl.create_default_context() + +with smtplib.SMTP_SSL("smtp.gmail.com", port, context=context) as server: + server.login(sender_email, password) + send_mail(req_file, resp_file, receiver_email, cc_email, + server, req_id, sender_email, sender_email) diff --git a/tools/trial/send_requests.py b/tools/trial/send_requests.py new file mode 100644 index 0000000..1e2f6a2 --- /dev/null +++ b/tools/trial/send_requests.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +# Portions copyright © 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate +# affiliate that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy +# of which is included with this software program. + +import smtplib +import ssl +from email import encoders +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import sqlite3 +import csv +import argparse +import requests +import json +import os +from email.message import EmailMessage + +time_val = 40 + + +def send_mail(file, outfile, email, cc_email, server, + req_id, requester, sender_email): + receiver_email = email + + body = f"Hello there,\nPlease find attached response for AFC trial requested with\n" + body += f"Unique Request ID: {req_id}\nRequester: {requester}\n" + body += f"\nThe attached response is in JSON format, in accordance with the " \ + "WFA AFC System to AFC Device Interface specification.\n" \ + "If you wish to manually inspect the JSON response, you might consider using a " \ + "free online parser tool such as: https://jsonformatter.org/\n\n" \ + "Best Regards,\nBroadcom AFC Trials." + + message = EmailMessage() + message['Subject'] = f"[Broadcom AFC Trials] Response for request id: {req_id}" + + message['From'] = sender_email + message['To'] = receiver_email + message['Cc'] = cc_email + + message.add_alternative(body) + with open(file, "rb") as attachment: + message.add_attachment(attachment.read(), maintype='application', + subtype='octet-stream', filename=file) + with open(outfile, "rb") as attachment: + message.add_attachment(attachment.read(), maintype='application', + subtype='octet-stream', filename=outfile) + + server.sendmail(sender_email, [receiver_email, + cc_email], message.as_string()) + + +def run_test(uid, requester, res, email, config_data): + headers = { + 'Content-Type': 'application/json', + } + params = { + 'debug': 'False', + } + conf_cert = config_data['cert'] + conf_key = config_data['key'] + cert = (conf_cert, conf_key) + filename = f'{indir}/UID_{uid}.json' + outfilename = f'{res}/UID_{uid}.json' + print("Sending request UID = %s" % uid) + + with open(filename) as f: + data = f.read().replace('\n', '').replace('\r', '').encode() + f.close() + response = requests.post( + config_data['afc_query'], + params=params, + headers=headers, + data=data, + cert=cert, + verify='/etc/ssl/certs/ca-bundle.crt', + ) + + print("Filter vendor extensions") + # Delete the vendor extensions from public trial results + filtered_response = response.json() + + try: + del filtered_response["availableSpectrumInquiryResponses"][0]["vendorExtensions"] + except KeyError: + pass + except IndexError: + print("Unknown response format!") + + fout = open(outfilename, "w") + fout.write(json.dumps(filtered_response, indent=1)) + + fout.close() + print("Email UID %s to %s" % (uid, email)) + # Send to requester if no override provided + if not email: + email = requester + + # Create a secure SSL context + context = ssl.create_default_context() + port = int(config_data['port']) + server = smtplib.SMTP_SSL("smtp.gmail.com", port, context=context) + password = config_data['password'] + server.login(sender_email, password) + send_mail(filename, outfilename, email, cc_email, + server, uid, requester, sender_email) + + +def init_db(conn): + # create cursor object + cur = conn.cursor() + + # check if table exists + listOfTables = cur.execute( + """SELECT name FROM sqlite_master WHERE type='table' + AND name='REQUESTS'; """).fetchall() + + if listOfTables == []: + print('Table not found!') + conn.execute('''CREATE TABLE REQUESTS + (ID INTEGER PRIMARY KEY AUTOINCREMENT , + UID TEXT NOT NULL, + TIMESTAMP TEXT NOT NULL);''') + print("Table created successfully") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Read and parse request json') + parser.add_argument('--csv', type=str, required=True, + help='request csv file name') + parser.add_argument('--res', type=str, required=True, + help='result directory') + parser.add_argument('--db', type=str, required=True, + help='result directory') + parser.add_argument('--config', type=str, required=True, + help='result directory') + parser.add_argument('--email', type=str, required=False, + help='To email address') + args = parser.parse_args() + + if not os.path.exists(args.res): + print("Make result directory %s " % args.res) + os.mkdir(args.res) + + req_file = args.csv + indir = os.path.dirname(req_file) + + if not dir: + indir = os.getcwd() + + conn = sqlite3.connect(args.db) + print("Opened database %s successfully" % args.db) + + conf_file = args.config + f = open(conf_file) + config_data = json.load(f) + if args.email: + email = args.email + else: + email = None + + sender_email = config_data['sender_email'] + + # optional cc_email in config.json + try: + cc_email = config_data['cc_email'] + except BaseException: + cc_email = "" + + try: + dry_run = (config_data['dry_run'].lower() == "true") + except BaseException: + dry_run = False + + f.close() + + # initialize db if not already exists. + init_db(conn) + + # run tests. + with open(req_file, 'r') as csv_file: + reader = csv.DictReader(csv_file) + i = 2 + try: + start_row = int(config_data['start_row']) + except BaseException: + start_row = 2 + + for row in reader: + ts = row['Timestamp'] + uid = row['Unique request ID'] + f'_R{i}' + requester = row['Email address'] + i = i + 1 + list = conn.execute( + "SELECT * from REQUESTS WHERE UID=?", (uid,)).fetchall() + if list == [] and i > start_row: + print("Querying for uid %s" % uid) + run_test(uid, requester, args.res, email, config_data) + + # if not dry run, update db to avoid running same req again + if not dry_run: + conn.execute("INSERT INTO REQUESTS (TIMESTAMP,UID) \ + VALUES (?, ?)", (ts, uid)) + conn.commit() + + else: + print("Ignore existing uid %s" % uid) + + conn.close() diff --git a/uls/Dockerfile-uls_service b/uls/Dockerfile-uls_service new file mode 100644 index 0000000..55c10ec --- /dev/null +++ b/uls/Dockerfile-uls_service @@ -0,0 +1,213 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +# default value of args +ARG BLD_TAG=latest +ARG PRINST_TAG=latest +ARG BLD_NAME=public.ecr.aws/w9v6y1o0/openafc/worker-al-build-image +ARG PRINST_NAME=public.ecr.aws/w9v6y1o0/openafc/worker-al-preinstall + +# Stage Build +FROM ${BLD_NAME}:${BLD_TAG} as build_image +ARG BUILDREV=localbuild +COPY CMakeLists.txt LICENSE.txt version.txt Doxyfile.in /root/afc/ +COPY cmake /root/afc/cmake/ +COPY pkg /root/afc/pkg/ +COPY selinux /root/afc/selinux/ +COPY src /root/afc/src/ + +RUN apk add py3-setuptools py3-pip + +RUN mkdir -p -m 777 /root/afc/build +RUN cd /root/afc/build && \ +cmake -DCMAKE_INSTALL_PREFIX=/root/afc/__install \ + -DCMAKE_BUILD_TYPE=Ulsprocessor -DSVN_LAST_REVISION=$BUILDREV \ + -G Ninja /root/afc && \ + ninja -j$(nproc --all --ignore=2) install + +FROM ${PRINST_NAME}:${PRINST_TAG} as install_image +RUN apk add py3-sqlalchemy py3-numpy py3-yaml py3-pydantic=~1.10 py3-psycopg2 +COPY uls/requirements.txt /wd +RUN pip3 install -r /wd/requirements.txt + +RUN mkdir -p /mnt/nfs/rat_transfer/daily_uls_parse/data_files \ + /mnt/nfs/rat_transfer/daily_uls_parse/temp \ + /mnt/nfs/rat_transfer/ULS_Database \ + /mnt/nfs/rat_transfer/Antenna_Patterns \ + /output_folder + +COPY --from=build_image /root/afc/__install/bin \ + /mnt/nfs/rat_transfer/daily_uls_parse/ +COPY --from=build_image /root/afc/build/src/ratapi/pkg/ratapi/db/ \ + /mnt/nfs/rat_transfer/daily_uls_parse/ +COPY uls/uls_service*.py uls/fsid_tool.py uls/fs_db_diff.py uls/fs_afc.py \ + uls/fs_afc.yaml /wd/ +RUN chmod -x /wd/*.py +RUN chmod +x /wd/uls_service.py /wd/uls_service_healthcheck.py \ + wd/fsid_tool.py wd/fs_db_diff.py wd/fs_afc.py + +FROM alpine:3.18 + +# DOWNLOADER SERVICE PARAMETERS + +# Where to directory with ULS databases is mapped +ENV ULS_EXT_DB_DIR=/ + +# Symlink that points to recent ULS database. May include path, that is used to +# override fsDatabaseFile in AFC Config +ENV ULS_CURRENT_DB_SYMLINK=rat_transfer/ULS_Database/FS_LATEST.sqlite3 + +# Name of RAS database (as mapped from external storage) +ENV ULS_EXT_RAS_DATABASE=/rat_transfer/RAS_Database/RASdatabase.dat + +# File name from where daily_uls_parse.py reads RAS database +# ENV ULS_RAS_DATABASE + +# YES to run downloader script on nice (lowered) priority, NO to use normal +# priority (this is default) +ENV ULS_NICE=YES + +# Download script +# ENV ULS_DOWNLOAD_SCRIPT + +# Additional arguments (besides --region) to pass to daily_uls_parse.py +# ENV ULS_DOWNLOAD_SCRIPT_ARGS + +# Colon separated list of regions to download. Default - all of them +# ENV ULS_DOWNLOAD_REGION + +# Directory where script puts downloaded database +# ENV ULS_RESULT_DIR + +# Directory where download script puts temporary files +# ENV ULS_TEMP_DIR + +# FSID file location used by daily_uls_parse.py +# ENV ULS_FSID_FILE + +# Connection string to service state database +# ENV ULS_SERVICE_STATE_DB_DSN + +# Create service state database if absent (default is True) +# ENV ULS_SERVICE_STATE_DB_CREATE_IF_ABSENT + +# Recreate (erase) service state database if exists (default is False) +# ENV ULS_SERVICE_STATE_DB_RECREATE + +# Prometheus port to serve metrics on. Prometheus metrics (if used) are only +# meaningful in continuous (not run-once) operation +# ENV ULS_PROMETHEUS_PORT + +# HOST[PORT] of StatsD server to send metrics to. StatsD operation may be used +# in run-once mode of operation +# ENV ULS_STATSD_SERVER + +# External files to check: BASE_URL1:SUBDIR1:FILENAME[,FILENAME...];BASE_URL2... +# ENV ULS_CHECK_EXT_FILES + +# Delay first download by given number of hours +# ENV ULS_DELAY_HR + +# Download interval in hours +# ENV ULS_INTERVAL_HR + +# Maximum allowed change since previous download in percents +# ENV ULS_MAX_CHANGE_PERCENT + +# URL to use for testing new ULS database against AFC Service +# ENV ULS_AFC_URL + +# Number of parallel AFC Requests to make during new ULS database verification +# Default see config file of uls_afc.py +# ENV ULS_AFC_PARALLEL + +# Download maximum duration in hours +# ENV ULS_TIMEOUT_HR + +# YES to run download once, NO to run periodically (this is default) +# ENV ULS_RUN_ONCE + +# True if Rcache enabled, False if not +# ENV RCACHE_ENABLED + +# URL of Rcache service +# ENV RCACHE_SERVICE_URL + + +# HEALTHCHECKER PARAMETERS +# Variables starting with ULS_HEALTH_ related to pronounce service healthy or +# unhealthy +# Variables starting with ULS_ALARM_ related to email sending. +# For all variables it's OK to be empty/unspecified + +# Pronounce service unhealthy if no download attempt was made for this number +# of hours. Default defined in uls_service.py (6 hours as of time of this +# writing) +# ENV ULS_HEALTH_ATTEMPT_MAX_AGE_HR + +# Pronounce service unhealthy if no download succeeded for this number +# of hours. Default defined in uls_service.py (8 hours as of time of this +# writing) +# ENV ULS_HEALTH_SUCCESS_MAX_AGE_HR + +# Pronounce service unhealthy if ULS data had not changed for this number of +# hours. Default defined in uls_service.py (40 hours as of time of this +# writing) +# ENV ULS_HEALTH_UPDATE_MAX_AGE_HR + +# Name of JSON secret with SMTP parameters (formatted as per +# NOTIFIER_MAIL.json). If not specified no emails will be sent +# ENV ULS_ALARM_SMTP_INFO + +# Email to send messages to. If not specified no emails will be sent +# ENV ULS_ALARM_EMAIL_TO +# +# Email to send beacon information messages to. If not specified alarm email +# will be used +# ENV ULS_BEACON_EMAIL_TO + +# Minimum interval (in hours) of alarm message (message on something that went +# wrong) sending. If not specified no alarm emails will be sent +# ENV ULS_ALARM_ALARM_INTERVAL_HR + +# Minimum interval (in hours) of beacon message (message on current status, be +# it good or bad) sending. If not specified no beacon emails will be sent +# ENV ULS_ALARM_BEACON_INTERVAL_HR + +# Send alarm email if last download attempt is older than this number of +# hours. If not specified last attempt time will not trigger alarm email +# sending +# ENV ULS_ALARM_ATTEMPT_MAX_AGE_HR + +# Send alarm email if last download success is older than this number of +# hours. If not specified last download success time will not trigger alarm +# email sending +# ENV ULS_ALARM_SUCCESS_MAX_AGE_HR + +# Send alarm email if region's ULS (FS) data was not changed for more than +# given number of hours. +# Format is REG1:HOURS1,REG2:HOURS2... where REG... is region code (US, CA, BR +# as of time of this writing). +# If not specified, data age will not trigger alarm email sending +# ENV ULS_ALARM_REG_UPD_MAX_AGE_HR + +# Optional note of where from is email. If specified - used as part of email +# message body +# ENV ULS_ALARM_SENDER_LOCATION + +COPY --from=install_image / / + +COPY src/afc-packages /wd/afc-packages +RUN pip3 install --use-pep517 --root-user-action=ignore \ + -r /wd/afc-packages/pkgs.uls \ + && rm -rf /wd/afc-packages + +WORKDIR /wd + +ENTRYPOINT /wd/uls_service.py + +HEALTHCHECK --interval=2h --timeout=2m --retries=3 \ + CMD /wd/uls_service_healthcheck.py || exit 1 diff --git a/uls/README.md b/uls/README.md new file mode 100644 index 0000000..cc28f30 --- /dev/null +++ b/uls/README.md @@ -0,0 +1,320 @@ +Copyright (C) 2022 Broadcom. All rights reserved. +The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate that +owns the software below. This work is licensed under the OpenAFC Project +License, a copy of which is included with this software program. + +# FS Database downloader service + +## Table of contents +- [FS Downloading overview](#overview) +- [On importance of FS Database continuity](#continuity) +- [FS Downloader service script](#service) +- [Service healthcheck](#healthcheck) +- [Service state](#service_state) + - [State database](#state_database) + - [Prometheus metrics](#metrics) +- [Troubleshooting](#troubleshooting) + - [Results of last download](#results_location) + - [Redownload](#redownload) + - [`fs_db_diff.py` FS Database comparison tool](#fs_db_diff) + - [`fs_afc.py` FS Database test tool](#fs_afc) + - [`fsid_tool.py` FSID extraction/embedding tool](#fsid_tool) + + +## FS Downloading overview + +The purpose of AFC service is to avoid interference of SP APs with existing **Fixed Service** (**FS**) microwave RF transmission networks. + +To avoid this interference AFC system needs information about these FS networks (location of FS receivers and transmitters, their antenna parameters, etc.). This information is (supposed to be) maintained by regulatory authorities - such as FCC in US, etc. - in some online databases. + +FS Downloader service downloads information from these disparate sources and puts it to **FS Database** - *SQLITE3* file that is subsequently used by AFC Engine to make AFC computations. + +For historical reasons (first supported authority was FCC and there this database named **ULS**), FS database may be (and usually is) interchangeably named **ULS Database**, FS Downloader - **ULS Downloader**, etc. + +FS Downloader itself (as of time of this writing - `src/ratapi/ratapi/daily_uls_parse.py`) is being developed in its own opaque way, of which author of this text knows nothing (even after the fact - not a lot), hence it will not be documented here. + +This text documents a bunch of scripts that wrap FS Downloader to make a service inside AFC. + + +## On importance of FS Database continuity + +While performing FS Downloader maintenance and troubleshooting, it is very important to keep in mind a subtle but very important aspect of **FS Database continuity**. + +**FS Paths** (FS receivers, transmitters and receivers that define path of RF signal) have objective unique names (callsign/id/frequency). However these names are **not** used by AFC Engine. Instead, AFC Engine refers to FS Paths via **FSID**s, sequentially assigned by FS downloader script. Don't ask why. + +To maintain FSID uniqueness, FS downloader script maintains a table of already used FSIDs. Between FS download script invocation this table is stored inside FS Database. + +Hence, essentially, FSID table for new FS Database is based on one, stored in previous database. If there is no previous database - FSIDs are restarted from 0. This is not a catastrophe, but may become a major nuisance in subsequent troubleshooting. + +Hence one should always strive to maintain FS Database lineage, deriving new one from previous (usually pointed to by a symlink, passed to `uls_service.py` via `--ext_db_dir` and `--ext_db_symlink` parameters. + + +## FS Downloader service script + +Service implemented in `uls/uls_service.py` script that has the following functionality: +- Downloads FS Database by means of *daily_uls_parse.py* +- Checks database integrity. Currently the following integrity checks implemented: + - How much database changed since previous time + - If it can be used for AFC Computations without errors +- If integrity check passed and database differs from previous - copies it to destination directory and retargets the symlink that points to current FS Database. This symlink is used by rest of AFC to work with FS data +- Notifies Rcache service on where there were changes - to invalidate affected cached responses and initiate precomputation + +`uls/uls_service.py` is controlled by command line parameters, most of which can be passed via environment variables. Most of default values defined in *uls_service.py*, but some defined in *Dockerfile-uls_service*). **It is recommended to run this script without parameters** (some exception can be found in [Troubleshooting](#troubleshooting) section. + + +soTable below summarizes these parameters and environment variables: + +|Parameter|Environment variable|Default|Comment| +|---------|-----------------------|-------|-------| +|--download_script **SCRIPT**|ULS_DOWNLOAD_SCRIPT|/mnt/nfs/rat_transfer/
    daily_uls_parse/
    daily_uls_parse.py|Download script| +|--download_script_args **ARGS**|ULS_DOWNLOAD_SCRIPT_ARGS||Additional (besides *--region*) arguments for *daily_uls_parse.py*| +|--region **REG1:REG2...**|ULS_DOWNLOAD_REGION||Colon-separated list of country codes (such as US, CA, BR) to download. Default to download all supported countries| +|--result_dir **DIR**|ULS_RESULT_DIR|/mnt/nfs/rat_transfer/
    ULS_Database/|Directory where to *daily_uls_parse.py* puts downloaded database| +|--temp_dir **DIR**|ULS_TEMP_DIR|/mnt/nfs/rat_transfer/
    daily_uls_parse/temp/|Directory where to *daily_uls_parse.py* puts temporary files| +|--ext_db_dir **DIR**|ULS_EXT_DB_DIR|/*Defined in dockerfile*|Directory where to resulting database should be placed (copied from script's result directory). May be only initial part of path, rest being in `--ext_db_symlink`| +|--ext_db_symlink **FILENAME**|ULS_CURRENT_DB_SYMLINK|rat_transfer/ULS_Database/
    FS_LATEST.sqlite3
    *Defined in dockerfile*|Name of symlink in resulting directory that should be retargeted at newly downloaded database. May contain path - same that should be used in AFC Config| +|--fsid_file **FILEPATH**|ULS_FSID_FILE|/mnt/nfs/rat_transfer/
    daily_uls_parse/
    data_files/fsid_table.csv|Full name of file with existing FSIDs, used by *daily_uls_parse.py*. Between downloads this data is stored in FS Database| +|--ext_ras_database **FILENAME**|ULS_EXT_RAS_DATABASE|rat_transfer/RAS_Database/
    RASdatabase.dat
    *Defined in dockerfile*|Name of externally maintained 'RAS database' (.csv file with restricted areas)| +|--ras_database **FILENAME**|ULS_RAS_DATABASE|/mnt/nfs/rat_transfer
    /daily_uls_parse/data_files
    /RASdatabase.dat|Where from *daily_uls_parse.py* reads RAS database| +|--service_state_db_dsn
    **CONNECTION_STRING**|ULS_SERVICE_STATE_DB_DSN||Connection string to state database. It contains FS service state that is used by healthcheck script. This parameter is mandatory| +|--service_state_db_create_if_absent|ULS_SERVICE_STATE_DB_CREATE_IF_ABSENT|True|Create state database if absent| +|--service_state_db_recreate|ULS_SERVICE_STATE_DB_RECREATE|False|Recreate state database if it exists| +|--prometheus_port **PORT**|ULS_PROMETHEUS_PORT||Port to serve Prometheus metrics on (default is to not serve)| +|--statsd_server **HOST[:PORT]**|ULS_STATSD_SERVER||StatsD server to send metrics to. Default is to not send| +|--check_ext_files **BASE_URL:SUBDIR:FILENAME[,FILENAME...][;...**|ULS_CHECK_EXT_FILES|"https://raw.githubusercontent.com
    /Wireless-Innovation-Forum/
    6-GHz-AFC/main/data/common_data:
    raw_wireless_innovation_forum_files:
    antenna_model_diameter_gain.csv,
    billboard_reflector.csv,
    category_b1_antennas.csv,
    high_performance_antennas.csv,
    fcc_fixed_service_channelization.csv,
    transmit_radio_unit_architecture.csv|Certain files used by *daily_uls_parse.py* should be identical to certain files on the Internet. Comparison performed by *uls_service.py*, this parameter specifies what to compare. Several such group may be specified semicolon-separated. This parameter may be specified several times and currently hardcoded in *uls/Dockerfile-uls_service*| +|--max_change_percent **PERCENT**|ULS_MAX_CHANGE_PERCENT|10|Downloaded FS Database fails integrity check if it differs from previous by more than this percent of number of paths. If absent/empty - this check is not performed| +|--afc_url **URL**|ULS_AFC_URL||REST API URL (in *rat_server*/*msghnd*) to use for AFC computation with custom FS database as part of AFS Database integrity check. If absent/empty - this check is not performed| +|--afc_parallel **N**|ULS_AFC_PARALLEL|1|Number of parallel AFC Requests to schedule while testing FS Database on *rat_server*/*msghnd*| +|--rcache_url **URL**|RCACHE_SERVICE_URL||Rcache REST API top level URL - for invalidation of areas with changed FSs. If empty/unspecified (or if Rcache is not enabled) - see below) - no invalidation is performed| +|--rcache_enabled [**TRUE/FALSE**]|RCACHE_ENABLED|TRUE|TRUE if Rcache enabled, FALSE if not| +|--delay_hr **HOURS**|ULS_DELAY_HR|0|Delay (in hours) before first download attempt. Makes sense to be nonzero in regression testing, to avoid overloading system with unrelated stuff. Ignored if --run_once specified| +|--interval_hr **HOURS**|ULS_INTERVAL_HR|4|Interval in hours between download attempts| +|--timeout_hr **HOURS**|ULS_TIMEOUT_HR|1|Timeout in hours for *daily_uls_parse.py*| +|--nice|ULS_NICE|*Defined in Dockerfile*|Execute download on lowered priority. Values for environment variable: TRUE/FALSE| +|--run_once|ULS_RUN_ONCE||Run once (default is to run periodically). Values for environment variable: TRUE/FALSE| +|--verbose|||Print more detailed (for debug purposes)| +|--force|||Force FS database update even if it is not changed and bypassing database validity checks (e.g. to overrule them)| + + +## Service healthcheck
    + +Being bona fide docker service, ULS downloader has a healthcheck procedure. It is implemented by `uls/service_healthcheck.py` script that has the following functionality: + +- Pronounces container healthy or not +- May send **alarm** emails on what's wrong +- May send periodical **beacon** emails on overall status (even when no problems detected) + +Healthcheck script makes its conclusion based on when there was last successful something (such as download attempt, download success, database update, data change, etc.) This information is left by service script in status (aka state) directory. + +Healthcheck script invocation periodicity, timeout etc. is controlled by parameters in `HEALTHCHECK` clause of *uls/Dockerfile-uls_service*. Looks like they can't be parameterized for setting from outside, so they are hardcoded. + +*service_healthcheck.py* is controlled by command line parameters, that can be set via environment variables. **It is recommended to run this script without parameters** + +Environment variables starting with *ULS_HEALTH_* control pronouncing container health, starting with *ULS_ALARM_* control sending emails (both alarm and beacon). + +Parameters are: + +|Parameter|Environment variable|Default|Comment| +|---------|-----------------------|-------|-------| +|--service_state_db_dsn
    **CONNECTION_STRING**|ULS_SERVICE_STATE_DB_DSN||Connection string to state database. This parameter is mandatory| +|--download_attempt_max_age_health_hr **HR**|ULS_HEALTH_ATTEMPT_MAX_AGE_HR|6|Age of last download attempt in hours, enough to pronounce container as unhealthy| +|--download_success_max_age_health_hr **HR**|ULS_HEALTH_SUCCESS_MAX_AGE_HR|8|Age of last successful download in hours, enough to pronounce container as unhealthy| +|--update_max_age_health_hr **HR**|ULS_HEALTH_UPDATE_MAX_AGE_HR|40|Age of last FS data change in hours enough to pronounce container as unhealthy| +|--smtp_info **FILEPATH**|ULS_ALARM_SMTP_INFO||Name of secret file with SMTP credentials for sending emails. If empty/unspecified no emails being sent| +|--email_to **EMAIL**|ULS_ALARM_EMAIL_TO||Whom to send alarm email. If empty/unspecified no emails being sent| +|--info_email_to **EMAIL**|ULS_BEACON_EMAIL_TO||Whom to send beacon information email. If empty/unspecified uses alarm email| +|--email_sender_location **COMMENT**|ULS_ALARM_SENDER_LOCATION||Note on sending service location to use in email. Optional| +|--alarm_email_interval_hr **HR**|ULS_ALARM_ALARM_INTERVAL_HR||Minimum interval in hours between alarm emails (emails on something that went wrong). If empty/unspecified no alarm emails being sent| +|--download_attempt_max_age_alarm_hr **HR**|ULS_ALARM_ATTEMPT_MAX_AGE_HR||Minimum age in hours of last download attempt to send alarm email. If empty/unspecified - not checked| +|--download_success_max_age_alarm_hr **HR**|ULS_ALARM_SUCCESS_MAX_AGE_HR||Minimum age in hours of last successful download attempt to send alarm email. If empty/unspecified - not checked| +|--region_update_max_age_alarm **REG1:HR1,REG2:HR2...**|ULS_ALARM_REG_UPD_MAX_AGE_HR||Per-region (`US`, `CA`, `BR`...) minimum age of last data change to send alarm email. Not checked for unspecified countries| +|--beacon_email_interval_hr **HR**|ULS_ALARM_BEACON_INTERVAL_HR||Interval in hours between beacon emails (emails that contain status information even if everything goes well). If empty/unspecified - no beacon emails is being sent| +|--verbose||Print mode detailed information (for debug purposes)| +|--force_success||Return success exit code even if errors were found (used when healthcheck script called from `--run_once` service script - only to send alarm/beacon emails)| +|--print_email||Print email instead of sending it (for debug purposes)| + +## Service state
    + +`uls_service.py` provides its state (what and when was accomplished, what problems were encountered) to two places: state database (for use by healthcheck script that pronounces container health status and may send alarm/beacon emails) and, optionally, to Prometheus (that may provide this state to Grafana for nice display and/or use its alarm facility to notify about problems). + +So there are two facilities that may send alarms (healthcheck script and Prometheus). As of time of this writing Prometheus alarms not yet ready, so it is healthcheck script that is currently responsible for alarms - but ultimately it will change. + +State database contains more information than needed for pronouncement service as health or sending alarm. It is intended to be a source of information for investigation of problem reasons. + +### State database + +State database stored PostgreSQL server. Since it is disposable, it is assumed to be *bulk_postgres* server. As of time of this writing, database name is *fs_state*. This database has the following tables: + +#### `milestone` table + +Contains dates when milestones were last passed. There are following milestones: + +|Milestone|Meaning| +|---------|-------| +|ServiceBirth|`uls_service.py` started for the first time (since state database creation)| +|ServiceStart|`uls_service.py` started| +|DownloadStart|FS download script started| +|DownloadSuccess|FS download script completed successfully| +|RegionChanged|Region (country) FS data changed. This milestone is per-region| +|DbUpdated|FS database file successfully updated (after being downloaded, found to be different from previous, successfully passed checks)| +|ExtParamsChecked|External parameter files successfully checked| +|Healthcheck|Healthcheck script ran| +|BeaconSent|Beacon email sent| +|AlarmSent|Alarm email sent| + +Table structure: + +|Column|Type|Content| +|------|----|-------| +|milestone|enum|Milestone name (see table above)| +|region|string or null|Region name for region-specific milestones, null for region-inspecific milestones| +|timestamp|datetime|When milestone was last reached| + +#### `checks` table + +Contains information about passed checks. As of time of this writing there are following types of check: + +|Check Type|Meaning|Check Item| +|----------|-------|----------| +|ExtParams|Files that should be in sync with their external prototypes|File name| +|FsDatabase|FS Database validity tests|Individual tests| + +Table structure: + +|Column|Type|Content| +|------|----|-------| +|check_type|enum|Type of check (see table above)| +|check_item|string|Individual check item (see table above)| +|errmsg|string or null|Null if check passed, error message if not| +|timestamp|datetime|When check was performed| + +#### `logs` table + +Contains one last log of FS script and subsequent actions for each log type. There are following log types + +|Log Type|Meaning| +|--------|-------| +|Last|Log of last FS download run| +|LastFailed|Log of last failed FS download| +|LastCompleted|Log of last successful FS download| + +Table structure: + +|Column|Type|Content| +|------|----|-------| +|log_type|enum|Log type (see table above)| +|text|string|Log text| +|timestamp|datetime|When log was collected| + +#### `alarms` table + +Contains set of alarms sent in most recent alarm message. There are following types of alarms: + +|Alarm type|Meaning|Reason column| +|----------|-------|-------------| +|MissingMilestone|Milestone was not reached for too long|Milestone name (see chapter on milestones above)| +|FailedCheck|Failed check|Type of failed check (see chapter on checks above)| + +Table structure: + +|Column|Type|Content| +|------|----|-------| +|alarm_type|enum|Alarm type (see table above)| +|alarm_reason|string|Alarm reason (see table above)| +|timestamp|datetime|When alarm was sent + +### Prometheus metrics + +When FS downloader service runs continuously, it may serve Prometheus metrics in a normal way (at `/metrics` over HTTP through specified port). However service is executed in run-once mode - this is not possible, in this case it may send metrics to some push gateway, of which StatsD was arbitrarily chosen (as it also might be used by Gunicorn). Which one (if any) is chosen is configured through `--prometheus_port` and `--statsd_server`. Here are metrics being served: + +All metrics are gauges. Time metrics contain seconds since the epoch (January 1, 1970, UTC) + +|Metric|Label|Meaning| +|------|-----|-------| +|fs_download_started||Seconds since the epoch of last time FS downloader script started| +|fs_download_succeeded||Seconds since the epoch of last time FS downloader script succeeded| +|fs_download_database_updated||Seconds since the epoch of last time FS database file was updated| +|fs_download_region_changed|region|Seconds since the epoch of last time region data was updated| +|fs_download_check_passed|check_type|1 if last time check of given type was succeeded, 0 otherwise| + +Since StatsD does not support labels, hey are appended to metric names (e.g. `fs_download_region_changed_US` instead of `fs_download_region_changed{region="US")`). + + +## Troubleshooting + +Whenever something goes wrong with ULS download (e.g. log shows some errors, alarm email receiving, general desire to tease author of this text), one can take a look into container log to see where things went wrong and then 'exec -it sh' into FS downloader container to make further investigations. + +All scripts mentioned below (and above) are located in */wd* directory in container (*uls* directory in sources). + +**And don't forget to read a chapter on [FS Database continuity](#continuity).** + +### Results of last download + +- Underlying FS Downloader script: **src/ratapi/ratapi/daily_uls_parse.py** +- *daily_uls_parse.py*'s temporary files from last download: **/mnt/nfs/rat_transfer/daily_uls_parse/temp/** + Cleaned immediately before next download attempt +- *daily_uls_parse.py*'s results directory: **/mnt/nfs/rat_transfer/ULS_Database/** + In particular, this directory contains recently downloaded FS database, even if it had not pass integrity check. + Cleaned immediately before next download attempt + +### Redownload + +In FS downloader container issue command: +`/wd/uls_service.py --force --run_once` + +### `fs_db_diff.py` FS Database comparison tool + +`/wd/fs_db_diff.py` (`uls/fs_db_diff.py` in sources) compares two FS databases. + +This script is used by *uls_service.py* for integrity checking (to make sure that amount of change not exceed certain threshold), it also can be used standalone for troubleshooting purposes (e.g. to see what exactly changed if amount of change is too big). + +FS Database is a set of RF transmission paths over fixed networks, so comparison is possible by: + +- What paths are different +- In what locations paths are different + +General format of this script invocation is: + +`$ ./fs_db_diff.py [--report_tiles] [--report_paths] FS_DATABASE1 FS_DATABASE2` + +This script prints how many paths each database has and how many of them are different. Besides: +- If *--report_tiles* parameter specified, list of 1x1 degree tiles containing difference is printed +- If *--report_tiles* parameter specified, for each path with difference a detailed report (in what field difference was found) is printed. + + +### `fs_afc.py` FS Database test tool + +`/wd/fs_afc.py` (`uls/fs_afc.py` in sources) tests FS Database by making AFC computations on some points in some countries. Result is unimportant, but it should be computed without errors. + +This script is used by *uls_service.py* for integrity checking (to make sure that all points successfully computed), it also can be used standalone for troubleshooting purposes (e.g. to see what what's wrong with points, for which result was not computed). + +Points, countries and AFC Request pattern are specified in `/wd/fs_afc.yaml` (`uls/fs_afc.yaml` in sources) config file. It's structure will not be reviewed here - hope it is self evident enough. + +General format of this script invocation is: + +`$ ./fs_afc.py --server_url $ULS_AFC_URL [PARAMETERS] FS_DATABASE` +Here `FS_DATABASE` may have path - same as should be used in AFC Config. + +Parameters are: + +|Parameter|Comment| +|---------|-------| +|--config **FILENAME**|Config file name. Default is file with same name and in same directory as this script, but with *.yaml* extension| +|--server_url **URL**|REST API URL for submitting AFC requests with custom FS Database. Same as specified by *--afc_url* to *uls_service.py*, hence may be taken from *ULS_AFC_URL* environment variable. This parameter is mandatory| +|--region **COUNTRY_CODE**|Country code (such as US, CA, BR...) for of points to run computation for. This parameter may be specified several times. Default is to run for all points in all countries, defined in the config file| +|--parallel **NUMBER**|Number of parallel AFC requests to issue. Default is defined in config file (3 as of time of this writing)| +|--timeout_min **MINUTES**|AFC request timeout in minutes. Default is defined in config file (5 minutes as of time of this writing)| +|--point **COORD_OR_NAME**|Point to make AFC Computation for. Specified either as *LAT,LON* (latitude in north-positive degrees, longitude in east-positive degrees), or as point name (in config file each point has name). Default is to run for all points defined in all (or selected by *--region* parameter) countries| +|--failed_json **FILENAME**|File name for JSON of failed AFC Request. May be subsequently used with curl| + +### `fsid_tool.py` FSID extraction/embedding tool + +As it was explained in chapter on [FS Database continuity](#continuity), FSID table is stored in FS database. `wd/fsid_tool.py` (`uls/fsid_tool.py` in sources) script allows to extract it from FS Database to CSV and embed it from CSV to FS Database. + +General format of this script invocation: + +`./fsid_tool.py extract|embed [--partial] FS_DATABASE CSV_FILE` + +Here *extract* subcommand extracts FSID table from FS Database to CSV file, whereas *embed* subcommand embeds FSID table from CSV file to FS Database. + +*--partial* allows for unexpected column names in FS Database or CSV file (which is normally an error). diff --git a/uls/fs_afc.py b/uls/fs_afc.py new file mode 100644 index 0000000..a86785e --- /dev/null +++ b/uls/fs_afc.py @@ -0,0 +1,481 @@ +#!/usr/bin/env python3 +""" Makes AFC Requests with custom FS database """ + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=wrong-import-order, too-many-statements, too-many-branches +# pylint: disable=invalid-name, too-many-locals, logging-fstring-interpolation +# pylint: disable=too-many-nested-blocks + +import argparse +import concurrent.futures +import copy +import datetime +import http +import json +import logging +import os +import random +import re +import requests +import string +import sys +from typing import Any, Dict, List, NamedTuple, Optional, Union +import yaml + +# Name of the default config file +DEAFULT_CONFIG = os.path.realpath(os.path.splitext(__file__)[0] + ".yaml") + + +# Pydantic wouldn't be out of place here... + +# Config key for default number of parallel requests +DEFAULT_PARALLEL_KEY = "default_parallel" + +# Config key for default request timeout in minutes +DEFAULT_TIMEOUT_MIN_KEY = "default_timeout_min" + +# Config key for request format +REQ_ID_FORMAT_KEY = "req_id_format" + +# Config key for request pattern +REQ_PATTERN_KEY = "req_pattern" + +# Config key for region definition patterns +REGION_PATTERNS_KEY = "region_patterns" + +# Config key for path definitions +PATHS_KEY = "paths" + +# Config subkey for path to Request ID in AFC Request JSON +REQ_ID_PATH_KEY = "request_id" + +# Config subkey for path to region definition in AFC Request JSON +REGION_PATH_KEY = "region" + +# Config subkey for path to coordinates in AFC Request JSON +COORD_PATH_KEY = "coordinates" + +# Config subkey for path to FS Database in AFC Request JSON +FS_DATABASE_PATH_KEY = "fs_database" + +# Config subkey for path to response code in AFC Response JSON +RESPONSE_CODE_PATH_KEY = "response_code" + +# Config subkey in response description in AFC Response JSON +RESPONSE_DESC_PATH_KEY = "response_desc" + +# Config subkey in response description in AFC Response JSON +RESPONSE_SUPP_PATH_KEY = "response_supp" + +# Config subkey for descrition substring of statuses to ignore +SUCCESS_STATUSES_KEY = "success_statuses" + +# Config subkey for code of ignored status +STATUS_CODE_KEY = "code" + +# Config subkey for description of ignore dstatus +STATUS_DESC_KEY = "desc_substr" + +# Config key for point definition patterns +POINT_PATTERNS_KEY = "point_patterns" + +# Config subkey for point name +POINT_NAME_KEY = "name" + +# Config subkey for point coordinates +POINT_COORD_DICT_KEY = "coordinates" + +# Config subkey for point latitude +POINT_COORD_LAT_KEY = "latitude" + +# Config subkey to point longitude +POINT_COORD_LON_KEY = "longitude" + + +def error(msg: str) -> None: + """ Prints given msg as error message and exits abnormally """ + logging.error(msg) + sys.exit(1) + + +def error_if(cond: Any, msg: str) -> None: + """ If condition evaluates to true prints given msg as error message and + exits abnormally """ + if cond: + error(msg) + + +def json_substitute(json_obj: Union[List[Any], Dict[str, Any]], + path: List[Union[int, str]], value: Any) -> None: + """ Substitute JSON element at given path with given value + + Element at given path must exist and its current value should be 'null' + + Arguments: + json_obj -- JSON Dictionary. Updated inplace + path -- Path (list of indices) leading to desired element + value -- New value + """ + error_if(not isinstance(json_obj, dict), + "Attempt to JSON-substitute in non-JSON object") + error_if(len(path) == 0, "JSON substitution path may not be empty") + container: Optional[Union[List[Any], Dict[str, Any]]] = None + original_obj = json_obj + for idx, path_elem in enumerate(path): + if isinstance(path_elem, int): + error_if(not (isinstance(json_obj, list) and + (0 <= path_elem < len(json_obj))), + f"Path {path[:idx + 1]} is invalid for '{original_obj}'") + elif isinstance(path_elem, str): + error_if(not (isinstance(json_obj, dict) and + (path_elem in json_obj)), + f"Path {path[:idx + 1]} is invalid for '{original_obj}'") + else: + error(f"Path index '{path_elem}' has invalid type. Must be string " + f"or integer") + container = json_obj + json_obj = json_obj[path_elem] + error_if(json_obj is not None, + f"Path {path} is invalid for '{original_obj}' - must end on " + f"'null' element, instead ends on '{json_obj}'") + container[path[-1]] = value + + +def json_retrieve(json_obj: Optional[Union[List[Any], Dict[str, Any]]], + path=List[Union[int, str]]) -> Any: + """ Try to read value at given path in given JSON + + Arguments: + json_obj -- JSON object to read from + path -- Path (sequence of indices) leading to desired element + Returns element value or None + """ + original_obj = json_obj + for idx, path_elem in enumerate(path): + if json_obj is None: + return None + if isinstance(path_elem, int): + error_if(not isinstance(json_obj, list), + f"Path {path[:idx + 1]} is invalid for '{original_obj}'") + if not (0 <= path_elem < len(json_obj)): + return None + elif isinstance(path_elem, str): + error_if(not isinstance(json_obj, dict), + f"Path {path[:idx + 1]} is invalid for '{original_obj}'") + if path_elem not in json_obj: + return None + else: + error(f"Path index '{path_elem}' has invalid type. Must be string " + f"or integer") + json_obj = json_obj[path_elem] + return json_obj + + +# Response from request worker function +ResponseInfo = \ + NamedTuple( + "ResponseInfo", + [ + # AFC Response object or None + ("afc_response", Optional[Dict[str, Any]]), + # True if there was a timeout + ("timeout", bool), + # HTTP Status code + ("status_code", Optional[int]), + # Request duration in seconds + ("duration_sec", int)]) + + +def do_request(req: Dict[str, Any], url: str, timeout_sec: float) \ + -> ResponseInfo: + """ Thread worker function that performs AFC Request + + Arguments + req -- AFC Request JSON dictionary + url -- Request URL + timeout_sec -- Timeout in seconds + Returns ResponseInfo object + """ + timeout = False + start_time = datetime.datetime.now() + result: Optional[requests.Response] = None + try: + result = requests.post(url=url, json=req, timeout=timeout_sec) + except requests.Timeout: + timeout = True + return \ + ResponseInfo( + afc_response=result.json() + if (result is not None) and + (result.status_code == http.HTTPStatus.OK) + else None, + timeout=timeout, + status_code=result.status_code if result is not None else None, + duration_sec=int((datetime.datetime.now() - + start_time).total_seconds())) + + +class ReqInfo(NamedTuple): + """ Request information - request itself and stuff for its prettyprinting + """ + # Point name (City) + name: str + # Point region (Country Code) + region: str + # AFC Request for point in form of JSON dictionary + req: Dict[str, Any] + # Point latitude in north-positive degrees + lat: float + # Point longitude in east-positive degrees + lon: float + + def point_info(self) -> str: + """ Point information for messages """ + return f"{self.name} ({self.region} @ " \ + f"{abs(self.lat)}{'N' if self.lat >= 0 else 'S'}, " \ + f"{abs(self.lon)}{'E' if self.lon >= 0 else 'W'})" + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + argument_parser = \ + argparse.ArgumentParser( + description="Makes AFC Requests with custom FS database") + argument_parser.add_argument( + "--config", metavar="CONGIG", default=DEAFULT_CONFIG, + help=f"Config file name. Default is '{DEAFULT_CONFIG}'") + argument_parser.add_argument( + "--server_url", metavar="URL", required=True, + help="URL of AFC Service to use (should accept " + "openAfc.overrideAfcConfig Vendor Extension)") + argument_parser.add_argument( + "--region", metavar="REGION", action="append", default=[], + help="Region to run tests for. This parameter may be specified " + "several times. Default is to run for all regions") + argument_parser.add_argument( + "--parallel", metavar="NUM_PARALLEL_REQS", type=int, + help="Number of parallel requests. Default see in the config file") + argument_parser.add_argument( + "--timeout_min", metavar="MINUTES", type=float, + help="Request timeout in minutes. Default see in the config file") + argument_parser.add_argument( + "--point", metavar="LAT_DEG,LON_DEG OR NAME", action="append", + help="Run request for given point. Point specified either by " + "coordinates (north/east positive degrees) or by name in config file. " + "In former case exactly one --region should be specified, in latter " + "case - only if it is required for disambiguation. This parameter may " + "be specified several times") + argument_parser.add_argument( + "--failed_json", metavar="FILENAME", + help="Write json that caused fail to file") + argument_parser.add_argument( + "FS_DATABASE", help="FS (aka ULS) database (.sqlite3 file) to use. " + "Should have exactly same path as AFC Config requires") + + if not argv: + argument_parser.print_help() + sys.exit(1) + + args = argument_parser.parse_args(argv) + + console_handler = logging.StreamHandler() + console_handler.setFormatter( + logging.Formatter( + f"{os.path.basename(__file__)}. " + f"%(levelname)s: %(asctime)s %(message)s")) + logging.getLogger().addHandler(console_handler) + logging.getLogger().setLevel(logging.INFO) + + error_if(not os.path.isfile(args.config), + f"Config file '{args.config}' not found") + with open(args.config, mode="r", encoding="utf-8") as f: + config = \ + yaml.load( + f.read(), + Loader=yaml.CLoader if hasattr(yaml, "CLoader") + else yaml.Loader) + + if args.region: + error_if( + not all(r in config[REGION_PATTERNS_KEY] for r in args.region), + f"Unknown region code '{args.region[0]}'. Known regions are: " + f"{', '.join(sorted(config[REGION_PATTERNS_KEY].keys()))}") + + start_time = datetime.datetime.now() + + req_pattern_s = config[REQ_PATTERN_KEY] + try: + req_pattern = json.loads(req_pattern_s) + except json.JSONDecodeError as ex: + error(f"Syntax error in config's request pattern: {ex}") + + # Building per-region lists of points to send requests for + region_points: Dict[str, List[Dict[str, Any]]] = {} + if args.point: + for point in args.point: + m = re.match(r"^([0-9+-.]+),([0-9+-.]+)$", point) + point_dict: Dict[str, Any] + if m: + error_if(len(args.region) != 1, + "One '--region' parameter must be specified along " + "with coordinate-based '--point' parameter") + point_dict = {POINT_NAME_KEY: point} + for field, value_s in [(POINT_COORD_LAT_KEY, m.group(1)), + (POINT_COORD_LON_KEY, m.group(2))]: + try: + point_dict.setdefault(POINT_COORD_DICT_KEY, + {})[field] = float(value_s) + except ValueError: + error( + f"Wrong structure of point coordinates '{point}'") + region_points.setdefault(args.region[0], []).append(point_dict) + else: + found = False + for region in config[POINT_PATTERNS_KEY]: + if args.region and region not in args.region: + continue + for point_dict in config[POINT_PATTERNS_KEY][region]: + if point_dict[POINT_NAME_KEY] == point: + region_points.setdefault(region, + []).append(point_dict) + found = True + error_if(not found, + f"Point '{point}' not found in config file") + else: + for region in config[POINT_PATTERNS_KEY]: + error_if(region not in config[REGION_PATTERNS_KEY], + f"Config file's '{POINT_PATTERNS_KEY}' section contains " + f"region code of '{region}', not found in config's " + f"'{REGION_PATTERNS_KEY}' section") + if args.region and (region not in args.region): + continue + region_points[region] = config[POINT_PATTERNS_KEY][region] + + request_timeout_sec = \ + 60 * (args.timeout_min if args.timeout_min is not None + else config[DEFAULT_TIMEOUT_MIN_KEY]) + max_workers = args.parallel if args.parallel is not None \ + else config[DEFAULT_PARALLEL_KEY] + + success = False + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) \ + as executor: + try: + paths: Dict[str, List[Union[str, int]]] = config[PATHS_KEY] + future_to_req_info: Dict[concurrent.futures.Future, ReqInfo] = {} + # Starting requests + for region, point_list in region_points.items(): + for point in point_list: + req = copy.deepcopy(req_pattern) + json_substitute( + json_obj=req, path=paths[REQ_ID_PATH_KEY], + value=config[REQ_ID_FORMAT_KEY].format( + req_idx=len(future_to_req_info), region=region, + random_str="".join( + random.choices( + string.ascii_uppercase + string.digits, + k=10)))) + json_substitute(json_obj=req, path=paths[REGION_PATH_KEY], + value=config[REGION_PATTERNS_KEY][region]) + json_substitute(json_obj=req, path=paths[COORD_PATH_KEY], + value=point[POINT_COORD_DICT_KEY]) + json_substitute(json_obj=req, + path=paths[FS_DATABASE_PATH_KEY], + value=args.FS_DATABASE) + future_to_req_info[ + executor.submit( + do_request, req=req, url=args.server_url, + timeout_sec=request_timeout_sec + )] = ReqInfo(name=point[POINT_NAME_KEY], + region=region, req=req, + lat=point[POINT_COORD_DICT_KEY][ + POINT_COORD_LAT_KEY], + lon=point[POINT_COORD_DICT_KEY][ + POINT_COORD_LON_KEY]) + # Processing finished requests + for future in concurrent.futures.as_completed(future_to_req_info): + req_info = future_to_req_info[future] + try: + exception = future.exception() + error_if(exception is not None, + f"Request '{req_info.point_info()}' ended with " + f"exception: {exception}") + response_info: Optional[ResponseInfo] = future.result() + assert response_info is not None + error_if(response_info.timeout, + f"Request '{req_info.point_info()}' timed out") + error_if(response_info.status_code != http.HTTPStatus.OK, + f"Request '{req_info.point_info()}' ended with " + f"status code {response_info.status_code}") + response_code = \ + json_retrieve(json_obj=response_info.afc_response, + path=paths[RESPONSE_CODE_PATH_KEY]) + response_desc = \ + json_retrieve(json_obj=response_info.afc_response, + path=paths[RESPONSE_DESC_PATH_KEY]) + response_supp = \ + json_retrieve(json_obj=response_info.afc_response, + path=paths[RESPONSE_SUPP_PATH_KEY]) + for ss in config[SUCCESS_STATUSES_KEY]: + if (STATUS_CODE_KEY in ss) and \ + (ss[STATUS_CODE_KEY] != response_code): + continue + if (STATUS_DESC_KEY in ss) and \ + (ss[STATUS_DESC_KEY] not in + (response_desc or "")): + continue + break + else: + response_desc_msg = "" + if response_desc or response_supp: + response_desc_msg = \ + " (" + \ + ", ".join(str(v) for v in + [response_desc, response_supp] + if v) + \ + ")" + error(f"Request '{req_info.point_info()}' ended with " + f"AFC response code " + f"{response_code}{response_desc_msg}") + success_verdict = "successfully completed" \ + if response_code == 0 else "failed without prejudice" + logging.info( + f"Request '{req_info.point_info()}' {success_verdict} " + f"in {response_info.duration_sec} seconds") + except KeyboardInterrupt: + break + except: # noqa + if args.failed_json: + with open(args.failed_json, mode="w", + encoding="utf-8") as f: + json.dump(req_info.req, f, indent=4, + sort_keys=False) + logging.info( + f"Offending request JSON written to " + f"'{os.path.realpath(args.failed_json)}'. Happy " + f"curling") + raise + success = True + except KeyboardInterrupt: + pass + finally: + if not success: + logging.info("Waiting for completion of in-progress requests") + executor.shutdown(wait=True, cancel_futures=True) + elapsed_seconds = \ + int((datetime.datetime.now() - start_time).total_seconds()) + logging.info(f"{len(future_to_req_info)} requests were processed in " + f"{elapsed_seconds // 60} min {elapsed_seconds % 60} sec") + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/uls/fs_afc.yaml b/uls/fs_afc.yaml new file mode 100644 index 0000000..1673e1e --- /dev/null +++ b/uls/fs_afc.yaml @@ -0,0 +1,830 @@ +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# Parameter file for fs_afc.py. +# Contains request template(s), region template(s), point coordinates +--- +# Default number of parallel requests +default_parallel: 3 + +# Default request timeout in minutes +default_timeout_min: 5 + +# Request ID format. May use following values: +# req_idx - 0-based request index +# region - 2-letter region name +# random_str - 10-character random string (uppercase + digits) +req_id_format: "FS_DOWNLOADER_{region}" + +# Substitution points (see 'paths' dictionary below must be set to null) +req_pattern: | + { + "version" : "1.4", + "availableSpectrumInquiryRequests": [ + { + "requestId": null, + "deviceDescriptor": { + "serialNumber": "FS_ACCEPTANCE_TEST", + "certificationId": [ null ] + }, + "location": { + "ellipse": { + "center": null, + "majorAxis": 20, + "minorAxis": 20, + "orientation": 70 + }, + "elevation": { + "height": 20.0, + "heightType": "AGL", + "verticalUncertainty": 2 + }, + "indoorDeployment": 2 + }, + "inquiredFrequencyRange": [ + { "lowFrequency": 5925, "highFrequency": 6425 }, + { "lowFrequency": 6525, "highFrequency": 6875 } + ], + "inquiredChannels": [ + { "globalOperatingClass": 131 }, + { "globalOperatingClass": 132 }, + { "globalOperatingClass": 133 }, + { "globalOperatingClass": 134 }, + { "globalOperatingClass": 135 }, + { "globalOperatingClass": 136 } + ], + "minDesiredPower": 24, + "vendorExtensions": [ + { + "extensionId": "openAfc.overrideAfcConfig", + "parameters": { + "fsDatabaseFile": null + } + } + ] + } + ] + } + +region_patterns: + US: + rulesetId: US_47_CFR_PART_15_SUBPART_E + id: FsDownloaderCertIdUS + CA: + rulesetId: CA_RES_DBS-06 + id: FsDownloaderCertIdCA + BR: + rulesetId: BRAZIL_RULESETID + id: FsDownloaderCertIdBR + +paths: + request_id: [availableSpectrumInquiryRequests, 0, requestId] + region: [availableSpectrumInquiryRequests, 0, deviceDescriptor, + certificationId, 0] + coordinates: [availableSpectrumInquiryRequests, 0, location, ellipse, + center] + fs_database: [availableSpectrumInquiryRequests, 0, vendorExtensions, 0, + parameters, fsDatabaseFile] + response_code: [availableSpectrumInquiryResponses, 0, response, + responseCode] + response_desc: [availableSpectrumInquiryResponses, 0, response, + shortDescription] + response_supp: [availableSpectrumInquiryResponses, 0, response, + supplementalInfo] + +# List of statuses to be considered successful. Each element may include 'code' +# (for a value of response/responseCode) and/or 'desc_substr' for substring of +# response/shortDescription +success_statuses: + - code: 0 + - code: 101 + desc_substr: No AFC configuration for ruleset + +point_patterns: + US: + - name: "New York" + coordinates: {latitude: 40.67, longitude: -73.94} + - name: "Los Angeles" + coordinates: {latitude: 34.11, longitude: -118.41} + - name: "Chicago" + coordinates: {latitude: 41.84, longitude: -87.68} + - name: "Houston" + coordinates: {latitude: 29.77, longitude: -95.39} + - name: "Philadelphia" + coordinates: {latitude: 40.01, longitude: -75.13} + - name: "Phoenix" + coordinates: {latitude: 33.54, longitude: -112.07} + - name: "San Diego" + coordinates: {latitude: 32.81, longitude: -117.14} + - name: "San Antonio" + coordinates: {latitude: 29.46, longitude: -98.51} + - name: "Dallas" + coordinates: {latitude: 32.79, longitude: -96.77} + - name: "Detroit" + coordinates: {latitude: 42.38, longitude: -83.1} + - name: "San Jose" + coordinates: {latitude: 37.3, longitude: -121.85} + - name: "Jacksonville" + coordinates: {latitude: 30.33, longitude: -81.66} + - name: "Indianapolis" + coordinates: {latitude: 39.78, longitude: -86.15} + - name: "San Francisco" + coordinates: {latitude: 37.77, longitude: -122.45} + - name: "Columbus" + coordinates: {latitude: 39.99, longitude: -82.99} + - name: "Austin" + coordinates: {latitude: 30.31, longitude: -97.75} + - name: "Memphis" + coordinates: {latitude: 35.11, longitude: -90.01} + - name: "Baltimore" + coordinates: {latitude: 39.3, longitude: -76.61} + - name: "Fort Worth" + coordinates: {latitude: 32.75, longitude: -97.34} + - name: "El Paso" + coordinates: {latitude: 31.85, longitude: -106.44} + - name: "Charlotte" + coordinates: {latitude: 35.2, longitude: -80.83} + - name: "Milwaukee" + coordinates: {latitude: 43.06, longitude: -87.97} + - name: "Boston" + coordinates: {latitude: 42.34, longitude: -71.02} + - name: "Seattle" + coordinates: {latitude: 47.62, longitude: -122.35} + - name: "Denver" + coordinates: {latitude: 39.77, longitude: -104.87} + - name: "Washington" + coordinates: {latitude: 38.91, longitude: -77.02} + - name: "Nashville" + coordinates: {latitude: 36.17, longitude: -86.78} + - name: "Portland" + coordinates: {latitude: 45.54, longitude: -122.66} + - name: "Las Vegas" + coordinates: {latitude: 36.21, longitude: -115.22} + - name: "Oklahoma City" + coordinates: {latitude: 35.47, longitude: -97.51} + - name: "Tucson" + coordinates: {latitude: 32.2, longitude: -110.89} + - name: "Long Beach" + coordinates: {latitude: 33.79, longitude: -118.16} + - name: "Albuquerque" + coordinates: {latitude: 35.12, longitude: -106.62} + - name: "New Orleans" + coordinates: {latitude: 30.07, longitude: -89.93} + - name: "Cleveland" + coordinates: {latitude: 41.48, longitude: -81.68} + - name: "Fresno" + coordinates: {latitude: 36.78, longitude: -119.79} + - name: "Sacramento" + coordinates: {latitude: 38.57, longitude: -121.47} + - name: "Virginia Beach" + coordinates: {latitude: 36.74, longitude: -76.04} + - name: "Kansas City" + coordinates: {latitude: 39.12, longitude: -94.55} + - name: "Mesa" + coordinates: {latitude: 33.42, longitude: -111.74} + - name: "Atlanta" + coordinates: {latitude: 33.76, longitude: -84.42} + - name: "Omaha" + coordinates: {latitude: 41.26, longitude: -96.01} + - name: "Oakland" + coordinates: {latitude: 37.77, longitude: -122.22} + - name: "Tulsa" + coordinates: {latitude: 36.13, longitude: -95.92} + - name: "Honolulu" + coordinates: {latitude: 21.32, longitude: -157.8} + - name: "Miami" + coordinates: {latitude: 25.78, longitude: -80.21} + - name: "Minneapolis" + coordinates: {latitude: 44.96, longitude: -93.27} + - name: "Colorado Springs" + coordinates: {latitude: 38.86, longitude: -104.76} + - name: "Arlington" + coordinates: {latitude: 32.69, longitude: -97.13} + - name: "Wichita" + coordinates: {latitude: 37.69, longitude: -97.34} + - name: "Santa Ana" + coordinates: {latitude: 33.74, longitude: -117.88} + - name: "Anaheim" + coordinates: {latitude: 33.84, longitude: -117.87} + - name: "Saint Louis" + coordinates: {latitude: 38.64, longitude: -90.24} + - name: "Raleigh" + coordinates: {latitude: 35.82, longitude: -78.66} + - name: "Pittsburgh" + coordinates: {latitude: 40.44, longitude: -79.98} + - name: "Tampa" + coordinates: {latitude: 27.96, longitude: -82.48} + - name: "Cincinnati" + coordinates: {latitude: 39.14, longitude: -84.51} + - name: "Toledo" + coordinates: {latitude: 41.66, longitude: -83.58} + - name: "Aurora" + coordinates: {latitude: 39.71, longitude: -104.73} + - name: "Riverside" + coordinates: {latitude: 33.94, longitude: -117.4} + - name: "Buffalo" + coordinates: {latitude: 42.89, longitude: -78.86} + - name: "Corpus Christi" + coordinates: {latitude: 27.71, longitude: -97.35} + - name: "Newark" + coordinates: {latitude: 40.72, longitude: -74.17} + - name: "Saint Paul" + coordinates: {latitude: 44.95, longitude: -93.1} + - name: "Bakersfield" + coordinates: {latitude: 35.36, longitude: -119} + - name: "Stockton" + coordinates: {latitude: 37.97, longitude: -121.31} + - name: "Anchorage" + coordinates: {latitude: 61.18, longitude: -149.19} + - name: "Lexington-Fayette" + coordinates: {latitude: 38.04, longitude: -84.46} + - name: "Saint Petersburg" + coordinates: {latitude: 27.76, longitude: -82.64} + - name: "Louisville" + coordinates: {latitude: 38.22, longitude: -85.74} + - name: "Plano" + coordinates: {latitude: 33.05, longitude: -96.75} + - name: "Norfolk" + coordinates: {latitude: 36.92, longitude: -76.24} + - name: "Jersey City" + coordinates: {latitude: 40.71, longitude: -74.06} + - name: "Lincoln" + coordinates: {latitude: 40.82, longitude: -96.69} + - name: "Glendale" + coordinates: {latitude: 33.58, longitude: -112.2} + - name: "Birmingham" + coordinates: {latitude: 33.53, longitude: -86.8} + - name: "Greensboro" + coordinates: {latitude: 36.08, longitude: -79.83} + - name: "Hialeah" + coordinates: {latitude: 25.86, longitude: -80.3} + - name: "Fort Wayne" + coordinates: {latitude: 41.07, longitude: -85.14} + - name: "Baton Rouge" + coordinates: {latitude: 30.45, longitude: -91.13} + - name: "Henderson" + coordinates: {latitude: 36.03, longitude: -115} + - name: "Scottsdale" + coordinates: {latitude: 33.69, longitude: -111.87} + - name: "Madison" + coordinates: {latitude: 43.08, longitude: -89.39} + - name: "Chandler" + coordinates: {latitude: 33.3, longitude: -111.87} + - name: "Garland" + coordinates: {latitude: 32.91, longitude: -96.63} + - name: "Chesapeake" + coordinates: {latitude: 36.68, longitude: -76.31} + - name: "Rochester" + coordinates: {latitude: 43.17, longitude: -77.62} + - name: "Akron" + coordinates: {latitude: 41.08, longitude: -81.52} + - name: "Modesto" + coordinates: {latitude: 37.66, longitude: -120.99} + - name: "Lubbock" + coordinates: {latitude: 33.58, longitude: -101.88} + - name: "Paradise" + coordinates: {latitude: 36.08, longitude: -115.13} + - name: "Orlando" + coordinates: {latitude: 28.5, longitude: -81.37} + - name: "Fremont" + coordinates: {latitude: 37.53, longitude: -122} + - name: "Chula Vista" + coordinates: {latitude: 32.63, longitude: -117.04} + - name: "Laredo" + coordinates: {latitude: 27.53, longitude: -99.49} + - name: "Glendale" + coordinates: {latitude: 34.18, longitude: -118.25} + - name: "Durham" + coordinates: {latitude: 35.98, longitude: -78.91} + - name: "Montgomery" + coordinates: {latitude: 32.35, longitude: -86.28} + - name: "San Bernardino" + coordinates: {latitude: 34.14, longitude: -117.29} + - name: "Reno" + coordinates: {latitude: 39.54, longitude: -119.82} + - name: "Shreveport" + coordinates: {latitude: 32.47, longitude: -93.8} + - name: "Yonkers" + coordinates: {latitude: 40.95, longitude: -73.87} + - name: "Spokane" + coordinates: {latitude: 47.67, longitude: -117.41} + - name: "Tacoma" + coordinates: {latitude: 47.25, longitude: -122.46} + - name: "Huntington Beach" + coordinates: {latitude: 33.69, longitude: -118.01} + - name: "Grand Rapids" + coordinates: {latitude: 42.96, longitude: -85.66} + - name: "Des Moines" + coordinates: {latitude: 41.58, longitude: -93.62} + - name: "Augusta-Richmond" + coordinates: {latitude: 33.46, longitude: -81.99} + - name: "Irving" + coordinates: {latitude: 32.86, longitude: -96.97} + - name: "Richmond" + coordinates: {latitude: 37.53, longitude: -77.47} + - name: "Mobile" + coordinates: {latitude: 30.68, longitude: -88.09} + - name: "Winston-Salem" + coordinates: {latitude: 36.1, longitude: -80.26} + - name: "Boise City" + coordinates: {latitude: 43.61, longitude: -116.23} + - name: "Arlington" + coordinates: {latitude: 38.88, longitude: -77.1} + - name: "Columbus" + coordinates: {latitude: 32.51, longitude: -84.87} + - name: "Little Rock" + coordinates: {latitude: 34.72, longitude: -92.35} + - name: "Oxnard" + coordinates: {latitude: 34.2, longitude: -119.21} + - name: "Newport News" + coordinates: {latitude: 37.08, longitude: -76.51} + - name: "Amarillo" + coordinates: {latitude: 35.2, longitude: -101.82} + - name: "Salt Lake City" + coordinates: {latitude: 40.78, longitude: -111.93} + # CA: + # - name: "Toronto" + # coordinates: {latitude: 43.65, longitude: -79.38} + # - name: "Montreal" + # coordinates: {latitude: 45.52, longitude: -73.57} + # - name: "Vancouver" + # coordinates: {latitude: 49.28, longitude: -123.13} + # - name: "Calgary" + # coordinates: {latitude: 51.05, longitude: -114.06} + # - name: "Ottawa" + # coordinates: {latitude: 45.42, longitude: -75.71} + # - name: "Edmonton" + # coordinates: {latitude: 53.57, longitude: -113.54} + # - name: "Hamilton" + # coordinates: {latitude: 43.26, longitude: -79.85} + # - name: "Quebec" + # coordinates: {latitude: 46.82, longitude: -71.23} + # - name: "Winnipeg" + # coordinates: {latitude: 49.88, longitude: -97.17} + # - name: "Kitchener" + # coordinates: {latitude: 43.46, longitude: -80.5} + # - name: "London" + # coordinates: {latitude: 42.97, longitude: -81.24} + # - name: "Saint Catharines-Niagara" + # coordinates: {latitude: 43.18, longitude: -79.24} + # - name: "Victoria" + # coordinates: {latitude: 48.43, longitude: -123.37} + # - name: "Windsor" + # coordinates: {latitude: 42.3, longitude: -83.03} + # - name: "Halifax" + # coordinates: {latitude: 44.67, longitude: -63.61} + # - name: "Oshawa" + # coordinates: {latitude: 43.89, longitude: -78.86} + # - name: "Gatineau" + # coordinates: {latitude: 45.42, longitude: -75.71} + # - name: "Saskatoon" + # coordinates: {latitude: 52.15, longitude: -106.66} + # - name: "Regina" + # coordinates: {latitude: 50.45, longitude: -104.61} + # - name: "Barrie" + # coordinates: {latitude: 44.38, longitude: -79.68} + # - name: "Abbotsford" + # coordinates: {latitude: 49.06, longitude: -122.3} + # - name: "Sherbrooke" + # coordinates: {latitude: 45.4, longitude: -71.9} + # - name: "Trois-Rivieres" + # coordinates: {latitude: 46.35, longitude: -72.57} + # - name: "Kelowna" + # coordinates: {latitude: 49.89, longitude: -119.46} + # - name: "Saint John's" + # coordinates: {latitude: 47.58, longitude: -52.69} + # - name: "Guelph" + # coordinates: {latitude: 43.56, longitude: -80.26} + # - name: "Kingston" + # coordinates: {latitude: 44.23, longitude: -76.5} + # - name: "Chicoutimi-Jonquiere" + # coordinates: {latitude: 48.43, longitude: -71.08} + # - name: "Sudbury" + # coordinates: {latitude: 46.49, longitude: -81.01} + # - name: "Thunder Bay" + # coordinates: {latitude: 48.42, longitude: -89.28} + # - name: "Saint John" + # coordinates: {latitude: 45.29, longitude: -66.06} + # - name: "Brantford" + # coordinates: {latitude: 43.15, longitude: -80.26} + # - name: "Moncton" + # coordinates: {latitude: 46.09, longitude: -64.78} + # - name: "Sarnia" + # coordinates: {latitude: 42.99, longitude: -82.4} + # - name: "Nanaimo" + # coordinates: {latitude: 49.21, longitude: -123.97} + # - name: "Kanata" + # coordinates: {latitude: 45.34, longitude: -75.88} + # - name: "Peterborough" + # coordinates: {latitude: 44.3, longitude: -78.34} + # - name: "Red Deer" + # coordinates: {latitude: 52.27, longitude: -113.83} + # - name: "Saint-Jean-Sur-Richelieu" + # coordinates: {latitude: 45.31, longitude: -73.26} + # - name: "Lethbridge" + # coordinates: {latitude: 49.69, longitude: -112.83} + # - name: "Kamloops" + # coordinates: {latitude: 50.68, longitude: -120.34} + # - name: "Sault Sainte Marie" + # coordinates: {latitude: 46.53, longitude: -84.35} + # - name: "White Rock" + # coordinates: {latitude: 49.03, longitude: -122.83} + # - name: "Prince George" + # coordinates: {latitude: 53.91, longitude: -122.77} + # - name: "Belleville" + # coordinates: {latitude: 44.17, longitude: -77.38} + # - name: "Medicine Hat" + # coordinates: {latitude: 50.04, longitude: -110.69} + # - name: "Drummondville" + # coordinates: {latitude: 45.89, longitude: -72.49} + # - name: "Saint-Jerome" + # coordinates: {latitude: 45.78, longitude: -74} + # - name: "Granby" + # coordinates: {latitude: 45.41, longitude: -72.73} + # - name: "Fredericton" + # coordinates: {latitude: 45.96, longitude: -66.66} + # - name: "Chilliwack" + # coordinates: {latitude: 49.17, longitude: -121.96} + # - name: "North Bay" + # coordinates: {latitude: 46.3, longitude: -79.45} + # - name: "Cornwall" + # coordinates: {latitude: 45.03, longitude: -74.74} + # - name: "Shawinigan" + # coordinates: {latitude: 46.56, longitude: -72.75} + # - name: "Saint-Hyacinthe" + # coordinates: {latitude: 45.63, longitude: -72.96} + # - name: "Chatham" + # coordinates: {latitude: 42.41, longitude: -82.19} + # - name: "Vernon" + # coordinates: {latitude: 50.27, longitude: -119.28} + # - name: "Beloeil" + # coordinates: {latitude: 45.55, longitude: -73.23} + # - name: "Wood Buffalo" + # coordinates: {latitude: 56.74, longitude: -111.43} + # - name: "Charlottetown" + # coordinates: {latitude: 46.24, longitude: -63.14} + # - name: "Grande Prairie" + # coordinates: {latitude: 55.18, longitude: -118.8} + # - name: "Georgina" + # coordinates: {latitude: 44.27, longitude: -79.25} + # - name: "Salaberry-De-Valleyfield" + # coordinates: {latitude: 45.26, longitude: -74.14} + # - name: "Saint Thomas" + # coordinates: {latitude: 42.78, longitude: -81.19} + # - name: "Rimouski" + # coordinates: {latitude: 48.44, longitude: -68.54} + # - name: "Sorel" + # coordinates: {latitude: 46.05, longitude: -73.14} + # - name: "Penticton" + # coordinates: {latitude: 49.5, longitude: -119.59} + # - name: "Victoriaville" + # coordinates: {latitude: 46.06, longitude: -71.96} + # - name: "Joliette" + # coordinates: {latitude: 46.03, longitude: -73.45} + # - name: "Prince Albert" + # coordinates: {latitude: 53.2, longitude: -105.75} + # - name: "Woodstock" + # coordinates: {latitude: 43.13, longitude: -80.76} + # - name: "Bowmanville-Newcastle" + # coordinates: {latitude: 43.9, longitude: -78.68} + # - name: "Sydney" + # coordinates: {latitude: 46.2, longitude: -59.97} + # - name: "Courtenay" + # coordinates: {latitude: 49.69, longitude: -125} + # - name: "Georgetown" + # coordinates: {latitude: 43.66, longitude: -79.93} + # - name: "Timmins" + # coordinates: {latitude: 48.49, longitude: -81.35} + # - name: "Campbell River" + # coordinates: {latitude: 49.99, longitude: -125.23} + # - name: "Moose Jaw" + # coordinates: {latitude: 50.39, longitude: -105.54} + # - name: "Midland" + # coordinates: {latitude: 44.75, longitude: -79.89} + # - name: "Stratford" + # coordinates: {latitude: 43.37, longitude: -80.98} + # - name: "Orangeville" + # coordinates: {latitude: 43.92, longitude: -80.1} + # - name: "Orillia" + # coordinates: {latitude: 44.6, longitude: -79.42} + # - name: "Leamington" + # coordinates: {latitude: 42.06, longitude: -82.6} + # - name: "Alma" + # coordinates: {latitude: 48.55, longitude: -71.66} + # - name: "Brandon" + # coordinates: {latitude: 49.84, longitude: -99.96} + # - name: "Nanticoke" + # coordinates: {latitude: 42.81, longitude: -80.09} + # - name: "North Cowichan" + # coordinates: {latitude: 48.82, longitude: -123.73} + # - name: "Val-D'or" + # coordinates: {latitude: 48.11, longitude: -77.79} + # - name: "Rouyn-Noranda" + # coordinates: {latitude: 48.24, longitude: -79.03} + # - name: "Brockville" + # coordinates: {latitude: 44.61, longitude: -75.7} + # - name: "Milton" + # coordinates: {latitude: 43.51, longitude: -79.89} + # - name: "Sept-Iles" + # coordinates: {latitude: 50.22, longitude: -66.38} + # - name: "Central Okanagan" + # coordinates: {latitude: 50.03, longitude: -119.87} + # - name: "Owen Sound" + # coordinates: {latitude: 44.57, longitude: -80.94} + # - name: "Airdrie" + # coordinates: {latitude: 51.3, longitude: -114.02} + # - name: "Duncan" + # coordinates: {latitude: 48.78, longitude: -123.7} + # - name: "Lloydminster" + # coordinates: {latitude: 53.28, longitude: -110.01} + # - name: "Thetford Mines" + # coordinates: {latitude: 46.1, longitude: -71.31} + # - name: "Walnut Grove" + # coordinates: {latitude: 49.19, longitude: -122.62} + # - name: "Bolton" + # coordinates: {latitude: 43.88, longitude: -79.73} + # - name: "Buckingham" + # coordinates: {latitude: 45.58, longitude: -75.42} + # - name: "Saint-Georges" + # coordinates: {latitude: 46.13, longitude: -70.68} + # - name: "Parksville" + # coordinates: {latitude: 49.33, longitude: -124.33} + # - name: "Port Alberni" + # coordinates: {latitude: 49.27, longitude: -124.82} + # - name: "Truro" + # coordinates: {latitude: 45.36, longitude: -63.28} + # - name: "Glace Bay" + # coordinates: {latitude: 46.2, longitude: -59.97} + # - name: "New Glasgow" + # coordinates: {latitude: 45.59, longitude: -62.65} + # - name: "Magog" + # coordinates: {latitude: 45.26, longitude: -72.14} + # - name: "Corner Brook" + # coordinates: {latitude: 48.96, longitude: -57.96} + # - name: "Whitehorse" + # coordinates: {latitude: 60.73, longitude: -135.05} + # - name: "Valley East" + # coordinates: {latitude: 46.64, longitude: -81.01} + # - name: "Cranbrook" + # coordinates: {latitude: 49.51, longitude: -115.77} + # - name: "Terrace" + # coordinates: {latitude: 54.52, longitude: -128.61} + # - name: "Bradford" + # coordinates: {latitude: 44.12, longitude: -79.56} + # - name: "North Battleford" + # coordinates: {latitude: 52.79, longitude: -108.29} + # - name: "Lindsay" + # coordinates: {latitude: 44.35, longitude: -78.74} + # - name: "Cobourg" + # coordinates: {latitude: 43.96, longitude: -78.17} + # - name: "Spruce Grove" + # coordinates: {latitude: 53.53, longitude: -113.92} + # - name: "Fergus" + # coordinates: {latitude: 43.7, longitude: -80.37} + # - name: "Bathurst" + # coordinates: {latitude: 47.62, longitude: -65.65} + # BR: + # - name: "Sao Paulo" + # coordinates: {latitude: -23.53, longitude: -46.63} + # - name: "Rio De Janeiro" + # coordinates: {latitude: -22.91, longitude: -43.2} + # - name: "Salvador" + # coordinates: {latitude: -12.97, longitude: -38.5} + # - name: "Belo Horizonte" + # coordinates: {latitude: -19.92, longitude: -43.94} + # - name: "Fortaleza" + # coordinates: {latitude: -3.78, longitude: -38.59} + # - name: "Brasilia" + # coordinates: {latitude: -15.78, longitude: -47.91} + # - name: "Curitiba" + # coordinates: {latitude: -25.42, longitude: -49.29} + # - name: "Manaus" + # coordinates: {latitude: -3.12, longitude: -60.02} + # - name: "Recife" + # coordinates: {latitude: -8.08, longitude: -34.92} + # - name: "Belem" + # coordinates: {latitude: -1.44, longitude: -48.5} + # - name: "Porto Alegre" + # coordinates: {latitude: -30.04, longitude: -51.22} + # - name: "Goiania" + # coordinates: {latitude: -16.72, longitude: -49.26} + # - name: "Guarulhos" + # coordinates: {latitude: -23.46, longitude: -46.49} + # - name: "Campinas" + # coordinates: {latitude: -22.91, longitude: -47.08} + # - name: "Nova Iguacu" + # coordinates: {latitude: -22.74, longitude: -43.47} + # - name: "Sao Goncalo" + # coordinates: {latitude: -22.84, longitude: -43.07} + # - name: "Sao Luis" + # coordinates: {latitude: -2.5, longitude: -44.3} + # - name: "Maceio" + # coordinates: {latitude: -9.65, longitude: -35.75} + # - name: "Duque De Caxias" + # coordinates: {latitude: -22.77, longitude: -43.31} + # - name: "Natal" + # coordinates: {latitude: -5.8, longitude: -35.22} + # - name: "Sao Bernardo Do Campo" + # coordinates: {latitude: -23.71, longitude: -46.54} + # - name: "Teresina" + # coordinates: {latitude: -5.1, longitude: -42.8} + # - name: "Campo Grande" + # coordinates: {latitude: -20.45, longitude: -54.63} + # - name: "Osasco" + # coordinates: {latitude: -23.53, longitude: -46.78} + # - name: "Santo Andre" + # coordinates: {latitude: -23.65, longitude: -46.53} + # - name: "Jaboatao" + # coordinates: {latitude: -8.11, longitude: -35.02} + # - name: "Joao Pessoa" + # coordinates: {latitude: -7.12, longitude: -34.86} + # - name: "Contagem" + # coordinates: {latitude: -19.91, longitude: -44.1} + # - name: "Sao Jose Dos Campos" + # coordinates: {latitude: -23.2, longitude: -45.88} + # - name: "Ribeirao Preto" + # coordinates: {latitude: -21.17, longitude: -47.8} + # - name: "Sorocaba" + # coordinates: {latitude: -23.49, longitude: -47.47} + # - name: "Uberlandia" + # coordinates: {latitude: -18.9, longitude: -48.28} + # - name: "Cuiaba" + # coordinates: {latitude: -15.61, longitude: -56.09} + # - name: "Aracaju" + # coordinates: {latitude: -10.91, longitude: -37.07} + # - name: "Niteroi" + # coordinates: {latitude: -22.9, longitude: -43.13} + # - name: "Juiz De Fora" + # coordinates: {latitude: -21.75, longitude: -43.36} + # - name: "Sao Joao De Meriti" + # coordinates: {latitude: -22.8, longitude: -43.35} + # - name: "Belford Roxo" + # coordinates: {latitude: -22.75, longitude: -43.42} + # - name: "Londrina" + # coordinates: {latitude: -23.3, longitude: -51.18} + # - name: "Feira De Santana" + # coordinates: {latitude: -12.25, longitude: -38.97} + # - name: "Joinville" + # coordinates: {latitude: -26.32, longitude: -48.84} + # - name: "Ananindeua" + # coordinates: {latitude: -1.38, longitude: -48.38} + # - name: "Santos" + # coordinates: {latitude: -23.95, longitude: -46.33} + # - name: "Aparecida De Goiania" + # coordinates: {latitude: -16.82, longitude: -49.24} + # - name: "Campos" + # coordinates: {latitude: -21.75, longitude: -41.34} + # - name: "Diadema" + # coordinates: {latitude: -23.69, longitude: -46.61} + # - name: "Vila Velha" + # coordinates: {latitude: -20.32, longitude: -40.28} + # - name: "Maua" + # coordinates: {latitude: -23.66, longitude: -46.46} + # - name: "Florianopolis" + # coordinates: {latitude: -27.6, longitude: -48.54} + # - name: "Olinda" + # coordinates: {latitude: -8, longitude: -34.85} + # - name: "Sao Jose Do Rio Preto" + # coordinates: {latitude: -20.8, longitude: -49.39} + # - name: "Caxias Do Sul" + # coordinates: {latitude: -29.18, longitude: -51.17} + # - name: "Serra" + # coordinates: {latitude: -20.13, longitude: -40.32} + # - name: "Carapicuiba" + # coordinates: {latitude: -23.52, longitude: -46.84} + # - name: "Campina Grande" + # coordinates: {latitude: -7.23, longitude: -35.88} + # - name: "Betim" + # coordinates: {latitude: -19.97, longitude: -44.19} + # - name: "Piracicaba" + # coordinates: {latitude: -22.71, longitude: -47.64} + # - name: "Cariacica" + # coordinates: {latitude: -20.23, longitude: -40.37} + # - name: "Bauru" + # coordinates: {latitude: -22.33, longitude: -49.08} + # - name: "Macapa" + # coordinates: {latitude: 0.04, longitude: -51.05} + # - name: "Canoas" + # coordinates: {latitude: -29.92, longitude: -51.18} + # - name: "Sao Vicente" + # coordinates: {latitude: -23.96, longitude: -46.39} + # - name: "Moji Das Cruzes" + # coordinates: {latitude: -23.52, longitude: -46.21} + # - name: "Jundiai" + # coordinates: {latitude: -23.2, longitude: -46.88} + # - name: "Montes Claros" + # coordinates: {latitude: -16.72, longitude: -43.86} + # - name: "Pelotas" + # coordinates: {latitude: -31.76, longitude: -52.34} + # - name: "Vitoria" + # coordinates: {latitude: -20.31, longitude: -40.31} + # - name: "Anapolis" + # coordinates: {latitude: -16.32, longitude: -48.96} + # - name: "Itaquaquecetuba" + # coordinates: {latitude: -23.47, longitude: -46.35} + # - name: "Porto Velho" + # coordinates: {latitude: -8.76, longitude: -63.91} + # - name: "Maringa" + # coordinates: {latitude: -23.41, longitude: -51.93} + # - name: "Franca" + # coordinates: {latitude: -20.53, longitude: -47.39} + # - name: "Guaruja" + # coordinates: {latitude: -24, longitude: -46.27} + # - name: "Ribeirao Das Neves" + # coordinates: {latitude: -19.76, longitude: -44.08} + # - name: "Ponta Grossa" + # coordinates: {latitude: -25.09, longitude: -50.16} + # - name: "Paulista" + # coordinates: {latitude: -7.9, longitude: -34.91} + # - name: "Foz Do Iguacu" + # coordinates: {latitude: -25.55, longitude: -54.58} + # - name: "Petropolis" + # coordinates: {latitude: -22.51, longitude: -43.2} + # - name: "Blumenau" + # coordinates: {latitude: -26.92, longitude: -49.09} + # - name: "Limeira" + # coordinates: {latitude: -22.55, longitude: -47.4} + # - name: "Suzano" + # coordinates: {latitude: -23.55, longitude: -46.31} + # - name: "Uberaba" + # coordinates: {latitude: -19.76, longitude: -47.94} + # - name: "Caucaia" + # coordinates: {latitude: -3.74, longitude: -38.67} + # - name: "Volta Redonda" + # coordinates: {latitude: -22.51, longitude: -44.12} + # - name: "Rio Branco" + # coordinates: {latitude: -9.98, longitude: -67.82} + # - name: "Governador Valadares" + # coordinates: {latitude: -18.87, longitude: -41.97} + # - name: "Novo Hamburgo" + # coordinates: {latitude: -29.71, longitude: -51.14} + # - name: "Cascavel" + # coordinates: {latitude: -24.96, longitude: -53.46} + # - name: "Taubate" + # coordinates: {latitude: -23.02, longitude: -45.56} + # - name: "Viamao" + # coordinates: {latitude: -30.09, longitude: -50.98} + # - name: "Santa Maria" + # coordinates: {latitude: -29.69, longitude: -53.83} + # - name: "Vitoria Da Conquista" + # coordinates: {latitude: -14.85, longitude: -40.84} + # - name: "Varzea Grande" + # coordinates: {latitude: -15.65, longitude: -56.14} + # - name: "Boa Vista" + # coordinates: {latitude: 2.83, longitude: -60.66} + # - name: "Barueri" + # coordinates: {latitude: -23.49, longitude: -46.86} + # - name: "Caruaru" + # coordinates: {latitude: -8.28, longitude: -35.98} + # - name: "Gravatai" + # coordinates: {latitude: -29.95, longitude: -50.99} + # - name: "Ipatinga" + # coordinates: {latitude: -19.48, longitude: -42.52} + # - name: "Praia Grande" + # coordinates: {latitude: -24, longitude: -46.4} + # - name: "Imperatriz" + # coordinates: {latitude: -5.52, longitude: -47.49} + # - name: "Juazeiro Do Norte" + # coordinates: {latitude: -7.21, longitude: -39.32} + # - name: "Embu" + # coordinates: {latitude: -23.64, longitude: -46.84} + # - name: "Sumare" + # coordinates: {latitude: -22.8, longitude: -47.29} + # - name: "Santa Luzia" + # coordinates: {latitude: -19.78, longitude: -43.87} + # - name: "Mage" + # coordinates: {latitude: -22.65, longitude: -43.05} + # - name: "Taboao Da Serra" + # coordinates: {latitude: -23.6, longitude: -46.78} + # - name: "Sao Jose Dos Pinhais" + # coordinates: {latitude: -25.57, longitude: -49.18} + # - name: "Marilia" + # coordinates: {latitude: -22.21, longitude: -49.95} + # - name: "Jacarei" + # coordinates: {latitude: -23.3, longitude: -45.96} + # - name: "Presidente Prudente" + # coordinates: {latitude: -22.12, longitude: -51.39} + # - name: "Sao Leopoldo" + # coordinates: {latitude: -29.78, longitude: -51.15} + # - name: "Mossoro" + # coordinates: {latitude: -5.19, longitude: -37.34} + # - name: "Itabuna" + # coordinates: {latitude: -14.79, longitude: -39.28} + # - name: "Sao Carlos" + # coordinates: {latitude: -22.02, longitude: -47.89} + # - name: "Alvorada" + # coordinates: {latitude: -30.02, longitude: -51.09} + # - name: "Colombo" + # coordinates: {latitude: -25.29, longitude: -49.24} + # - name: "Santarem" + # coordinates: {latitude: -2.43, longitude: -54.72} + # - name: "Sete Lagoas" + # coordinates: {latitude: -19.45, longitude: -44.25} + # - name: "Americana" + # coordinates: {latitude: -22.75, longitude: -47.33} + # - name: "Sao Jose" + # coordinates: {latitude: -27.59, longitude: -48.62} +# \ No newline at end of file diff --git a/uls/fs_db_diff.py b/uls/fs_db_diff.py new file mode 100644 index 0000000..15a9c4e --- /dev/null +++ b/uls/fs_db_diff.py @@ -0,0 +1,747 @@ +#!/usr/bin/env python3 +""" Computes path difference between two FS (ULS) Databases """ + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=wrong-import-order, invalid-name, too-many-statements +# pylint: disable=too-many-branches, too-many-nested-blocks +# pylint: disable=too-many-instance-attributes, too-many-arguments +# pylint: disable=too-few-public-methods, too-many-locals, protected-access + +import argparse +from collections.abc import Iterator +import enum +import logging +import math +import os +import sqlalchemy as sa +import sys +from typing import Any, cast, Dict, List, NamedTuple, Optional, Set, Tuple + + +# START OF DATABASE SCHEMA-DEPENDENT STUFF + +# Name of table with FS Paths +FS_TABLE_NAME = "uls" + +# Name of table with Passive Repeaters +PR_TABLE_NAME = "pr" + +# Name of table with Restricted Areas +RAS_TABLE_NAME = "ras" + + +class PathIdent(NamedTuple): + """ FS Path table columns that globally uniquely identify the path + + Not FSID as it is not necessarily unique across different databases) """ + + region: str + callsign: str + path_number: int + freq_assigned_start_mhz: float + freq_assigned_end_mhz: float + + def __str__(self): + """ String representation for difference report """ + return f"{self.region}:{self.callsign}/{self.path_number}" \ + f"[{self.freq_assigned_start_mhz}-" \ + f"{self.freq_assigned_end_mhz}]MHz" + + +# Fields of FS Path table to NOT include into FS path hash (besides those that +# included into PathIdent) +FS_NO_HASH_FIELDS = ["fsid", "p_rp_num", "name"] + +# Fields of Passive Repeater table to NOT include into FS path hash +PR_NO_HASH_FIELDS = ["id", "fsid", "prSeq"] + +# RAS table fields to NOT include into hash +RAS_NO_HASH_FIELDS = ["rasid"] + +# Coordinate field names of some receiver in some database record +RxCoordFields = NamedTuple("RxCoordFields", [("lat", str), ("lon", str)]) + +# Fields in FS Path table that comprise coordinates of path points that affect +# AFC Engine computations. Specified in (lat, lon) order +FS_COORD_FIELDS = [RxCoordFields("rx_lat_deg", "rx_long_deg")] + +# Fields in Passive Repeater table that comprise coordinates of path points. +# Specified in (lat, lon) order +PR_COORD_FIELDS = [RxCoordFields("pr_lat_deg", "pr_lon_deg")] + +# Primary key field in FS Path table +FS_FSID_FIELD = "fsid" + +# Foreign key field in Passive Repeater table +PR_FSID_FIELD = "fsid" + +# Field in FS Path table that denotes the presence of passive Repeater in the +# path +FS_PR_FIELD = "p_rp_num" + +# Field in Passive Repeater table that sorts Repeater in due order +PR_ORDER_FIELD = "prSeq" + +# END OF DATABASE SCHEMA-DEPENDENT STUFF + + +def error(msg: str) -> None: + """ Prints given msg as error message and exits abnormally """ + logging.error(msg) + sys.exit(1) + + +def error_if(cond: Any, msg: str) -> None: + """ If condition evaluates to true prints given msg as error message and + exits abnormally """ + if cond: + error(msg) + + +class Tile(NamedTuple): + """ 1x1 degree tile containing some receiver """ + # Minimum latitude in north-positive degrees + min_lat: int + + # Minimum longitude in east-positive degrees + min_lon: int + + def __str__(self) -> str: + """ String representation for difference report """ + return f"[{abs(self.min_lat)}-{abs(self.min_lat + 1)}]" \ + f"{'N' if self.min_lat >= 0 else 'S'}, " \ + f"[{abs(self.min_lon)}-{abs(self.min_lon + 1)}]" \ + f"{'E' if self.min_lon >= 0 else 'W'}" + + +class HashFields: + """ Fields of some table that participate in hash computation + + Private attributes: + self._field_infos -- List of _FieldInfo object that specify indices (within + row) and names of fields + """ + # Field descriptor (index in DB row and name) + _FieldInfo = NamedTuple("_FieldInfo", [("name", str), ("idx", int)]) + + def __init__(self, table: sa.Table, exclude: List[str]) -> None: + """ Constructor + + Arguments: + table -- SqlAlchemy table object (contains information about all + columns) + exclude -- List of names of fields not to include + """ + error_if(not all(ef in table.c for ef in exclude), + f"Table '{table.name}' missing these fields: " + f"{', '.join([ef for ef in exclude if ef not in table.c])}") + self._field_infos: List["HashFields._FieldInfo"] = [] + for idx, col in enumerate(table.c): + if col.name not in exclude: + self._field_infos.append( + self._FieldInfo(name=col.name, idx=idx)) + + def get_hash_fields(self, row: "sa.engine.RowProxy") \ + -> "Iterator[Tuple[str, Any]]": + """ Iterator over fields of given row + + Yields (filed_name, field_value) tuples + """ + for fi in self._field_infos: + yield (fi.name, row[fi.idx]) + + +class FsPath: + """ Information about FS Path + + Public attributes: + ident -- Globally unique FS Path identifier + fsid -- Path primary key in DB + + Private attributes: + _row_fields -- Dictionary of FS Path table row fields used in Path Hash + computation. None if fields computation was not requested + _prs -- List of per-repeater dictionaries of fields used in Path + Hash computation. None if fields computation was not + requested + _tiles -- Set of tiles containing receivers. None if its computation + was not requested + _path_hash -- Path hash (hash of pertinent non-identification fields in + the path) if its computation was requested, None otherwise + """ + class _CoordFieldMapping(NamedTuple): + """ Latitude/longitude field names """ + lat: str + lon: str + + def make_tile(self, row: "sa.engine.RowProxy") -> Tile: + """ Makes tile from latitude/longitude fields in given DB row """ + return Tile(min_lat=math.floor(row[self.lat]), + min_lon=math.floor(row[self.lon])) + + def __init__(self, fs_row: "sa.engine.RowProxy", + conn: sa.engine.Connection, + pr_table: sa.Table, fs_hash_fields: HashFields, + pr_hash_fields: HashFields, compute_hash: bool = False, + compute_fields: bool = False, compute_tiles: bool = False) \ + -> None: + """ Constructor + + Arguments: + fs_row -- Row from FS table + conn -- Connection (to use for reading Passive Repeater + table) + pr_table -- Passive Repeater SqlAlchemy table + fs_hash_fields -- Hash fields iterator for FS table + pr_hash_fields -- Hash fields iterator for Passive Repeater table + compute_hash -- True to compute path hash + compute_fields -- True to compute fields' dictionary + compute_tiles -- True to compute receiver tiles + """ + self.ident = PathIdent(**{f: fs_row[f] for f in PathIdent._fields}) + self.fsid = fs_row[FS_FSID_FIELD] + self._row_fields: Optional[Dict[str, Any]] = \ + {} if compute_fields else None + self._prs: Optional[List[Dict[str, Any]]] = \ + [] if compute_fields else None + self._tiles: Optional[Set[Tile]] = set() if compute_tiles else None + hash_fields: Optional[List[Any]] = [] if compute_hash else None + for field_name, field_value in fs_hash_fields.get_hash_fields(fs_row): + if hash_fields is not None: + hash_fields.append(field_value) + if self._row_fields is not None: + self._row_fields[field_name] = field_value + if self._tiles is not None: + self._tiles |= self._tiles_from_record(fs_row, FS_COORD_FIELDS) + if fs_row[FS_PR_FIELD]: + sel = sa.select(pr_table).\ + where(pr_table.c[PR_FSID_FIELD] == self.fsid).\ + order_by(pr_table.c[PR_ORDER_FIELD]) + for pr_row in conn.execute(sel): + pr: Optional[Dict[str, Any]] = {} if self._prs is not None \ + else None + if self._prs is not None: + assert pr is not None + self._prs.append(pr) + for field_name, field_value in \ + pr_hash_fields.get_hash_fields(pr_row): + if hash_fields is not None: + hash_fields.append(field_value) + if pr is not None: + pr[field_name] = field_value + if self._tiles is not None: + self._tiles |= \ + self._tiles_from_record(pr_row, PR_COORD_FIELDS) + self._path_hash: Optional[int] = \ + hash(tuple(hash_fields)) if hash_fields is not None else None + + def path_hash(self) -> int: + """ Returns path hash """ + assert self._path_hash is not None + return self._path_hash + + def tiles(self) -> Set[Tile]: + """ Returns receiver tiles """ + assert self._tiles is not None + return self._tiles + + def diff_report(self, other: "FsPath") -> str: + """ Returns report on difference with other FsPath object """ + assert self._row_fields is not None + assert self._prs is not None + assert other._row_fields is not None + assert other._prs is not None + diffs: List[str] = [] + for field, this_value in self._row_fields.items(): + other_value = other._row_fields[field] + if this_value != other_value: + diffs.append(f"'{field}': '{this_value}' vs '{other_value}'") + if len(self._prs) != len(other._prs): + diffs.append(f"Different number of repeaters: " + f"{len(self._prs)} vs {len(other._prs)}") + else: + for pr_idx, this_pr_dict in enumerate(self._prs): + other_pr_dict = other._prs[pr_idx] + for field, this_value in this_pr_dict.items(): + other_value = other_pr_dict[field] + if this_value != other_value: + diffs.append(f"Repeater #{pr_idx + 1}, '{field}': " + f"'{this_value}' vs '{other_value}'") + return ", ".join(diffs) + + def _tiles_from_record(self, row: "sa.engine.RowProxy", + rx_fields: List[RxCoordFields]) -> Set[Tile]: + """ Make set of tiles according to given list of receiver coordinates + """ + ret: Set[Tile] = set() + for rf in rx_fields: + ret.add(Tile(min_lat=math.floor(row[rf.lat]), + min_lon=math.floor(row[rf.lon]))) + return ret + + @classmethod + def compute_hash_fields(cls, fs_table: sa.Table, pr_table: sa.Table) \ + -> Tuple[HashFields, HashFields]: + """ Computes hash fields' iterators + + Arguments: + fs_table -- SqlAlchemy table object for FS + pr_table -- SqlAlchemy table object Passive Repeater + Returns (FS_iterator, PR_Iterator) tuple + """ + missing_ident_fields = \ + [f for f in PathIdent._fields if f not in fs_table.c] + error_if( + missing_ident_fields, + f"Table '{fs_table.name}' does not contain following path " + f"identification fields: {', '.join(missing_ident_fields)}") + return \ + (HashFields( + table=fs_table, + exclude=list(PathIdent._fields) + FS_NO_HASH_FIELDS), + HashFields(table=pr_table, exclude=PR_NO_HASH_FIELDS)) + + +class Ras: + """ Information about single restricted area: + + Public attributes: + location -- Location name + + Private attributes: + _figure -- Restricted area figure (source of tile sequence) + _hash_fields -- Hash fields (represent restricted area identity) + """ + # Earth radius, same as cconst.cpp CConst::earthRadius + EARTH_RADIUS_KM = 6378.137 + # Maximum imaginable SP AP height above ground in kilometers + MAX_AP_AGL_KM = 1 + + class RasType(enum.Enum): + """ RAS definition types. Values are DB names of these types """ + Rect = "One Rectangle" + TwoRect = "Two Rectangles" + Circle = "Circle" + Horizon = "Horizon Distance" + + class Point: + """ Geodetic point (latitude and longitude) """ + + def __init__(self, lat: float, lon: float) -> None: + if not isinstance(lat, float): + raise ValueError(f"Latitude {lat} has incorrect type") + if not isinstance(lon, float): + raise ValueError(f"Longitude {lon} has incorrect type") + self.lat = lat + self.lon = lon + + class FigureBase: + """ Abstract base class for restricted area figure """ + + def tiles(self) -> List[Tile]: + """ Returns list of 1 degree tiles covering figure """ + raise NotImplementedError( + f"{self.__class__}.tiles() not implemented") + + @classmethod + def rect_tiles(cls, min_pt: "Ras.Point", max_pt: "Ras.Point") \ + -> List[Tile]: + """ Returns list of 1 degree tiles covering given geodetic + rectangle + + Arguments: + min_pt -- Left bottom point + max_pt -- Right top point + Returns sequence of 1 degree tiles + """ + ret: List[Tile] = [] + for min_lat in range(math.floor(min_pt.lat), + math.ceil(max_pt.lat)): + for min_lon in range(math.floor(min_pt.lon), + math.ceil(max_pt.lon)): + adjusted_min_lon = min_lon + if adjusted_min_lon < -180: + adjusted_min_lon += 360 + elif adjusted_min_lon >= 180: + adjusted_min_lon -= 360 + ret.append(Tile(min_lat=min_lat, min_lon=adjusted_min_lon)) + return ret + + class Rect(FigureBase): + """ Rectangular RAS + + Private attributes: + _min -- Left bottom point + _max -- Right top point + """ + + def __init__(self, pt: "Ras.Point") -> None: + """ Constructor, constructs from one of corners """ + self._min = pt + self._max = pt + + def extend(self, pt: "Ras.Point") -> None: + """ Extending rectangle with corner point """ + other_lon = pt.lon + center_lon = (self._min.lon + self._max.lon) / 2 + if (other_lon - center_lon) > 180: + other_lon -= 360 + elif (other_lon - center_lon) < -180: + other_lon += 360 + self._min = Ras.Point(lat=min(pt.lat, self._min.lat), + lon=min(other_lon, self._min.lon)) + self._max = Ras.Point(lat=max(pt.lat, self._max.lat), + lon=max(other_lon, self._max.lon)) + + def tiles(self) -> List[Tile]: + """ Returns sequence of covering 1 degree tiles """ + return self.rect_tiles(min_pt=self._min, max_pt=self._max) + + class Circle(FigureBase): + """ Round restricted area + + Private attributes: + _center -- Center geodetic point + _radius_km -- Radius in kilometers + """ + + def __init__(self, center: "Ras.Point") -> None: + """ Construct by center """ + self._center = center + self._radius_km: float = 0 + + def set_radius(self, radius_km: float) -> None: + """ Sets Radius in kilometers """ + if not isinstance(radius_km, float): + raise ValueError(f"Invalid circle radius: {radius_km}") + self._radius_km = radius_km + + def set_horizon_radius(self, ant_agl_m: float) -> None: + """ Sets horizon radius by specifying central antenna height above + ground in meters """ + if not isinstance(ant_agl_m, float): + raise ValueError(f"Invalid antenna elevation: {ant_agl_m}") + self._radius_km = math.sqrt(2 * Ras.EARTH_RADIUS_KM * 4 / 3) * \ + (math.sqrt(ant_agl_m / 1000) + math.sqrt(Ras.MAX_AP_AGL_KM)) + + def tiles(self) -> List[Tile]: + """ Returns sequence of covering 1 degree tiles """ + radian_radius = self._radius_km / Ras.EARTH_RADIUS_KM + lat_radius_deg = math.degrees(radian_radius) + lon_radius_deg = \ + math.degrees(radian_radius / + math.cos(math.radians(self._center.lat))) + return \ + self.rect_tiles( + min_pt=Ras.Point(lat=self._center.lat - lat_radius_deg, + lon=self._center.lon - lon_radius_deg), + max_pt=Ras.Point(lat=self._center.lat + lat_radius_deg, + lon=self._center.lon + lon_radius_deg)) + + def __init__(self, fs_row: "sa.engine.RowProxy", hash_fields: HashFields) \ + -> None: + """ Constructor + + Arguments: + fs_row -- Database row to construct from + hash_fields -- Retriever of identity fields + """ + try: + self.location = \ + f"{fs_row.region}: {fs_row.name} @ {fs_row.location}" + ras_type = [rt for rt in self.RasType + if rt.value == fs_row.exclusionZone][0] + self._figure: "Ras.FigureBase" + if ras_type in (self.RasType.Rect, self.RasType.TwoRect): + rect = self.Rect(self.Point(lat=fs_row.rect1lat1, + lon=fs_row.rect1lon1)) + rect.extend(self.Point(lat=fs_row.rect1lat2, + lon=fs_row.rect1lon2)) + if ras_type == self.RasType.TwoRect: + rect.extend(self.Point(lat=fs_row.rect2lat1, + lon=fs_row.rect2lon1)) + rect.extend(self.Point(lat=fs_row.rect2lat2, + lon=fs_row.rect2lon2)) + self._figure = rect + elif ras_type in (self.RasType.Circle, self.RasType.Horizon): + circle = self.Circle(self.Point(lat=fs_row.centerLat, + lon=fs_row.centerLon)) + if ras_type == self.RasType.Circle: + circle.set_radius(fs_row.radiusKm) + else: + assert ras_type == self.RasType.Horizon + circle.set_horizon_radius(ant_agl_m=fs_row.heightAGL) + self._figure = circle + else: + raise ValueError(f"Unsupported RAS type: {ras_type.name}") + except (LookupError, ValueError, sa.exc.SQLAlchemyError) as ex: + error( + f"RAS record '{fs_row}' has unsupported structure: {repr(ex)}") + self._hash_fields = \ + tuple(v for _, v in hash_fields.get_hash_fields(fs_row)) + + def tiles(self) -> List[Tile]: + """ Returns sequence of covering 1 degree tiles """ + return self._figure.tiles() + + def __eq__(self, other: Any) -> bool: + """ Equality comparator """ + return isinstance(other, self.__class__) and \ + (self._hash_fields == other._hash_fields) + + def __hash__(self) -> int: + """ Hash computer """ + return hash(self._hash_fields) + + +class Db: + """ Database access (most part of it) + + Public attributes + filename -- SQLite database filename + conn -- Database connection + fs_table -- SqlAlchemy table object for FS + pr_table -- SqlAlchemy table object for Passive Repeater + ras_table -- Optional SqlAlchemy table object for RAS + fs_hash_fields -- Iterator over hash fields in FS record + pr_hash_fields -- Iterator over hash fields in Passive Repeater record + ras_hash_fields -- Optional iterator over hash fields in RAS record + """ + + def __init__(self, filename: str) -> None: + """ Constructor + + Arguments: + filename -- SQLite database file + """ + error_if(not os.path.isfile(filename), + f"FS SQLite database '{filename}' not found") + self.filename = filename + try: + self._engine = sa.create_engine("sqlite:///" + self.filename) + self._metadata = sa.MetaData() + self._metadata.reflect(bind=self._engine) + self.conn = self._engine.connect() + except sa.exc.SQLAlchemyError as ex: + error(f"Error opening FS database '{self.filename}': {ex}") + error_if(FS_TABLE_NAME not in self._metadata.tables, + f"'{self.filename}' does not contain table '{FS_TABLE_NAME}'") + self.fs_table = self._metadata.tables[FS_TABLE_NAME] + error_if(PR_TABLE_NAME not in self._metadata.tables, + f"'{self.filename}' does not contain table '{PR_TABLE_NAME}'") + self.pr_table = self._metadata.tables[PR_TABLE_NAME] + self.ras_table = self._metadata.tables.get(RAS_TABLE_NAME) + self.fs_hash_fields, self.pr_hash_fields = \ + FsPath.compute_hash_fields(fs_table=self.fs_table, + pr_table=self.pr_table) + self.ras_hash_fields = \ + None if self.ras_table is None \ + else HashFields(table=self.ras_table, exclude=RAS_NO_HASH_FIELDS) + + def close(self): + """ Close connection """ + self.conn.close() + + def all_fs_rows(self) -> "Iterator[sa.engine.RowProxy]": + """ Iterator that reads all FS Path rows """ + try: + for fs_row in self.conn.execute(sa.select(self.fs_table)).\ + fetchall(): + yield fs_row + except sa.exc.SQLAlchemyError as ex: + error(f"Error reading '{self.fs_table.name}' table from FS " + f"database '{self.filename}': {ex}") + + def fs_row_by_fsid(self, fsid: int) -> "sa.engine.RowProxy": + """ Fetch FS row by FSID """ + try: + ret = \ + self.conn.execute( + sa.select(self.fs_table). + where(self.fs_table.c[FS_FSID_FIELD] == fsid)).first() + error_if( + ret is None, + f"Record with FSID of {fsid} not found in table " + f"'{self.fs_table.name}' of FS database '{self.filename}'") + assert ret is not None + return ret + except sa.exc.SQLAlchemyError as ex: + error(f"Error reading '{self.fs_table.name}' table from FS " + f"database '{self.filename}': {ex}") + raise # Will never happen, just to pacify pylint + + def all_ras_rows(self) -> "Iterator[sa.engine.RowProxy]": + """ Iterator that reads all RAS rows """ + if self.ras_table is None: + return + try: + for fs_row in self.conn.execute(sa.select(self.ras_table)).\ + fetchall(): + yield fs_row + except sa.exc.SQLAlchemyError as ex: + error(f"Error reading '{self.ras_table.name}' table from FS " + f"database '{self.filename}': {ex}") + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + argument_parser = \ + argparse.ArgumentParser( + description="Computes path difference between two FS (aka ULS) " + "Databases") + argument_parser.add_argument( + "--report_tiles", action="store_true", + help="Print 1x1 degree tiles affected by difference") + argument_parser.add_argument( + "--report_paths", action="store_true", + help="Print paths that constitute difference") + argument_parser.add_argument( + "DB1", + help="First FS (aka ULS) sqlite3 database file to compare") + argument_parser.add_argument( + "DB2", + help="Second FS (aka ULS) sqlite3 database file to compare") + + if not argv: + argument_parser.print_help() + sys.exit(1) + + args = argument_parser.parse_args(argv) + + console_handler = logging.StreamHandler() + console_handler.setFormatter( + logging.Formatter( + f"{os.path.basename(__file__)}. " + f"%(levelname)s: %(asctime)s %(message)s")) + logging.getLogger().addHandler(console_handler) + logging.getLogger().setLevel(logging.INFO) + + dbs: List[Db] = [] + try: + dbs.append(Db(args.DB1)) + dbs.append(Db(args.DB2)) + + IdentHash = NamedTuple("IdentHash", [("ident", PathIdent), + ("hash", int)]) + IdentHashToFsid = Dict[IdentHash, int] + + # Per-database dictionaries that map (FS Ident, FS Hash) pairs to FSIDs + # Building them... + ident_hash_to_fsid_by_db: List[IdentHashToFsid] = [] + for db in dbs: + ident_hash_to_fsid: IdentHashToFsid = {} + ident_hash_to_fsid_by_db.append(ident_hash_to_fsid) + try: + for fs_row in db.all_fs_rows(): + fs_path = \ + FsPath( + fs_row=fs_row, conn=db.conn, pr_table=db.pr_table, + fs_hash_fields=db.fs_hash_fields, + pr_hash_fields=db.pr_hash_fields, + compute_hash=True) + ident_hash_to_fsid[ + IdentHash(ident=fs_path.ident, + hash=cast(int, fs_path.path_hash()))] = \ + fs_path.fsid + except sa.exc.SQLAlchemyError as ex: + error(f"Error reading FS database '{db.filename}': {ex}") + + # Idents of FS Paths with differences mapped to FSID dictionaries + # (dictionaries indexed by DB index (0 or 1) to correspondent FSIDs). + # Building it + ident_to_fsids_diff: Dict[PathIdent, Dict[int, int]] = {} + # Loop by unique (FS Ident, FS Hash) pairs + for unique_ident_hash in (set(ident_hash_to_fsid_by_db[0].keys()) ^ + set(ident_hash_to_fsid_by_db[1].keys())): + db_idx = 0 if unique_ident_hash in ident_hash_to_fsid_by_db[0] \ + else 1 + ident_to_fsids_diff.setdefault(unique_ident_hash.ident, + {})[db_idx] = \ + ident_hash_to_fsid_by_db[db_idx][unique_ident_hash] + + # Reading RAS tables from databases + ras_sets: List[Set[Ras]] = [] + for db in dbs: + ras_sets.append(set()) + for fs_row in db.all_ras_rows(): + assert db.ras_hash_fields is not None + ras_sets[-1].add(Ras(fs_row=fs_row, + hash_fields=db.ras_hash_fields)) + ras_difference = ras_sets[0] ^ ras_sets[1] + + # Making brief report + print(f"Paths in DB1: {len(ident_hash_to_fsid_by_db[0])}") + print(f"Paths in DB2: {len(ident_hash_to_fsid_by_db[1])}") + print(f"Different paths: {len(ident_to_fsids_diff)}") + print(f"Different RAS entries: {len(ras_difference)}") + + # Detailed reports + if args.report_paths or args.report_tiles: + # RX tiles belonging to unique paths + tiles_of_difference: Set[Tile] = set() + # Loop by unique FS identifiers + for path_ident in sorted(ident_to_fsids_diff.keys()): + # Per-database FsPoath objects + paths: List[Optional[FsPath]] = [] + # Filling it (and adding tiles) + for db_idx, db in enumerate(dbs): + fsid = ident_to_fsids_diff[path_ident].get(db_idx) + if fsid is None: + paths.append(None) + continue + try: + fs_row = db.fs_row_by_fsid(fsid) + fs_path = \ + FsPath( + fs_row=fs_row, conn=db.conn, + pr_table=db.pr_table, + fs_hash_fields=db.fs_hash_fields, + pr_hash_fields=db.pr_hash_fields, + compute_fields=args.report_paths, + compute_tiles=args.report_tiles) + except sa.exc.SQLAlchemyError as ex: + error( + f"Error reading FS database '{db.filename}': {ex}") + if args.report_tiles: + tiles_of_difference |= fs_path.tiles() + paths.append(fs_path) + if args.report_paths: + if paths[0] is not None: + if paths[1] is not None: + diff_report = paths[0].diff_report(paths[1]) + else: + diff_report = "Only present in DB1" + else: + assert paths[1] is not None + diff_report = "Only present in DB2" + print(f"Difference in path {path_ident}: {diff_report}") + if args.report_paths: + for db_idx, ras_set in enumerate(ras_sets): + for ras in (ras_set - ras_sets[1 - db_idx]): + print(f"RAS '{ras.location}' from DB{db_idx + 1} not " + f"present in or different from " + f"DB{1 - db_idx + 1}") + if args.report_tiles: + for ras in ras_difference: + tiles_of_difference |= set(ras.tiles()) + for tile in sorted(tiles_of_difference): + print(f"Difference in tile {tile}") + except KeyboardInterrupt: + pass + finally: + for db in dbs: + if db: + db.close() + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/uls/fsid_tool.py b/uls/fsid_tool.py new file mode 100755 index 0000000..97933f9 --- /dev/null +++ b/uls/fsid_tool.py @@ -0,0 +1,512 @@ +#!/usr/bin/env python3 +# Embedding/extracting FSID table to/from FS SQLite database + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=wrong-import-order, invalid-name +# pylint: disable=too-many-instance-attributes + +import argparse +from collections.abc import Iterator, Sequence +import csv +import os +import sqlalchemy as sa +import sys +from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Union + +# Name of table for FSID in FS Database +FSID_TABLE_NAME = "fsid_history" + + +def warning(warnmsg: str) -> None: + """ Print given warning message """ + print(f"{os.path.basename(__file__)}: Warning: {warnmsg}", file=sys.stderr) + + +def error(errmsg: str) -> None: + """ Print given error message and exit """ + print(f"{os.path.basename(__file__)}: Error: {errmsg}", file=sys.stderr) + sys.exit(1) + + +def error_if(condition: Any, errmsg: str) -> None: + """ If given condition met - print given error message and exit """ + if condition: + error(errmsg) + + +class Record: + """ Stuff about and around of records in FSID table + + Private attributes: + _fields -- FSID record as dictionary, ordered by database field names (i.e. + a database row dictionary). Absent fields not included + """ + class SchemaCheckResult(NamedTuple): + """ Results of database or CSV schema check """ + # Fields in csv/database not known to script + extra_fields: List[str] + + # Optional fields known to script not found in csv/database + missing_optional_fields: List[str] + + # Required fields known to script not found in csv/database + missing_required_fields: List[str] + + def is_ok(self) -> bool: + """ True if fields in csv/database are the same as in script """ + return not (self.extra_fields or self.missing_optional_fields or + self.missing_required_fields) + + def is_fatal(self) -> bool: + """ True if csv/database lacks some mandatory fields + """ + return not self.missing_required_fields + + def errmsg(self) -> str: + """ Error message on field nomenclature mismatch. Empty of all OK + """ + ret: List[str] = [] + for fields, who, what in \ + [(self.missing_required_fields, "required", "missing"), + (self.missing_optional_fields, "optional", "missing"), + (self.extra_fields, "unknown", "present")]: + if fields: + ret.append(f"Following {who} fields are {what}: " + f"{', '.join(fields)}") + return ". ".join(ret) + + # Database dictionary row data type + RecordDataType = Mapping[str, Optional[Union[str, int, float]]] + + # Dictionary stored in this class data type + StoredDataType = Dict[str, Union[str, int, float]] + + # Field descriptor + FieldDsc = NamedTuple("FieldDsc", [("csv_name", str), + ("column", sa.Column)]) + + # Fields of FSID table + _FIELDS = [ + FieldDsc("FSID", + sa.Column("fsid", sa.Integer(), primary_key=True)), + FieldDsc("Region", + sa.Column("region", sa.String(10), nullable=True)), + FieldDsc("Callsign", + sa.Column("callsign", sa.String(16), nullable=False)), + FieldDsc("Path Number", + sa.Column("path_number", sa.Integer(), nullable=False)), + FieldDsc("Center Frequency (MHz)", + sa.Column("center_freq_mhz", sa.Float(), nullable=False)), + FieldDsc("Bandwidth (MHz)", + sa.Column("bandwidth_mhz", sa.Float(), nullable=False))] + + # Index of field descriptors by CSV names + _BY_CSV_NAME = {fd.csv_name: fd for fd in _FIELDS} + + # Index of field descriptors by DB names + _BY_COLUMN = {fd.column.name: fd for fd in _FIELDS} + + def __init__(self, data_dict: "Record.RecordDataType", is_csv: bool) \ + -> None: + """ Constructor + + Arguments: + data_dict -- Row data dictionary + is_csv -- True if data_dict is from CSV (field names from heading + row, values all strings), False if from DB (field names + are column names, values are of proper types) + """ + self._fields: "Record.StoredDataType" + if is_csv: + self._fields = {} + for csv_name, value in data_dict.items(): + fd = self._BY_CSV_NAME.get(csv_name) + if (fd is None) or (value is None) or (value == ""): + continue + field_name = fd.column.name + try: + if isinstance(fd.column.type, sa.String): + self._fields[field_name] = value + elif isinstance(fd.column.type, sa.Integer): + self._fields[field_name] = int(value) + elif isinstance(fd.column.type, sa.Float): + self._fields[field_name] = float(value) + else: + assert not (f"Internal error: data type " + f"{fd.column.type} not supported by " + f"script") + except ValueError as ex: + error(f"Value '{value}' not valid for field of type " + f"'{fd.column.type}': {ex}") + else: + self._fields = \ + {name: value for name, value in data_dict.items() + if (name in self._BY_COLUMN) and (value is not None)} + + def db_dict(self) -> "Record.StoredDataType": + """ Row value as dictionary for inserting to DB """ + return self._fields + + def csv_list(self) -> List[str]: + """ Row value as list of strings for writing to CSV """ + ret: List[str] = [] + for fd in self._FIELDS: + value = self._fields.get(fd.column.name) + ret.append("" if value is None else str(value)) + return ret + + @classmethod + def csv_heading(cls) -> List[str]: + """ List of CSV headings """ + return [fd.csv_name for fd in cls._FIELDS] + + @classmethod + def db_columns(cls) -> List[sa.Column]: + """ List of DB column descriptors """ + return [fd.column for fd in cls._FIELDS] + + @classmethod + def check_table(cls, table_fields: "Sequence[str]") \ + -> "Record.SchemaCheckResult": + """ Check database table columns schema in this class """ + return cls.SchemaCheckResult( + extra_fields=[field for field in table_fields + if field not in cls._BY_COLUMN], + missing_optional_fields=[fd.column.name for fd in cls._FIELDS + if fd.column.nullable and + (fd.column.name not in table_fields)], + missing_required_fields=[fd.column.name for fd in cls._FIELDS + if (not fd.column.nullable) and + (fd.column.name not in table_fields)]) + + @classmethod + def check_csv(cls, csv_headings: List[str]) -> "Record.SchemaCheckResult": + """ Check CSV headings against schema in this class """ + return cls.SchemaCheckResult( + extra_fields=[cn for cn in csv_headings + if cn not in cls._BY_CSV_NAME], + missing_optional_fields=[fd.csv_name for fd in cls._FIELDS + if fd.column.nullable and + (fd.csv_name not in csv_headings)], + missing_required_fields=[fd.column.name for fd in cls._FIELDS + if (not fd.column.nullable) and + (fd.csv_name not in csv_headings)]) + + @classmethod + def transaction_length(cls, legacy: bool = False) -> int: + """ Maximum length of SqlAlchemy transaction. + + Frankly, I do not remember where from I got these two constants, and + this is not that easy to google """ + return (999 if legacy else 32000) // len(cls._FIELDS) + + @classmethod + def num_fields(cls) -> int: + """ Number of fields in record """ + return len(cls._FIELDS) + + +class Db: + """ Handles all database-related stuff + + Should be used as a context manager (with 'with') + + Private attributes: + _engine -- SqlAlchemy engine + _conn -- SqlAlchemy connection + _metadata -- SqlAlchemy metadata + _fsid_table -- SqlAlchemy table for FSID table, None if + it is not in the table + _bulk -- List of record dictionaries for subsequent + bulk insert + _use_legacy_transaction_limit -- True to use legacy transaction length, + False for use more recent one + _current_transaction_length -- Currently used maximum transaction length + """ + + def __init__(self, database_file: str, + recreate_fsid_table: bool = False) -> None: + """ Constructor + + Arguments: + database_file -- SQLite database file + recreate_fsid_table -- True to create FSID table anew + """ + error_if(not os.path.isfile(database_file), + f"FS SQLite database '{database_file}' not found") + self._database_file = database_file + self._conn: sa.engine.Connection = None + try: + self._engine = \ + sa.create_engine("sqlite:///" + self._database_file) + self._metadata = sa.MetaData() + self._metadata.reflect(bind=self._engine) + self._conn = self._engine.connect() + self._fsid_table: Optional[sa.Table] = \ + self._metadata.tables.get(FSID_TABLE_NAME) + except sa.exc.SQLAlchemyError as ex: + error(f"Error opening FS database '{self._database_file}': {ex}") + if recreate_fsid_table: + try: + if self._fsid_table is not None: + self._metadata.drop_all(self._conn, + tables=[self._fsid_table]) + # Somehow drop_all() leaves table in metadata + self._metadata.remove(self._fsid_table) + self._fsid_table = sa.Table(FSID_TABLE_NAME, self._metadata, + *Record.db_columns()) + self._metadata.create_all(self._engine) + except sa.exc.SQLAlchemyError as ex: + error(f"Error recreating FSID table in FS database " + f"'{self._database_file}': {ex}") + self._bulk: List[Record.RecordDataType] = [] + self._use_legacy_transaction_limit = False + self._current_transaction_length = self._get_transaction_length() + + def fetchall(self) -> "Iterator[Record]": + """ Iterator that reads all records of FSID table """ + try: + for db_row in self._conn.execute(sa.select(self._fsid_table)).\ + fetchall(): + yield Record(data_dict=db_row._mapping, is_csv=False) + except sa.exc.SQLAlchemyError as ex: + error(f"Error reading FSID table from FS database " + f"'{self._database_file}': {ex}") + + def write_row(self, row: Record) -> None: + """ Writes one row to FSID table """ + self._bulk.append(row.db_dict()) + if len(self._bulk) >= self._current_transaction_length: + self._bulk_write() + + def fsid_fields(self) -> Optional[List[str]]: + """ List of FSID table fields, None if table is absent """ + return None if self._fsid_table is None \ + else list(self._fsid_table.c.keys()) + + def _get_transaction_length(self) -> int: + """ Maximum length of SqlAlchemy transaction. + + Frankly, I do not remember where from I got these two constants, and + this is not that easy to google """ + return (999 if self._use_legacy_transaction_limit else 32000) // \ + Record.num_fields() + + def _bulk_write(self) -> None: + """ Do bulk write to database """ + if not self._bulk: + return + transaction: Optional[sa.engine.Transaction] = None + assert self._fsid_table is not None + try: + transaction = self._conn.begin() + ins = \ + sa.insert(self._fsid_table).execution_options(autocommit=False) + self._conn.execute(ins, self._bulk) + transaction.commit() + transaction = None + self._bulk = [] + return + except (sa.exc.CompileError, sa.exc.OperationalError) as ex: + error_if(self._use_legacy_transaction_limit, + f"Error reading FSID table to FS database " + f"'{self._database_file}': {ex}") + finally: + if transaction is not None: + transaction.rollback() + transaction = None + self._use_legacy_transaction_limit = True + self._current_transaction_length = self._get_transaction_length() + try: + for offset in range(0, len(self._bulk), + self._current_transaction_length): + transaction = self._conn.begin() + ins = sa.insert(self._fsid_table).\ + execution_options(autocommit=False) + self._conn.execute( + ins, + self._bulk[offset: + offset + self._current_transaction_length]) + transaction.commit() + transaction = None + self._bulk = [] + return + finally: + if transaction is not None: + transaction.rollback() + transaction = None + + def __enter__(self) -> "Db": + """ Context entry - returns self """ + return self + + def __exit__(self, exc_type: Any, exc_value: Any, exc_tb: Any) -> None: + """ Context exit """ + if self._conn is not None: + if exc_value is None: + self._bulk_write() + self._conn.close() + + +def do_check(args: Any) -> None: + """Execute "extract" command. + + Arguments: + args -- Parsed command line arguments + """ + with Db(args.SQLITE_FILE) as db: + if db.fsid_fields() is None: + print(f"No FSID table in '{args.SQLITE_FILE}'") + sys.exit(1) + + +def do_extract(args: Any) -> None: + """Execute "extract" command. + + Arguments: + args -- Parsed command line arguments + """ + try: + with Db(args.SQLITE_FILE) as db: + fields = db.fsid_fields() + error_if(fields is None, + f"FS database '{args.SQLITE_FILE}' does not contain FSID " + f"history table named '{FSID_TABLE_NAME}'") + assert fields is not None + scr = Record.check_table(fields) + error_if(scr.is_fatal() if args.partial else (not scr.is_ok()), + scr.errmsg()) + if not scr.is_ok(): + warning(scr.errmsg()) + with open(args.FSID_FILE, mode="w", newline="", encoding="utf-8") \ + as f: + csv_writer = csv.writer(f, lineterminator="\n") + csv_writer.writerow(Record.csv_heading()) + for record in db.fetchall(): + csv_writer.writerow(record.csv_list()) + except OSError as ex: + error(f"Error writing FSID table file '{args.FSID_FILE}': {ex}") + + +def do_embed(args: Any) -> None: + """Execute "embed" command. + + Arguments: + args -- Parsed command line arguments + """ + error_if(not os.path.isfile(args.FSID_FILE), + f"FSID table CSV file '{args.FSID_FILE}' not found") + with open(args.FSID_FILE, mode="r", newline="", encoding="utf-8") as f: + headings: List[str] = [] + for csv_row in csv.reader(f): + headings = csv_row + break + scr = Record.check_csv(headings) + error_if(scr.is_fatal() if args.partial else (not scr.is_ok()), + scr.errmsg()) + if not scr.is_ok(): + warning(scr.errmsg()) + try: + with Db(args.SQLITE_FILE, recreate_fsid_table=True) as db: + with open(args.FSID_FILE, mode="r", newline="", encoding="utf-8") \ + as f: + for csv_dict in csv.DictReader(f): + db.write_row(Record(csv_dict, is_csv=True)) + except OSError as ex: + error(f"Error reading FSID table file '{args.FSID_FILE}': {ex}") + + +def do_help(args: Any) -> None: + """Execute "help" command. + + Arguments: + args -- Parsed command line arguments (also contains 'argument_parser' and + 'subparsers' fields) + """ + if args.subcommand is None: + args.argument_parser.print_help() + else: + args.subparsers.choices[args.subcommand].print_help() + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + # Switches for database file + switches_database = argparse.ArgumentParser(add_help=False) + switches_database.add_argument( + "SQLITE_FILE", + help="SQLite file containing FS (aka ULS) database") + + # Switches for FSID CSV file + switches_fsid = argparse.ArgumentParser(add_help=False) + switches_fsid.add_argument( + "FSID_FILE", + help="CSV file containing FSID table") + + # Top level parser + argument_parser = argparse.ArgumentParser( + description="Embedding/extracting FSID table to/from FS (aka ULS) " + "SQLite database") + + subparsers = argument_parser.add_subparsers(dest="subcommand", + metavar="SUBCOMMAND") + + # Subparser for 'check' command + parser_check = subparsers.add_parser( + "check", parents=[switches_database], + help="Exits normally if database contains FSID table (of whatever " + "scheme), with error otherwise") + parser_check.set_defaults(func=do_check) + + # Subparser for 'extract' command + parser_extract = subparsers.add_parser( + "extract", parents=[switches_database, switches_fsid], + help="Extract FSID table from FS (aka ULS) Database") + parser_extract.add_argument( + "--partial", action="store_true", + help="Ignore unknown database column names, fill with empty strings " + "missing columns. By default database columns must match ones, known " + "to the script") + parser_extract.set_defaults(func=do_extract) + + # Subparser for 'embed' command + parser_embed = subparsers.add_parser( + "embed", parents=[switches_database, switches_fsid], + help="Embed FSID table from FS (aka ULS) Database") + parser_embed.add_argument( + "--partial", action="store_true", + help="Ignore unknown CSV column names, fill missing columns with " + "NULL. By default CSV columns must match ones, known to the script") + parser_embed.set_defaults(func=do_embed) + + # Subparser for 'help' command + parser_help = subparsers.add_parser( + "help", add_help=False, + help="Prints help on given subcommand") + parser_help.add_argument( + "subcommand", metavar="SUBCOMMAND", nargs="?", + choices=subparsers.choices, + help="Name of subcommand to print help about (use " + + "\"%(prog)s --help\" to get list of all subcommands)") + parser_help.set_defaults(func=do_help, subparsers=subparsers, + argument_parser=argument_parser) + + if not argv: + argument_parser.print_help() + sys.exit(1) + + args = argument_parser.parse_args(argv) + args.func(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/uls/requirements.txt b/uls/requirements.txt new file mode 100644 index 0000000..b7dff45 --- /dev/null +++ b/uls/requirements.txt @@ -0,0 +1,8 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +prometheus-client==0.17.1 +python-statsd==2.1.0 diff --git a/uls/uls-docker-entrypoint.sh b/uls/uls-docker-entrypoint.sh new file mode 100644 index 0000000..c964294 --- /dev/null +++ b/uls/uls-docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# +# Copyright (C) 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} +case "$AFC_DEVEL_ENV" in + "devel") + echo "Running debug profile" + apk add --update --no-cache bash + ;; + "production") + echo "Running production profile" + ;; + *) + echo "Uknown profile" + ;; +esac + +AFC_ULS_OUTPUT=/output_folder +cd /mnt/nfs/rat_transfer/daily_uls_parse +python3 daily_uls_parse.py && +echo "Copy results to $AFC_ULS_OUTPUT" && +cp /mnt/nfs/rat_transfer/ULS_Database/*.sqlite3 $AFC_ULS_OUTPUT/ diff --git a/uls/uls_service.py b/uls/uls_service.py new file mode 100755 index 0000000..2ab0ca7 --- /dev/null +++ b/uls/uls_service.py @@ -0,0 +1,1260 @@ +#!/usr/bin/env python3 +""" ULS data download control service """ + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=unused-wildcard-import, wrong-import-order, wildcard-import +# pylint: disable=too-many-statements, too-many-branches, unnecessary-pass +# pylint: disable=logging-fstring-interpolation, invalid-name, too-many-locals +# pylint: disable=too-few-public-methods, too-many-arguments +# pylint: disable=too-many-nested-blocks, too-many-lines +# pylint: disable=too-many-instance-attributes + +import argparse +import datetime +import glob +import logging +import os +import prometheus_client +import pydantic +import re +import shlex +import shutil +import signal +import sqlalchemy as sa +import statsd +import subprocess +import sys +import tempfile +import threading +import time +from typing import cast, Dict, Iterable, List, NamedTuple, Optional, Tuple, \ + Union +import urllib.error +import urllib.request + +from rcache_client import RcacheClient +from rcache_models import LatLonRect, RcacheClientSettings +from uls_service_common import * +from uls_service_state_db import CheckType, DownloaderMilestone, LogType, \ + safe_dsn, StateDb + +# Filemask for ULS databases +ULS_FILEMASK = "*.sqlite3" + +# ULS database identity table +DATA_IDS_TABLE = "data_ids" + +# Identity table region column +DATA_IDS_REG_COLUMN = "region" + +# Identity table region ID column +DATA_IDS_ID_COLUMN = "identity" + +# Name of FSID tool script +FSID_TOOL = os.path.join(os.path.dirname(__file__), "fsid_tool.py") + +# Name of FS DB Diff script +FS_DB_DIFF = os.path.join(os.path.dirname(__file__), "fs_db_diff.py") + +# Name of FS AFC test script +FS_AFC = os.path.join(os.path.dirname(__file__), "fs_afc.py") + +# Healthcheck script +HEALTHCHECK_SCRIPT = os.path.join(os.path.dirname(__file__), + "uls_service_healthcheck.py") + +# Default StatsD port +DEFAULT_STATSD_PORT = 8125 + + +class Settings(pydantic.BaseSettings): + """ Arguments from command lines - with their default values """ + download_script: str = \ + pydantic.Field( + "/mnt/nfs/rat_transfer/daily_uls_parse/daily_uls_parse.py", + env="ULS_DOWNLOAD_SCRIPT", + description="FS download script") + download_script_args: Optional[str] = \ + pydantic.Field(None, env="ULS_DOWNLOAD_SCRIPT_ARGS", + description="Additional download script parameters") + region: Optional[str] = \ + pydantic.Field(None, env="ULS_DOWNLOAD_REGION", + description="Download regions", no="All") + result_dir: str = \ + pydantic.Field( + "/mnt/nfs/rat_transfer/ULS_Database/", env="ULS_RESULT_DIR", + description="Directory where download script puts downloaded file") + temp_dir: str = \ + pydantic.Field( + "/mnt/nfs/rat_transfer/daily_uls_parse/temp/", env="ULS_TEMP_DIR", + description="Temporary directory of ULS download script, cleaned " + "before downloading") + ext_db_dir: str = \ + pydantic.Field( + ..., env="ULS_EXT_DB_DIR", + description="Ultimate downloaded file destination directory") + ext_db_symlink: str = \ + pydantic.Field(..., env="ULS_CURRENT_DB_SYMLINK", + description="Symlink pointing to current ULS file") + fsid_file: str = \ + pydantic.Field( + "/mnt/nfs/rat_transfer/daily_uls_parse/data_files/fsid_table.csv", + env="ULS_FSID_FILE", + description="FSID file location expected by ULS download script") + ext_ras_database: str = \ + pydantic.Field(..., env="ULS_EXT_RAS_DATABASE", + description="RAS database") + ras_database: str = \ + pydantic.Field( + "/mnt/nfs/rat_transfer/daily_uls_parse/data_files/RASdatabase.dat", + env="ULS_RAS_DATABASE", + description="Where from ULS script reads RAS database") + service_state_db_dsn: str = \ + pydantic.Field( + ..., env="ULS_SERVICE_STATE_DB_DSN", + description="Connection string to service state database", + convert=safe_dsn) + service_state_db_create_if_absent: bool = \ + pydantic.Field( + True, env="ULS_SERVICE_STATE_DB_CREATE_IF_ABSENT", + description="Create service state database if it is absent") + service_state_db_recreate: bool = \ + pydantic.Field( + False, env="ULS_SERVICE_STATE_DB_RECREATE", + description="Recreate service state database if it exists") + prometheus_port: Optional[int] = \ + pydantic.Field(None, env="ULS_PROMETHEUS_PORT", + description="Port to serve Prometheus metrics on") + statsd_server: Optional[str] = \ + pydantic.Field(None, env="ULS_STATSD_SERVER", + description="StatsD server to send metrics to") + check_ext_files: Optional[List[str]] = \ + pydantic.Field( + "https://raw.githubusercontent.com/Wireless-Innovation-Forum/" + "6-GHz-AFC/main/data/common_data" + ":raw_wireless_innovation_forum_files" + ":antenna_model_diameter_gain.csv,billboard_reflector.csv," + "category_b1_antennas.csv,high_performance_antennas.csv," + "fcc_fixed_service_channelization.csv," + "transmit_radio_unit_architecture.csv", + env="ULS_CHECK_EXT_FILES", + description="Verify that that files are the same as in internet", + no="None") + max_change_percent: Optional[float] = \ + pydantic.Field( + 10., env="ULS_MAX_CHANGE_PERCENT", + description="Limit on number of paths changed", + convert=lambda v: f"{v}%" if v else "Don't check") + afc_url: Optional[str] = \ + pydantic.Field( + None, env="ULS_AFC_URL", + description="AFC Service URL to use for database validity check", + no="Don't check") + afc_parallel: Optional[int] = \ + pydantic.Field( + None, env="ULS_AFC_PARALLEL", + description="Number of parallel AFC Requests to use when doing " + "validity check", no="fs_afc.py's default") + rcache_url: Optional[str] = \ + pydantic.Field(None, env="RCACHE_SERVICE_URL", + description="Rcache service url", + no="Don't do spatial invalidation") + rcache_enabled: bool = \ + pydantic.Field(True, env="RCACHE_ENABLED", + description="Rcache spatial invalidation", + yes="Enabled", no="Disabled") + delay_hr: float = \ + pydantic.Field(0., env="ULS_DELAY_HR", + description="Hours to delay first download by") + interval_hr: float = \ + pydantic.Field(4, env="ULS_INTERVAL_HR", + description="Download interval in hours") + timeout_hr: float = \ + pydantic.Field(1, env="ULS_TIMEOUT_HR", + description="Download maximum duration in hours") + nice: bool = \ + pydantic.Field(False, env="ULS_NICE", + description="Run in lowered (nice) priority") + verbose: bool = \ + pydantic.Field(False, description="Print debug info") + run_once: bool = \ + pydantic.Field(False, env="ULS_RUN_ONCE", + description="Run", yes="Once", no="Indefinitely") + force: bool = \ + pydantic.Field(False, + description="Force FS database update (even if not " + "changed or found invalid)") + + @pydantic.validator("check_ext_files", pre=True) + @classmethod + def check_ext_files_str_to_list(cls, v: Any) -> Any: + """ Converts string value of 'check_ext_files' from environment from + string to list (as it is list in argparse) """ + return [v] if v and isinstance(v, str) else v + + @pydantic.validator("statsd_server", pre=False) + @classmethod + def check_statsd_server(cls, v: Any) -> Any: + """ Applies default StatsD port """ + if v: + if ":" not in v: + v = f"{v}:{DEFAULT_STATSD_PORT}" + else: + host, port = v.split(":", 1) + int(port) + assert host + return v + + @pydantic.root_validator(pre=True) + @classmethod + def remove_empty(cls, v: Any) -> Any: + """ Prevalidator that removes empty values (presumably from environment + variables) to force use of defaults + """ + if not isinstance(v, dict): + return v + for key, value in list(v.items()): + if value in (None, "", []): + del v[key] + return v + + +class ProcessingException(Exception): + """ ULS processing exception """ + pass + + +def print_args(settings: Settings) -> None: + """ Print invocation parameters to log """ + logging.info("FS downloader started with the following parameters") + for name, model_field in settings.__fields__.items(): + value = getattr(settings, name) + extra = getattr(model_field.field_info, "extra", {}) + value_repr: str + if "convert" in extra: + value_repr = extra["convert"](value) + elif value and ("yes" in extra): + value_repr = extra["yes"] + elif (not value) and ("no" in extra): + value_repr = extra["no"] + elif model_field.type_ == bool: + value_repr = "Yes" if value else "No" + else: + value_repr = str(value) + logging.info(f"{model_field.field_info.description}: {value_repr}") + + +class LoggingExecutor: + """ Program executor that collects output + + Private attributes: + _lines -- Output lines + """ + + def __init__(self) -> None: + self._lines: List[str] = [] + + def execute(self, cmd: Union[str, List[str]], fail_on_error: bool, + return_output: bool = False, cwd: Optional[str] = None, + timeout_sec: Optional[float] = None) -> \ + Union[bool, Optional[str]]: + """ Execute command + + Arguments: + cmd -- Command as string or list of strings + fail_on_error -- True to raise exception on error, False to return + failure code on error + return_output -- True to return output (None on failure), False to + return boolean success status + cwd -- Directory to execute in or None + timeout_sec -- Timeout in seconds or None + Returns If 'return_output' - returns output/None on success/failure, + otherwise returns boolean success status + """ + ret_lines: Optional[List[str]] = [] if return_output else None + success = True + timed_out = False + + def killer_timer(pgid: int) -> None: + """ Kills given process by process group id (rumors are that + os.kill() only adequate if shell=False) + """ + try: + os.killpg(pgid, signal.SIGTERM) + timed_out = True + except OSError: + pass + + self._lines.append( + "> " + + (cmd if isinstance(cmd, str) + else ''.join(shlex.quote(arg) for arg in cmd)) + + "\n") + try: + with subprocess.Popen(cmd, shell=isinstance(cmd, str), text=True, + encoding="utf-8", stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, bufsize=0, + cwd=cwd) as p: + timer: Optional[threading.Timer] = \ + threading.Timer(timeout_sec, killer_timer, + kwargs={"pgid": os.getpgid(p.pid)}) \ + if timeout_sec is not None else None + assert p.stdout is not None + for line in p.stdout: + print(line, end="", flush=True) + self._lines.append(line) + if ret_lines is not None: + ret_lines.append(line) + p.wait() + if timer is not None: + timer.cancel() + if timed_out: + raise subprocess.TimeoutExpired(cmd, + cast(float, timeout_sec)) + if p.returncode: + raise subprocess.CalledProcessError(p.returncode, p.args) + except (OSError, subprocess.SubprocessError) as ex: + success = False + self._lines.append(f"{ex}\n") + if fail_on_error: + raise + if return_output: + assert ret_lines is not None + return "".join(ret_lines) if success else None + return success + + def get_output(self) -> str: + """ Returns and resets accumulated output """ + ret = "".join(self._lines) + self._lines = [] + return ret + + +class StatusUpdater: + """ Updater of State DB and Prometheus/StatsD metrics + + Private attributes: + _state_db -- StateDb object to update + _prometheus_metrics -- By-milestone dictionary of region-inspecific + Prometheus metrics. Metrics are gauges, + containing seconds since epoch of recent + milestone occurrence. Empty if Prometheus + metrics are not served + _prometheus_region_metrics -- By-milestone dictionary of region-specific + Prometheus metrics. Metrics are gauges, + containing seconds since epoch of recent + milestone occurrence. Each metric has + 'region' label. Empty if Prometheus metrics\ + are not served + _prometheus_check_metric -- None or Prometheus gauge, with 'check_type' + label (containing name of CheckType item), + containing 1 if check passed, 0 if not + _statsd_connection -- statsd.Connection object or None + _statsd_metrics -- By-milestone dictionary of region-inspecific + StatsD metrics. Metrics are gauges, + containing seconds since epoch of recent + milestone occurrence. Empty if StatsD metrics + are not served + _statsd_region_metrics -- By-milestone dictionary of region-specific + StatsD metric descriptors. Empty if StatsD + metrics are not served + _statsd_check_metrics -- None or check result StatsD gauge descriptor + """ + # StatsD metric descriptor (working around the absence of labels in StatsD + # by putting them into name) + _StatsdLabeledMetricInfo = \ + NamedTuple( + "_StatsdLabeledMetricInfo", + [ + # String.format()-compatible pattern, containing placeholder for + # label value + ("pattern", str), + # Dictionary of StatsD metrics, indexed by label value name + ("metrics", Dict[str, statsd.Gauge])]) + + def __init__(self, state_db: StateDb, prometheus_port: Optional[int], + statsd_server: Optional[str]) -> None: + """ Constructor + + Arguments: + state_db -- StateDb object + prometheus_port -- Port to serve Prometheus metrics on or None + statsd_server -- Address of StatsD server to send metrics to or None + """ + self._state_db = state_db + self._prometheus_metrics: Dict[DownloaderMilestone, Any] = {} + self._prometheus_region_metrics: Dict[DownloaderMilestone, Any] = {} + self._prometheus_check_metric: Any = None + self._statsd_connection: Optional[statsd.Connection] = None + self._statsd_metrics: Dict[DownloaderMilestone, Any] = {} + self._statsd_region_metrics: \ + Dict[DownloaderMilestone, + "StatusUpdater._StatsdLabeledMetricInfo"] = {} + self._statsd_check_metrics: \ + Optional["StatusUpdater._StatsdLabeledMetricInfo"] = None + if prometheus_port is not None: + prometheus_client.start_http_server(prometheus_port) + self._prometheus_metrics[DownloaderMilestone.DownloadStart] = \ + prometheus_client.Gauge( + "fs_download_started", + "Seconds since epoch of last downloader script run") + self._prometheus_metrics[DownloaderMilestone.DownloadSuccess] = \ + prometheus_client.Gauge( + "fs_download_succeeded", + "Seconds since epoch of last downloader script success") + self._prometheus_metrics[DownloaderMilestone.DbUpdated] = \ + prometheus_client.Gauge( + "fs_download_database_updated", + "Seconds since epoch of last FS database file update") + self._prometheus_region_metrics[ + DownloaderMilestone.RegionChanged] = \ + prometheus_client.Gauge( + "fs_download_region_changed", + "Seconds since epoch of last region changed", ["region"]) + self._prometheus_check_metric = \ + prometheus_client.Gauge( + "fs_download_check_passed", + "Recent check state (1 - success, 0 - failure)", + ["check_type"]) + if statsd_server: + host, port = statsd_server.split(":", 1) + self._statsd_connection = \ + statsd.Connection(host=host, port=int(port)) + self._statsd_metrics[DownloaderMilestone.DownloadStart] = \ + statsd.Gauge("fs_download_started") + self._statsd_metrics[DownloaderMilestone.DownloadSuccess] = \ + statsd.Gauge("fs_download_succeeded") + self._statsd_metrics[DownloaderMilestone.DbUpdated] = \ + statsd.Gauge("fs_download_database_updated") + self._statsd_region_metrics[DownloaderMilestone.RegionChanged] = \ + self._StatsdLabeledMetricInfo( + pattern="fs_download_region_changed_{region}", metrics={}) + self._statsd_check_metrics = \ + self._StatsdLabeledMetricInfo( + pattern="fs_download_check_passed_{check_type}", + metrics={}) + + def milestone(self, milestone: DownloaderMilestone, + updated_regions: Optional[Iterable[str]] = None, + all_regions: Optional[List[str]] = None) -> None: + """ Update milestone + + Arguments: + milestone -- Milestone to write + updated_regions -- List of region strings of updated regions, None for + region-inspecific milestones + all_regions -- List of all region strings, None for + region-inspecific milestones + """ + self._state_db.write_milestone( + milestone=milestone, updated_regions=updated_regions, + all_regions=all_regions) + seconds_since_epoch = int(time.time()) + if milestone in self._prometheus_metrics: + self._prometheus_metrics[milestone].set(seconds_since_epoch) + if (milestone in self._prometheus_region_metrics) and updated_regions: + for region in updated_regions: + self._prometheus_region_metrics[milestone].\ + labels(region=region).set(seconds_since_epoch) + if milestone in self._statsd_metrics: + self._statsd_metrics[milestone].send(seconds_since_epoch) + if (milestone in self._statsd_region_metrics) and updated_regions: + mi = self._statsd_region_metrics[milestone] + for region in updated_regions: + metric: Optional[statsd.Gauge] = mi.metrics.get(region) + if not metric: + metric = statsd.Gauge(mi.pattern.format(region=region)) + mi.metrics[region] = metric + metric.send(seconds_since_epoch) + + def status_check(self, check_type: CheckType, + results: Optional[Dict[str, Optional[str]]] = None) \ + -> None: + """ Update status check results + + Arguments: + check_type -- Type of check + results -- Itemized results: dictionary contained error message for + failed checks, None for succeeded ones + """ + self._state_db.write_check_results(check_type=check_type, + results=results) + success = all(result is None for result in (results or {}).values()) + if self._prometheus_check_metric: + self._prometheus_check_metric.labels(check_type=check_type.name).\ + set(1 if success else 0) + if self._statsd_check_metrics: + metric: Optional[statsd.Gauge] = \ + self._statsd_check_metrics.metrics.get(check_type.name) + if not metric: + metric = \ + statsd.Gauge( + self._statsd_check_metrics.pattern.format( + check_type=check_type.name)) + self._statsd_check_metrics.metrics[check_type.name] = metric + metric.send(1 if success else 0) + + +def extract_fsid_table(uls_file: str, fsid_file: str, + executor: LoggingExecutor) -> None: + """ Try to extract FSID table from ULS database: + + Arguments: + uls_file -- FS database filename + fsid_file -- FSID table filename + executor -- LoggingExecutor object + """ + # Clean previous FSID files... + fsid_name_parts = os.path.splitext(fsid_file) + logging.info("Extracting FSID table") + if not executor.execute(f"rm -f {fsid_name_parts[0]}*{fsid_name_parts[1]}", + fail_on_error=False): + logging.warning(f"Strangely can't remove " + f"{fsid_name_parts[0]}*{fsid_name_parts[1]}. " + f"Proceeding nevertheless") + # ... and extracting latest one from previous FS database + if os.path.isfile(uls_file): + if executor.execute([FSID_TOOL, "check", uls_file], + fail_on_error=False): + executor.execute([FSID_TOOL, "extract", uls_file, fsid_file], + fail_on_error=True) + + +def get_uls_identity(uls_file: str) -> Optional[Dict[str, str]]: + """ Read regions' identity from ULS database + + Arguments: + uls_file -- ULS database + Returns dictionary of region identities indexed by region name + """ + if not os.path.isfile(uls_file): + return None + engine = sa.create_engine("sqlite:///" + uls_file) + conn = engine.connect() + try: + metadata = sa.MetaData() + metadata.reflect(bind=engine) + id_table = metadata.tables.get(DATA_IDS_TABLE) + if id_table is None: + return None + if not all(col in id_table.c for col in (DATA_IDS_REG_COLUMN, + DATA_IDS_ID_COLUMN)): + return None + ret: Dict[str, str] = {} + for row in conn.execute(sa.select(id_table)).fetchall(): + ret[row[DATA_IDS_REG_COLUMN]] = row[DATA_IDS_ID_COLUMN] + return ret + finally: + conn.close() + + +def update_uls_file(uls_dir: str, uls_file: str, symlink: str, + executor: LoggingExecutor) -> None: + """ Atomically retargeting symlink to new ULS file + + Arguments: + uls_dir -- Directory containing ULS files and symlink + uls_file -- Base name of new ULS file (already in ULS directory) + symlink -- Base name of symlink pointing to current ULS file + executor -- LoggingExecutor object + """ + # Getting random name for temporary symlink + fd, temp_symlink_name = tempfile.mkstemp(dir=uls_dir, suffix="_" + symlink) + os.close(fd) + os.unlink(temp_symlink_name) + # Name race condition is astronomically improbable, as name is random and + # downloader is one (except for development environment) + + assert uls_file == os.path.basename(uls_file) + assert os.path.isfile(os.path.join(uls_dir, uls_file)) + os.symlink(uls_file, temp_symlink_name) + + executor.execute( + ["mv", "-fT", temp_symlink_name, os.path.join(uls_dir, symlink)], + fail_on_error=True) + logging.info(f"FS database symlink '{os.path.join(uls_dir, symlink)}' " + f"now points to '{uls_file}'") + + +class DbDiff: + """ Computes and holds difference between two FS (aka ULS) databases + + Public attributes: + valid -- True if difference is valid + prev_len -- Number of paths in previous database + new_len -- Number of paths in new database + diff_len -- Number of different paths + ras_diff_len -- Number of different RAS entries + diff_tiles -- Tiles containing receivers of different paths + """ + + def __init__(self, prev_filename: str, new_filename: str, + executor: LoggingExecutor) -> None: + """ Constructor + + Arguments: + prev_filename -- Previous file name + new_filename -- New filename + executor -- LoggingExecutor object + """ + self.valid = False + self.prev_len = 0 + self.new_len = 0 + self.diff_len = 0 + self.diff_tiles: List[LatLonRect] = [] + logging.info("Getting differences with previous database") + output = \ + executor.execute( + [FS_DB_DIFF, "--report_tiles", prev_filename, new_filename], + timeout_sec=10 * 60, return_output=True, fail_on_error=False) + if output is None: + logging.error("Database comparison failed") + return + m = re.search(r"Paths in DB1:\s+(?P\d+)(.|\n)+" + r"Paths in DB2:\s+(?P\d+)(.|\n)+" + r"Different paths:\s+(?P\d+)(.|\n)+" + r"Different RAS entries:\s+(?P\d+)(.|\n)+", + cast(str, output)) + if m is None: + logging.error( + f"Output of '{FS_DB_DIFF}' has unrecognized structure") + return + self.prev_len = int(cast(str, m.group("db2"))) + self.new_len = int(cast(str, m.group("db2"))) + self.diff_len = int(cast(str, m.group("diff"))) + self.ras_diff_len = int(cast(str, m.group("ras_diff"))) + for m in re.finditer( + r"Difference in tile " + r"\[(?P[0-9.]+)-(?P[0-9.]+)\]" + r"(?P[NS]), " + r"\[(?P[0-9.]+)-(?P[0-9.]+)\]" + r"(?P[EW])", cast(str, output)): + lat_sign = 1 if m.group("lat_sign") == "N" else -1 + lon_sign = 1 if m.group("lon_sign") == "E" else -1 + self.diff_tiles.append( + LatLonRect( + min_lat=float(m.group("min_lat")) * lat_sign, + max_lat=float(m.group("max_lat")) * lat_sign, + min_lon=float(m.group("min_lon")) * lon_sign, + max_lon=float(m.group("max_lon")) * lon_sign)) + logging.info( + f"Database comparison succeeded: " + f"{os.path.basename(prev_filename)} has {self.prev_len} paths, " + f"{os.path.basename(new_filename)} has {self.new_len} paths, " + f"difference is in {self.diff_len} paths, " + f"{self.ras_diff_len} RAS entries, " + f"{len(self.diff_tiles)} tiles") + self.valid = True + + +class UlsFileChecker: + """ Checker of ULS database validity + + Private attributes: + _max_change_percent -- Optional percent of maximum difference + _afc_url -- Optional AFC Service URL to test ULS database on + _afc_parallel -- Optional number of parallel AFC requests to make + during database verification + _regions -- List of regions to test database on. Empty if on all + regions + _executor -- LoggingExecutor object + _status_updater -- StatusUpdater object + """ + + def __init__(self, executor: LoggingExecutor, + status_updater: StatusUpdater, + max_change_percent: Optional[float] = None, + afc_url: Optional[str] = None, + afc_parallel: Optional[int] = None, + regions: Optional[List[str]] = None) -> None: + """ Constructor + + Arguments: + executor -- LoggingExecutor object + status_updater -- StatusUpdater object + max_change_percent -- Optional percent of maximum difference + afc_url -- Optional AFC Service URL to test ULS database on + afc_parallel -- Optional number of parallel AFC requests to make + during database verification + regions -- Optional list of regions to test database on. If + empty or None - test on all regions + """ + self._executor = executor + self._status_updater = status_updater + self._max_change_percent = max_change_percent + self._afc_url = afc_url + self._afc_parallel = afc_parallel + self._regions: List[str] = regions or [] + + def valid(self, base_dir: str, new_filename: str, + db_diff: Optional[DbDiff]) -> bool: + """ Checks validity of given database + + Arguments: + base_dir -- Directory, containing database being checked. This + argument is currently unused + new_filename -- Database being checked. Should have exactly same path + as required in AFC Config + db_diff -- None or difference from previous database + Returns True if check passed + """ + check_results: Dict[str, Optional[str]] = {} + for item, (tested, errmsg) in \ + [("difference from previous", self._check_diff(db_diff)), + ("usable by AFC Service", self._check_afc(new_filename))]: + if not tested: + continue + if errmsg is not None: + logging.error(errmsg) + check_results[item] = errmsg + self._status_updater.status_check(CheckType.FsDatabase, check_results) + return all(errmsg is None for errmsg in check_results.values()) + + def _check_diff(self, db_diff: Optional[DbDiff]) \ + -> Tuple[bool, Optional[str]]: + """ Checks amount of difference since previous database. + Returns (check_performed, error_message) tuple """ + if db_diff is None: + return (False, None) + if not db_diff.valid: + return (True, "Database difference can't be obtained") + if ((db_diff.diff_len == 0) and (db_diff.ras_diff_len == 0)) != \ + (len(db_diff.diff_tiles) == 0): + return \ + (True, + f"Inconsistent indication of database difference: difference " + f"is in {db_diff.diff_len} paths and in " + f"{db_diff.ras_diff_len} RAS entries, but in " + f"{len(db_diff.diff_tiles)} tiles") + if self._max_change_percent is None: + return (False, None) + diff_percent = \ + round( + 100 if db_diff.new_len == 0 else + ((db_diff.diff_len * 100) / db_diff.new_len), + 3) + if diff_percent > self._max_change_percent: + return \ + (True, + f"Database changed by {diff_percent}%, which exceeds the " + f"limit of {self._max_change_percent}%") + return (True, None) + + def _check_afc(self, new_filename: str) -> Tuple[bool, Optional[str]]: + """ Checks new database against AFC Service + + Arguments: + new_filename -- Database being checked. Should have exactly same path + as required in AFC Config + Returns (check_performed, error_message) tuple + """ + if self._afc_url is None: + return (False, None) + logging.info("Testing new FS database on AFC Service") + args = [FS_AFC, "--server_url", self._afc_url] + \ + (["--parallel", str(self._afc_parallel)] + if self._afc_parallel is not None else []) + \ + [f"--region={r}" for r in self._regions] + [new_filename] + logging.debug(" ".join(shlex.quote(arg) for arg in args)) + if not self._executor.execute(args, fail_on_error=False, + timeout_sec=30 * 60): + return (True, "AFC Service test failed") + return (True, None) + + +class ExtParamFilesChecker: + """ Checks that external parameter file match those in image + + Private attributes: + _epf_list -- List of external parameter file sets descriptors + (_ExtParamFiles objects) or None + _script_dir -- Downloader script directory or None + _status_updater -- StatusUpdater object + """ + + # Descriptor of external parameter files + _ExtParamFiles = \ + NamedTuple("_ExtParamFiles", + [ + # Location in the internet + ("base_url", str), + # Downloader script subdirectory + ("subdir", str), ("files", List[str])]) + + def __init__(self, status_updater: StatusUpdater, + ext_files_arg: Optional[List[str]] = None, + script_dir: Optional[str] = None) -> None: + """ Constructor + + Arguments: + status_updater -- StatusUpdater object + ext_files_arg -- List of 'BASE_URL:SUBDIR:FILES,FILE...' groups, + separated with semicolon: external parameter file + descriptors from command line + script_dir -- Downloader script directory + """ + self._epf_list: \ + Optional[List["ExtParamFilesChecker._ExtParamFiles"]] = None + if ext_files_arg is not None: + self._epf_list = [] + for efg in ext_files_arg: + for ef in efg.split(";"): + m = re.match(r"^(.*):(.+):(.+)$", ef) + error_if(m is None, + f"Invalid format of --check_ext_files parameter: " + f"'{ef}'") + assert m is not None + self._epf_list.append( + self._ExtParamFiles(base_url=m.group(1).rstrip("/"), + subdir=m.group(2), + files=m.group(3).split(","))) + self._script_dir = script_dir + self._status_updater = status_updater + + def check(self) -> None: + """ Check that external files matches those in image """ + assert (self._epf_list is not None) and (self._script_dir is not None) + temp_dir: Optional[str] = None + check_results: Dict[str, Optional[str]] = {} + try: + temp_dir = tempfile.mkdtemp() + for epf in self._epf_list: + for filename in epf.files: + errmsg: Optional[str] = None + try: + internal_file_name = \ + os.path.join(self._script_dir, epf.subdir, + filename) + if not os.path.isfile(internal_file_name): + errmsg = "Absent in container" + continue + try: + url = f"{epf.base_url}/{filename}" + external_file_name = os.path.join(temp_dir, + filename) + urllib.request.urlretrieve(url, external_file_name) + except urllib.error.URLError as ex: + logging.warning(f"Error downloading '{url}': {ex}") + errmsg = "Download failed" + continue + contents: List[bytes] = [] + try: + for fn in (internal_file_name, external_file_name): + with open(fn, "rb") as f: + contents.append(f.read()) + except OSError as ex: + logging.warning(f"Read failed: {ex}") + errmsg = "Read failed" + continue + if contents[0] != contents[1]: + errmsg = "External content changed" + finally: + check_results[os.path.join(epf.subdir, filename)] = \ + errmsg + finally: + self._status_updater.status_check(check_type=CheckType.ExtParams, + results=check_results) + if temp_dir: + shutil.rmtree(temp_dir, ignore_errors=True) + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + argument_parser = argparse.ArgumentParser( + description="ULS data download control service") + argument_parser.add_argument( + "--download_script", metavar="ULS_PARSER_SCRIPT", + help=f"ULS download script{env_help(Settings, 'download_script')}") + argument_parser.add_argument( + "--download_script_args", metavar="ARGS", + help=f"Optional additional arguments to pass to ULS download script" + f"{env_help(Settings, 'download_script_args')}") + argument_parser.add_argument( + "--region", metavar="REG1[:REG2[:REG3...]]", + help=f"Colon-separated list of regions to download. Default is all " + f"regions{env_help(Settings, 'region')}") + argument_parser.add_argument( + "--result_dir", metavar="RESULT_DIR", + help=f"Directory where ULS download script puts resulting database" + f"{env_help(Settings, 'result_dir')}") + argument_parser.add_argument( + "--temp_dir", metavar="TEMP_DIR", + help=f"Directory containing downloader's temporary files (cleared " + f"before downloading){env_help(Settings, 'temp_dir')}") + argument_parser.add_argument( + "--ext_db_dir", metavar="EXTERNAL_DATABASE_DIR", + help=f"Directory where new ULS databases should be copied. If " + f"--ext_db_symlink contains path, this parameter is root directory " + f"for this path{env_help(Settings, 'ext_db_dir')}") + argument_parser.add_argument( + "--ext_db_symlink", metavar="CURRENT_DATABASE_SYMLINK", + help=f"Symlink in database directory (specified with --ext_db_dir) " + f"that points to current database. May contain path - if so, this " + f"path is used for AFC Config override during database validity check" + f"{env_help(Settings, 'ext_db_symlink')}") + argument_parser.add_argument( + "--ext_ras_database", metavar="FILENAME", + help=f"Externally maintained RAS 'database' file" + f"{env_help(Settings, 'ext_ras_database')}") + argument_parser.add_argument( + "--ras_database", metavar="FILENAME", + help=f"Where from downloader scripts reads RAS 'database'" + f"{env_help(Settings, 'ras_database')}") + argument_parser.add_argument( + "--fsid_file", metavar="FSID_FILE", + help=f"FSID file where ULS downloader is expected to read/update it" + f"{env_help(Settings, 'fsid_file')}") + argument_parser.add_argument( + "--service_state_db_dsn", metavar="STATE_DB_DSN", + help=f"Connection string to database containing FS service state " + f"(that is used by healthcheck script)" + f"{env_help(Settings, 'service_state_db_dsn')}") + argument_parser.add_argument( + "--service_state_db_create_if_absent", action="store_true", + help=f"Create state database if absent" + f"{env_help(Settings, 'service_state_db_create_if_absent')}") + argument_parser.add_argument( + "--service_state_db_recreate", action="store_true", + help=f"Recreate state DB if it exists" + f"{env_help(Settings, 'service_state_db_recreate')}") + argument_parser.add_argument( + "--prometheus_port", metavar="PORT_NUMBER", + help=f"Port to serve Prometheus metrics on. Default is to not serve " + f"Prometheus metrics. Ignored (as irrelevant) if specified with " + f"'--run_once'{env_help(Settings, 'prometheus_port')}") + argument_parser.add_argument( + "--statsd_server", metavar="HOST[:PORT]", + help=f"Send metrics to given StatsD host. Default is not to" + f"{env_help(Settings, 'prometheus_port')}") + argument_parser.add_argument( + "--check_ext_files", metavar="BASE_URL:SUBDIR:FILENAME[,...][;...]", + action="append", default=[], + help=f"Verify that given files at given location match files at given " + f"subdirectory of ULS downloader script, several such " + f"semicolon-separated groups may be specified (e.g. in environment " + f"variable){env_help(Settings, 'check_ext_files')}") + argument_parser.add_argument( + "--max_change_percent", metavar="MAX_CHANGE_PERCENT", + help=f"Maximum allowed change since previous database in percents" + f"{env_help(Settings, 'max_change_percent')}") + argument_parser.add_argument( + "--afc_url", metavar="URL", + help=f"URL for making trial AFC Requests with new database" + f"{env_help(Settings, 'afc_url')}") + argument_parser.add_argument( + "--afc_parallel", metavar="NUMBER", + help=f"Number of parallel AFC Request to make during verifying new " + f"database against AFC Engine{env_help(Settings, 'afc_parallel')}") + argument_parser.add_argument( + "--rcache_url", metavar="URL", + help=f"URL for spatial invalidation{env_help(Settings, 'rcache_url')}") + argument_parser.add_argument( + "--rcache_enabled", metavar="TRUE/FALSE", + help=f"FALSE to disable spatial invalidation (even if URL specified)" + f"{env_help(Settings, 'rcache_enabled')}") + argument_parser.add_argument( + "--delay_hr", metavar="DELAY_HR", + help=f"Delay before invocation in hours" + f"{env_help(Settings, 'delay_hr')}") + argument_parser.add_argument( + "--interval_hr", metavar="INTERVAL_HR", + help=f"Download interval in hours{env_help(Settings, 'interval_hr')}") + argument_parser.add_argument( + "--timeout_hr", metavar="TIMEOUT_HR", + help=f"Download script execution timeout in hours" + f"{env_help(Settings, 'timeout_hr')}") + argument_parser.add_argument( + "--nice", action="store_true", + help=f"Run download script on nice (low) priority" + f"{env_help(Settings, 'nice')}") + argument_parser.add_argument( + "--verbose", action="store_true", + help=f"Print detailed log information{env_help(Settings, 'verbose')}") + argument_parser.add_argument( + "--run_once", action="store_true", + help=f"Run download once and exit{env_help(Settings, 'run_once')}") + argument_parser.add_argument( + "--force", action="store_true", + help=f"Force database update even if it is not noticeably changed or " + f"not passed validity check{env_help(Settings, 'force')}") + + settings: Settings = \ + cast(Settings, merge_args(settings_class=Settings, + args=argument_parser.parse_args(argv))) + + try: + setup_logging(verbose=settings.verbose) + + if settings.run_once and (settings.prometheus_port is not None): + logging.warning( + "There is no point in using Prometheus in run-once mode. Use " + "--statsd_server if metrics are necessary") + settings.prometheus_port = None + + print_args(settings) + + state_db = StateDb(db_dsn=settings.service_state_db_dsn) + state_db.check_server() + if settings.service_state_db_create_if_absent: + state_db.create_db( + recreate_tables=settings.service_state_db_recreate) + + status_updater = \ + StatusUpdater(state_db=state_db, + prometheus_port=settings.prometheus_port, + statsd_server=settings.statsd_server) + + if not state_db.read_milestone(DownloaderMilestone.ServiceBirth): + status_updater.milestone(DownloaderMilestone.ServiceBirth) + + error_if(not os.path.isfile(settings.download_script), + f"Download script '{settings.download_script}' not found") + full_ext_db_dir = \ + os.path.join(settings.ext_db_dir, + os.path.dirname(settings.ext_db_symlink)) + error_if( + not os.path.isdir(full_ext_db_dir), + f"External database directory '{full_ext_db_dir}' not found") + + executor = LoggingExecutor() + + ext_params_file_checker = \ + ExtParamFilesChecker( + ext_files_arg=settings.check_ext_files, + script_dir=os.path.dirname(settings.download_script), + status_updater=status_updater) + + current_uls_file = os.path.join(settings.ext_db_dir, + settings.ext_db_symlink) + + uls_file_checker = \ + UlsFileChecker( + max_change_percent=settings.max_change_percent, + afc_url=settings.afc_url, afc_parallel=settings.afc_parallel, + regions=None if settings.region is None + else settings.region.split(":"), + executor=executor, status_updater=status_updater) + + rcache_settings = \ + RcacheClientSettings( + enabled=settings.rcache_enabled and bool(settings.rcache_url), + service_url=settings.rcache_url, postgres_dsn=None, + rmq_dsn=None) + rcache_settings.validate_for(rcache=True) + rcache: Optional[RcacheClient] = \ + RcacheClient(rcache_settings) if rcache_settings.enabled else None + + status_updater.milestone(DownloaderMilestone.ServiceStart) + + if settings.delay_hr and (not settings.run_once): + logging.info(f"Delaying by {settings.delay_hr} hours") + time.sleep(settings.delay_hr * 3600) + + if settings.run_once: + logging.info("Running healthcheck script") + executor.execute( + [sys.executable, HEALTHCHECK_SCRIPT, "--force_success"], + timeout_sec=200, fail_on_error=True) + + # Temporary name of new ULS database in external directory + temp_uls_file_name: Optional[str] = None + + while True: + err_msg: Optional[str] = None + completed = False + logging.info("Starting ULS download") + download_start_time = datetime.datetime.now() + status_updater.milestone(DownloaderMilestone.DownloadStart) + try: + has_previous = os.path.islink(current_uls_file) + if has_previous: + logging.info( + f"Current database: '{os.readlink(current_uls_file)}'") + + extract_fsid_table(uls_file=current_uls_file, + fsid_file=settings.fsid_file, + executor=executor) + + # Clear some directories from stuff left from previous + # downloads + for dir_to_clean in [settings.result_dir, settings.temp_dir]: + if dir_to_clean and os.path.isdir(dir_to_clean): + executor.execute(f"rm -rf {dir_to_clean}/*", + timeout_sec=100, fail_on_error=True) + + logging.info("Copying RAS database") + shutil.copyfile(settings.ext_ras_database, + settings.ras_database) + + logging.info("Checking if external parameter files changed") + ext_params_file_checker.check() + + # Issue download script + cmdline_args: List[str] = [] + if settings.nice and (os.name == "posix"): + cmdline_args.append("nice") + cmdline_args.append(settings.download_script) + if settings.region: + cmdline_args += ["--region", settings.region] + if settings.download_script_args: + cmdline_args.append(settings.download_script_args) + logging.info(f"Starting {' '.join(cmdline_args)}") + executor.execute( + " ".join(cmdline_args) if settings.download_script_args + else cmdline_args, + cwd=os.path.dirname(settings.download_script), + timeout_sec=settings.timeout_hr * 3600, + fail_on_error=True) + + # Find out name of new ULS file + uls_files = glob.glob(os.path.join(settings.result_dir, + ULS_FILEMASK)) + if len(uls_files) < 1: + raise ProcessingException( + "ULS file not generated by ULS downloader") + if len(uls_files) > 1: + raise ProcessingException( + f"More than one {ULS_FILEMASK} file generated by ULS " + f"downloader. What gives?") + + # Check what regions were updated + new_uls_file = uls_files[0] + logging.info(f"ULS file '{new_uls_file}' created. It will " + f"undergo some inspection") + new_uls_identity = get_uls_identity(new_uls_file) + if new_uls_identity is None: + raise ProcessingException( + "Generated ULS file does not contain identity " + "information") + status_updater.milestone(DownloaderMilestone.DownloadSuccess) + + updated_regions = set(new_uls_identity.keys()) + if has_previous: + for current_region, current_identity in \ + (get_uls_identity(current_uls_file) or {}).items(): + if current_identity and \ + (new_uls_identity.get(current_region) == + current_identity): + updated_regions.remove(current_region) + if updated_regions: + logging.info(f"Updated regions: " + f"{', '.join(sorted(updated_regions))}") + + # If anything was updated - do the update routine + if updated_regions or settings.force: + # Embed updated FSID table to the new database + logging.info("Embedding FSID table") + executor.execute( + [FSID_TOOL, "embed", new_uls_file, settings.fsid_file], + fail_on_error=True) + + temp_uls_file_name = \ + os.path.join(full_ext_db_dir, + "temp_" + os.path.basename(new_uls_file)) + # Copy new ULS file to external directory + logging.debug( + f"Copying '{new_uls_file}' to '{temp_uls_file_name}'") + shutil.copy2(new_uls_file, temp_uls_file_name) + + db_diff = DbDiff(prev_filename=current_uls_file, + new_filename=temp_uls_file_name, + executor=executor) \ + if has_previous else None + if settings.force or \ + uls_file_checker.valid( + base_dir=settings.ext_db_dir, + new_filename=os.path.join( + os.path.dirname(settings.ext_db_symlink), + os.path.basename(temp_uls_file_name)), + db_diff=db_diff): + if settings.force: + status_updater.status_check(CheckType.FsDatabase, + None) + # Renaming database + permanent_uls_file_name = \ + os.path.join(full_ext_db_dir, + os.path.basename(new_uls_file)) + os.rename(temp_uls_file_name, permanent_uls_file_name) + # Retargeting symlink + update_uls_file( + uls_dir=full_ext_db_dir, + uls_file=os.path.basename(new_uls_file), + symlink=os.path.basename(settings.ext_db_symlink), + executor=executor) + if rcache and (db_diff is not None) and \ + db_diff.diff_tiles: + tile_list = \ + "<" + \ + ">, <".join(tile.short_str() for tile in + db_diff.diff_tiles[: 1000]) + \ + ">" + logging.info(f"Requesting invalidation of the " + f"following tiles: {tile_list}") + rcache.rcache_spatial_invalidate( + tiles=db_diff.diff_tiles) + + # Update data change times (for health checker) + status_updater.milestone( + milestone=DownloaderMilestone.RegionChanged, + updated_regions=updated_regions, + all_regions=list(new_uls_identity.keys())) + + status_updater.milestone(DownloaderMilestone.DbUpdated) + completed = True + else: + logging.info("FS data is identical to previous. No update " + "will be done") + except (OSError, subprocess.SubprocessError, ProcessingException) \ + as ex: + err_msg = str(ex) + logging.error(f"Download failed: {ex}") + finally: + exec_output = executor.get_output() + if err_msg: + exec_output = f"{exec_output.rstrip()}\n{err_msg}\n" + state_db.write_log(log_type=LogType.Last, log=exec_output) + if completed: + state_db.write_log(log_type=LogType.LastCompleted, + log=exec_output) + if err_msg: + state_db.write_log(log_type=LogType.LastFailed, + log=exec_output) + try: + if temp_uls_file_name and \ + os.path.isfile(temp_uls_file_name): + logging.debug(f"Removing '{temp_uls_file_name}'") + os.unlink(temp_uls_file_name) + except OSError as ex: + logging.error(f"Attempt to remove temporary ULS database " + f"'{temp_uls_file_name}' failed: {ex}") + + # Prepare to sleep + download_duration_sec = \ + (datetime.datetime.now() - download_start_time).total_seconds() + logging.info( + f"Download took {download_duration_sec // 60} minutes") + if settings.run_once: + break + remaining_seconds = \ + max(0, settings.interval_hr * 3600 - download_duration_sec) + if remaining_seconds: + next_attempt_at = \ + (datetime.datetime.now() + + datetime.timedelta(seconds=remaining_seconds)).isoformat() + logging.info(f"Next download at {next_attempt_at}") + time.sleep(remaining_seconds) + except KeyboardInterrupt: + sys.exit(1) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/uls/uls_service_common.py b/uls/uls_service_common.py new file mode 100755 index 0000000..70390c4 --- /dev/null +++ b/uls/uls_service_common.py @@ -0,0 +1,137 @@ +""" Common stuff for ULS Service related scripts """ + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=wrong-import-order, invalid-name, +# pylint: disable=logging-fstring-interpolation, global-statement + +import __main__ +import inspect +import logging +import os +import pydantic +import sys +from typing import Any, Dict, List, Optional + +# Default directory for status filers +DEFAULT_STATUS_DIR = os.path.join(os.path.dirname(__file__), "status") + +# Exit code to use in error() +error_exit_code = 1 + +# Logging level to use in error() +error_log_level = logging.CRITICAL + + +def set_error_exit_params(exit_code: Optional[int] = None, + log_level: Optional[int] = None) -> None: + """ Sets what to do in error + + Arguments: + exit_code -- None or what exit code to use + log_level -- None or log level to use + """ + global error_exit_code, error_log_level + if exit_code is not None: + error_exit_code = exit_code + if log_level is not None: + error_log_level = log_level + + +def error(msg: str) -> None: + """ Prints given msg as error message and exit abnormally """ + logging.log(error_log_level, msg) + sys.exit(error_exit_code) + + +def error_if(cond: Any, msg: str) -> None: + """ If condition evaluates to true prints given msg as error message and + exits abnormally """ + if cond: + error(msg) + + +def dp(*args, **kwargs) -> None: + """Print debug message + + Arguments: + args -- Format and positional arguments. If latter present - formatted + with % + kwargs -- Keyword arguments. If present formatted with format() + """ + msg = args[0] if args else "" + if len(args) > 1: + msg = msg % args[1:] + if args and kwargs: + msg = msg.format(**kwargs) + cur_frame = inspect.currentframe() + assert (cur_frame is not None) and (cur_frame.f_back is not None) + frameinfo = inspect.getframeinfo(cur_frame.f_back) + logging.info(f"DP {frameinfo.function}()@{frameinfo.lineno}: {msg}") + + +def setup_logging(verbose: bool) -> None: + """ Logging setup + + Arguments: + verbose -- enable debug printout + """ + console_handler = logging.StreamHandler() + console_handler.setFormatter( + logging.Formatter( + f"{os.path.basename(__main__.__file__)}. " + f"%(levelname)s: %(asctime)s %(message)s")) + logging.getLogger().addHandler(console_handler) + logging.getLogger().setLevel( + logging.DEBUG if verbose else logging.INFO) + if verbose: + logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) + + +def env_help(settings_class: Any, arg: str, prefix: str = ". ") -> str: + """ Prints help on environment variable for given command line argument + (aka setting name). + + Environment variable name must be explicitly defined in Field() with 'env=' + + Arguments: + settings_class -- Type, derived from pydantic.BaseSettings + arg -- Command line argument + prefix -- Prefix to use if result is nonempty + Returns fragment for help message + """ + props = settings_class.schema()["properties"].get(arg) + error_if(props is None, + f"Command line argument '--{arg}' not found in settings class " + f"{settings_class.schema()['title']}") + assert props is not None + ret: List[str] = [] + default = props.get("default") + if default is not None: + ret.append(f"Default is '{default}'") + if "env" in props: + ret.append(f"May be set with '{props['env']}' environment variable") + value = os.environ.get(props["env"]) + if value is not None: + ret[-1] += f" (which is currently '{value}')" + if "default" not in props: + ret.append("This parameter is mandatory") + return (prefix + ". ".join(ret)) if ret else "" + + +def merge_args(settings_class: Any, args: Any) -> pydantic.BaseSettings: + """ Merges settings from command line arguments and Pydantic settings + + Arguments: + settings_class -- Type, derived from pydantic.BaseSettings + args -- Arguments, parsed by ArgumentParser + Returns Object of type derived from pydantic.BaseSettings + """ + kwargs: Dict[str, Any] = \ + {k: getattr(args, k) for k in settings_class.schema()["properties"] + if getattr(args, k) not in (None, False)} + return settings_class(**kwargs) diff --git a/uls/uls_service_healthcheck.py b/uls/uls_service_healthcheck.py new file mode 100755 index 0000000..dd7b722 --- /dev/null +++ b/uls/uls_service_healthcheck.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python3 +""" ULS data download control service healthcheck """ + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=unused-wildcard-import, wrong-import-order, wildcard-import +# pylint: disable=logging-fstring-interpolation, invalid-name, too-many-locals +# pylint: disable=too-many-branches, too-few-public-methods +# pylint: disable=too-many-statements + +import argparse +import datetime +import email.mime.text +import json +import logging +import os +import pydantic +import smtplib +import sqlalchemy as sa +import sys +from typing import Any, cast, List, NamedTuple, Optional, Set, Union + +from uls_service_common import * +from uls_service_state_db import AlarmType, CheckType, DownloaderMilestone, \ + LogType, safe_dsn, StateDb + + +class Settings(pydantic.BaseSettings): + """ Arguments from command lines - with their default values """ + + service_state_db_dsn: str = \ + pydantic.Field(..., env="ULS_SERVICE_STATE_DB_DSN") + smtp_info: Optional[str] = pydantic.Field(None, env="ULS_ALARM_SMTP_INFO") + email_to: Optional[str] = pydantic.Field(None, env="ULS_ALARM_EMAIL_TO") + beacon_email_to: Optional[str] = \ + pydantic.Field(None, env="ULS_BEACON_EMAIL_TO") + email_sender_location: Optional[str] = \ + pydantic.Field(None, env="ULS_ALARM_SENDER_LOCATION") + alarm_email_interval_hr: Optional[float] = \ + pydantic.Field(None, env="ULS_ALARM_ALARM_INTERVAL_HR") + beacon_email_interval_hr: Optional[float] = \ + pydantic.Field(None, env="ULS_ALARM_BEACON_INTERVAL_HR") + download_attempt_max_age_health_hr: Optional[float] = \ + pydantic.Field(6, env="ULS_HEALTH_ATTEMPT_MAX_AGE_HR") + download_success_max_age_health_hr: Optional[float] = \ + pydantic.Field(8, env="ULS_HEALTH_SUCCESS_MAX_AGE_HR") + update_max_age_health_hr: Optional[float] = \ + pydantic.Field(40, env="ULS_HEALTH_UPDATE_MAX_AGE_HR") + download_attempt_max_age_alarm_hr: Optional[float] = \ + pydantic.Field(None, env="ULS_ALARM_ATTEMPT_MAX_AGE_HR") + download_success_max_age_alarm_hr: Optional[float] = \ + pydantic.Field(None, env="ULS_ALARM_SUCCESS_MAX_AGE_HR") + region_update_max_age_alarm: Optional[str] = \ + pydantic.Field(None, env="ULS_ALARM_REG_UPD_MAX_AGE_HR") + verbose: bool = pydantic.Field(False) + force_success: bool = pydantic.Field(False) + print_email: bool = pydantic.Field(False) + + @pydantic.root_validator(pre=True) + @classmethod + def remove_empty(cls, v: Any) -> Any: + """ Prevalidator that removes empty values (presumably from environment + variables) to force use of defaults + """ + if not isinstance(v, dict): + return v + for key, value in list(v.items()): + if value is None: + del v[key] + return v + + +def send_email_smtp(smtp_info_filename: Optional[str], to: Optional[str], + subject: str, body: str) -> None: + """ Send an email via SMTP + + Arguments: + smtp_info -- Optional name of JSON file containing SMTP credentials + to -- Optional recipient email + subject -- Message subject + body -- Message body + """ + if not (smtp_info_filename and to): + return + error_if(not os.path.isfile(smtp_info_filename), + f"SMTP credentials file '{smtp_info_filename}' not found") + with open(smtp_info_filename, mode="rb") as f: + smtp_info_content = f.read() + if not smtp_info_content: + return + try: + credentials = json.loads(smtp_info_content) + except json.JSONDecodeError as ex: + error(f"Invalid format of '{smtp_info_filename}': {ex}") + smtp_kwargs: Dict[str, Any] = {} + login_kwargs: Dict[str, Any] = {} + for key, key_type, kwargs, arg_name in \ + [("SERVER", str, smtp_kwargs, "host"), + ("PORT", int, smtp_kwargs, "port"), + ("USERNAME", str, login_kwargs, "user"), + ("PASSWORD", str, login_kwargs, "password")]: + value = credentials.get(key) + if value is None: + continue + error_if(not isinstance(value, key_type), + f"Invalid type for '{key}' in '{smtp_info_filename}'") + kwargs[arg_name] = value + error_if("user" not in login_kwargs, + f"`USERNAME' (sender email) not found in '{smtp_info_filename}'") + msg = email.mime.text.MIMEText(body) + msg["Subject"] = subject + msg["From"] = login_kwargs["user"] + to_list = to.split(",") + msg["To"] = to + + try: + smtp = smtplib.SMTP_SSL(**smtp_kwargs) if credentials.get("USE_SSL") \ + else smtplib.SMTP(**smtp_kwargs) + if credentials.get("USE_TLS"): + smtp.starttls() # No idea how it would work without key/cert + else: + smtp.login(**login_kwargs) + smtp.sendmail(msg["From"], to_list, msg.as_string()) + smtp.quit() + except smtplib.SMTPException as ex: + error(f"Email send failure: {ex}") + + +def expired(event_td: Optional[datetime.datetime], + max_age_hr: Optional[Union[int, float]]) -> bool: + """ True if given amount of time expired since given event + + Arguments: + event_td -- Optional event timedate. None means that yes, expired (unless + timeout is None) + max_age_hr -- Optional timeout in hours. None means no, not expired + Returns true if timeout expired + """ + if max_age_hr is None: + return False + if event_td is None: + return True + return (datetime.datetime.now() - event_td) > \ + datetime.timedelta(hours=max_age_hr) + + +def email_if_needed(state_db: StateDb, settings: Any) -> None: + """ Send alarm/beacon emails if needed + + Arguments: + state_db -- StateDb object + settings -- Settings object + """ + if (settings.alarm_email_interval_hr is None) and \ + (settings.beacon_email_interval_hr is None): + return + service_birth = \ + state_db.read_milestone(DownloaderMilestone.ServiceBirth).get(None) + + ProblemInfo = \ + NamedTuple("ProblemInfo", + [("alarm_type", AlarmType), ("alarm_reason", str), + ("message", str)]) + + problems: List[ProblemInfo] = [] + email_to = settings.email_to + if expired( + event_td=state_db.read_milestone( + DownloaderMilestone.DownloadStart).get(None, service_birth), + max_age_hr=settings.download_attempt_max_age_alarm_hr): + problems.append( + ProblemInfo( + alarm_type=AlarmType.MissingMilestone, + alarm_reason=DownloaderMilestone.DownloadStart.name, + message=f"AFC ULS database download attempts were not taken " + f"for more than {settings.download_attempt_max_age_alarm_hr} " + f"hours")) + if expired( + event_td=state_db.read_milestone( + DownloaderMilestone.DownloadSuccess).get(None, service_birth), + max_age_hr=settings.download_success_max_age_alarm_hr): + problems.append( + ProblemInfo( + alarm_type=AlarmType.MissingMilestone, + alarm_reason=DownloaderMilestone.DownloadSuccess.name, + message=f"AFC ULS database download attempts were not " + f"succeeded for more than " + f"{settings.download_success_max_age_alarm_hr} hours")) + reg_data_changes = \ + state_db.read_milestone(DownloaderMilestone.RegionChanged) + regions: Set[str] = set() + for reg_hr in ((settings.region_update_max_age_alarm or "").split(",") + or []): + if not reg_hr: + continue + error_if(":" not in reg_hr, + f"Invalid format of '--region_update_max_age_alarm " + f"{reg_hr}': no colon found") + reg, hr = reg_hr.split(":", maxsplit=1) + regions.add(reg) + try: + max_age_hr = float(hr) + except ValueError: + error(f"Invalid value for hours in '--region_update_max_age_alarm " + f"{reg_hr}'") + if expired(event_td=reg_data_changes.get(reg, service_birth), + max_age_hr=max_age_hr): + problems.append( + ProblemInfo( + alarm_type=AlarmType.MissingMilestone, + alarm_reason=DownloaderMilestone.RegionChanged.name, + message=f"No new data for region '{reg}' were downloaded " + f"for more than {max_age_hr} hours")) + check_prefixes = \ + {CheckType.ExtParams: "External parameters synchronization problem", + CheckType.FsDatabase: "FS database validity check problem"} + for check_type, check_infos in state_db.read_check_results().items(): + prefix = check_prefixes[check_type] + for check_info in check_infos: + if check_info.errmsg is None: + continue + problems.append( + ProblemInfo( + alarm_type=AlarmType.FailedCheck, + alarm_reason=check_type.name, + message=f"{prefix}: {check_info.errmsg}")) + + loc = settings.email_sender_location + + alarms_cleared = False + if problems: + message_subject = "AFC ULS Service encountering problems" + nl = "\n" + message_body = \ + (f"AFC ULS download service {('at ' + loc) if loc else ''} " + f"encountered the following problems:\n" + f"{nl.join(problem.message for problem in problems)}\n\n") + email_interval_hr = settings.alarm_email_interval_hr + email_milestone = DownloaderMilestone.AlarmSent + else: + if state_db.read_alarm_reasons(): + alarms_cleared = True + message_subject = "AFC FS Service problems resolved" + else: + message_subject = "AFC FS Service works fine" + message_body = \ + (f"AFC FS download service {('at ' + loc) if loc else ''} " + f"works fine\n\n") + email_interval_hr = settings.beacon_email_interval_hr + email_milestone = DownloaderMilestone.BeaconSent + if settings.beacon_email_to: + email_to = settings.beacon_email_to + + if (not alarms_cleared) and \ + (not expired( + event_td=state_db.read_milestone(email_milestone).get(None), + max_age_hr=email_interval_hr)): + return + + message_body += "Overall service state is as follows:\n" + for state, heading in \ + [(DownloaderMilestone.ServiceBirth, + "First time service was started in: "), + (DownloaderMilestone.ServiceStart, + "Last time service was started in: "), + (DownloaderMilestone.DownloadStart, + "Last FS download attempt was taken in: "), + (DownloaderMilestone.DownloadSuccess, + "Last FS download attempt succeeded in: "), + (DownloaderMilestone.DbUpdated, + "Last time ULS database was updated in: ")]: + et = state_db.read_milestone(state).get(None) + message_body += \ + f"{heading}{'Unknown' if et is None else et.isoformat()}" + if et is not None: + message_body += f" ({datetime.datetime.now() - et} ago)" + message_body += "\n" + + for reg in sorted(set(cast(str, r) for r in reg_data_changes.keys()) | + regions): + et = reg_data_changes.get(reg) + message_body += \ + (f"FS data for region '{reg}' last time updated in: " + f"{'Unknown' if et is None else et.isoformat()}") + if et is not None: + message_body += f" ({datetime.datetime.now() - et} ago)" + message_body += "\n" + if email_milestone == DownloaderMilestone.AlarmSent: + log_info = state_db.read_last_log(log_type=LogType.Last) + if log_info: + message_body += \ + f"\nDownload log of {log_info.timestamp.isoformat()}:\n" + \ + f"{log_info.text}\n" + + if settings.print_email: + print(f"SUBJECT: {message_subject}\nBody:\n{message_body}") + else: + send_email_smtp(smtp_info_filename=settings.smtp_info, + to=email_to, subject=message_subject, + body=message_body) + state_db.write_milestone(email_milestone) + + alarm_reasons: Dict[AlarmType, Set[str]] = {} + for problem in problems: + alarm_reasons.setdefault(problem.alarm_type, set()).\ + add(problem.alarm_reason) + state_db.write_alarm_reasons(alarm_reasons) + + +def main(argv: List[str]) -> None: + """Do the job. + + Arguments: + argv -- Program arguments + """ + argument_parser = argparse.ArgumentParser( + description="ULS data download control service healthcheck") + argument_parser.add_argument( + "--service_state_db_dsn", metavar="STATE_DB_DSN", + help=f"Connection string to database containing FS service state " + f"(that is used by healthcheck script)" + f"{env_help(Settings, 'service_state_db_dsn')}") + argument_parser.add_argument( + "--smtp_info", metavar="SMTP_CREDENTIALS_FILE", + help=f"SMTP credentials file. For its structure - see " + f"NOTIFIER_MAIL.json secret in one of files in " + f"tools/secrets/templates directory. If parameter not specified or " + f"secret file is empty - no alarm/beacon emails will be sent" + f"{env_help(Settings, 'smtp_info')}") + argument_parser.add_argument( + "--email_to", metavar="EMAIL", + help=f"Email address to send alarms/beacons to. If parameter not " + f"specified - no alarm/beacon emails will be sent" + f"{env_help(Settings, 'email_to')}") + argument_parser.add_argument( + "--beacon_email_to", metavar="BEACON_EMAIL", + help=f"Email address to send beacon information notification to. If " + f"parameter not specified - alarm email_to will used " + f"{env_help(Settings, 'beacon_email_to')}") + argument_parser.add_argument( + "--email_sender_location", metavar="TEXT", + help=f"Information on whereabouts of service that sent alarm/beacon " + f"email{env_help(Settings, 'email_sender_location')}") + argument_parser.add_argument( + "--alarm_email_interval_hr", metavar="HOURS", + help=f"Minimum interval (in hours) between alarm emails. Default is " + f"to not send alarm emails" + f"{env_help(Settings, 'alarm_email_interval_hr')}") + argument_parser.add_argument( + "--beacon_email_interval_hr", metavar="HOURS", + help=f"Interval (in hours) between beacon (everything is OK) email " + f"messages. Default is to not send beacon emails" + f"{env_help(Settings, 'beacon_email_interval_hr')}") + argument_parser.add_argument( + "--download_attempt_max_age_health_hr", metavar="HOURS", + help=f"Pronounce service unhealthy if no download attempts were made " + f"for this number of hour" + f"{env_help(Settings, 'download_attempt_max_age_health_hr')}") + argument_parser.add_argument( + "--download_success_max_age_health_hr", metavar="HOURS", + help=f"Pronounce service unhealthy if no download attempts were " + f"succeeded for this number of hours" + f"{env_help(Settings, 'download_success_max_age_health_hr')}") + argument_parser.add_argument( + "--update_max_age_health_hr", metavar="HOURS", + help=f"Pronounce service unhealthy if no ULS updates were made for " + f"this number of hours" + f"{env_help(Settings, 'update_max_age_health_hr')}") + argument_parser.add_argument( + "--download_attempt_max_age_alarm_hr", metavar="HOURS", + help=f"Send email alarm if no download attempts were made for this " + f"number of hours" + f"{env_help(Settings, 'download_attempt_max_age_alarm_hr')}") + argument_parser.add_argument( + "--download_success_max_age_alarm_hr", metavar="HOURS", + help=f"Send email alarm if no download attempts were succeeded for " + f"this number of hours" + f"{env_help(Settings, 'download_success_max_age_alarm_hr')}") + argument_parser.add_argument( + "--region_update_max_age_alarm", + metavar="REG1:HOURS1[,REG2:HOURS2...]", + help=f"Send alarm email if data for given regions (e.g. 'US', 'CA', " + f"etc.) were not updated for given number of hours" + f"{env_help(Settings, 'region_update_max_age_alarm')}") + argument_parser.add_argument( + "--verbose", action="store_true", + help=f"Print detailed log information{env_help(Settings, 'verbose')}") + argument_parser.add_argument( + "--force_success", action="store_true", + help="Don't return error exit code if container found to be unhealthy") + argument_parser.add_argument( + "--print_email", action="store_true", + help="Print email instead of sending it (for debug purposes)") + + settings: Settings = \ + cast(Settings, merge_args(settings_class=Settings, + args=argument_parser.parse_args(argv))) + + setup_logging(verbose=settings.verbose) + + set_error_exit_params(log_level=logging.ERROR) + if settings.force_success: + set_error_exit_params(exit_code=00) + + state_db = StateDb(db_dsn=settings.service_state_db_dsn) + try: + state_db.connect() + except sa.exc.SQLAlchemyError as ex: + error(f"Can't connect to FS downloader state database " + f"{safe_dsn(settings.service_state_db_dsn)}: {repr(ex)}") + + state_db.write_milestone(DownloaderMilestone.Healthcheck) + + email_if_needed(state_db=state_db, settings=settings) + + last_start = state_db.read_milestone(DownloaderMilestone.ServiceStart) + error_if(last_start is None, "FS downloader service not started") + + error_if( + expired(event_td=state_db.read_milestone( + DownloaderMilestone.DownloadStart).get(None), + max_age_hr=settings.download_attempt_max_age_health_hr), + f"Last download attempt happened more than " + f"{settings.download_attempt_max_age_health_hr} hours ago or not at " + f"all") + + error_if( + expired(event_td=state_db.read_milestone( + DownloaderMilestone.DownloadSuccess).get(None), + max_age_hr=settings.download_success_max_age_health_hr), + f"Last download success happened more than " + f"{settings.download_success_max_age_health_hr} hours ago or not at " + f"all") + + error_if( + expired(event_td=state_db.read_milestone( + DownloaderMilestone.DbUpdated).get(None), + max_age_hr=settings.update_max_age_health_hr), + f"Last time FS database was updated happened more than " + f"{settings.update_max_age_health_hr} hours ago or not at all") + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/uls/uls_service_state_db.py b/uls/uls_service_state_db.py new file mode 100644 index 0000000..c72a999 --- /dev/null +++ b/uls/uls_service_state_db.py @@ -0,0 +1,546 @@ +""" ULS service state database """ + +# Copyright (C) 2022 Broadcom. All rights reserved. +# The term "Broadcom" refers solely to the Broadcom Inc. corporate affiliate +# that owns the software below. +# This work is licensed under the OpenAFC Project License, a copy of which is +# included with this software program. + +# pylint: disable=invalid-name, broad-exception-caught, wildcard-import +# pylint: disable=wrong-import-order, unused-wildcard-import + +import datetime +import enum +import sqlalchemy as sa +import sqlalchemy.dialects.postgresql as sa_pg +from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Set +import urllib.parse +from uls_service_common import * + +# FS database downloader milestone. Names used in database and Prometheus +# metrics. Please change judiciously (if at all) +DownloaderMilestone = \ + enum.Enum("DownloaderMilestone", + [ + # Service birth (first write to milestone database) + "ServiceBirth", + # Service start + "ServiceStart", + # Download start + "DownloadStart", + # Download success + "DownloadSuccess", + # FS region changed + "RegionChanged", + # FS database file updated + "DbUpdated", + # External parameters checked + "ExtParamsChecked", + # Healthcheck performed + "Healthcheck", + # Beacon sent + "BeaconSent", + # Alarm sent + "AlarmSent"]) + +# Type of log +LogType = \ + enum.Enum("LogType", + [ + # Log of last download attempt + "Last", + # Log of last failed attempt + "LastFailed", + # Log of last completed update + "LastCompleted"]) + +# Information about retrieved log record +LogInfo = \ + NamedTuple( + "LogInfo", + [("text", str), ("timestamp", datetime.datetime), + ("log_type", LogType)]) + +# Check type +CheckType = enum.Enum("CheckType", ["ExtParams", "FsDatabase"]) + +# Information about check +CheckInfo = \ + NamedTuple( + "CheckInfo", + [ + # Check type + ("check_type", CheckType), + # Item checked + ("check_item", str), + # Error message (None if check passed) + ("errmsg", Optional[str]), + # Check timestamp + ("timestamp", datetime.datetime)]) + + +# Alarm types +AlarmType = enum.Enum("AlarmType", ["MissingMilestone", "FailedCheck"]) + +# Information about alarm +AlarmInfo = \ + NamedTuple( + "AlarmInfo", + [ + # Type of alarm + ("alarm_type", AlarmType), + # Specific reason for alarm (name of missing milestone, + # name of offending external file, etc.) + ("alarm_reason", str), + # Alarm timestramp + ("timestamp", datetime.datetime)]) + + +def safe_dsn(dsn: Optional[str]) -> Optional[str]: + """ Returns DSN without password (if there was any) """ + if not dsn: + return dsn + try: + parsed = urllib.parse.urlparse(dsn) + if not parsed.password: + return dsn + return \ + urllib.parse.urlunparse( + parsed._replace( + netloc=parsed.netloc.replace(":" + parsed.password, + ":"))) + except Exception: + return dsn + + +class StateDb: + """ Status database access wrapper + + Public attributes: + db_dsn -- Connection string (might be None in Alembic environment) + db_name -- Database name from connection string (might be None in Alembic + environment) + metadata -- Metadata. Originally as predefined, after connection - actual + + Private attributes:: + _engine -- SqlAlchemy engine (set on connection, reset to None on + disconnection or error) + """ + # Name of table with milestones + MILESTONE_TABLE_NAME = "milestones" + + # Name of table with alarming milestones + ALARM_TABLE_NAME = "alarms" + + # Name of table with recent successful and unsuccessful logs + LOG_TABLE_NAME = "logs" + + # Name of table with check results + CHECKS_TABLE = "checks" + + # All known table names (not including Alembic, etc.) + ALL_TABLE_NAMES = [MILESTONE_TABLE_NAME, ALARM_TABLE_NAME, LOG_TABLE_NAME, + CHECKS_TABLE] + + class _RootDb: + """ Context wrapper to work with everpresent root database + + Public attributes: + dsn -- Connection string to root database + conn -- Sqlalchemy connection object to root database + + Private attributes: + _engine -- SqlAlchemy connection object + """ + # Name of Postgres root database + ROOT_DB_NAME = "postgres" + + def __init__(self, dsn: str) -> None: + """ Constructor + + Arguments: + dsn -- Connection string to (nonroot) database of interest + """ + self.dsn = \ + urllib.parse.urlunsplit( + urllib.parse.urlsplit(dsn). + _replace(path=f"/{self.ROOT_DB_NAME}")) + self._engine: Any = None + self.conn: Any = None + try: + self._engine = sa.create_engine(self.dsn) + self.conn = self._engine.connect() + except sa.exc.SQLAlchemyError as ex: + error( + f"Can't connect to root database '{safe_dsn(self.dsn)}': " + f"{ex}") + finally: + if (self.conn is None) and (self._engine is not None): + # Connection failed + self._engine.dispose() + + def __enter__(self) -> "StateDb._RootDb": + """ Context entry """ + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """ Context exit """ + if self.conn is not None: + self.conn.close() + if self._engine is not None: + self._engine.dispose() + + def __init__(self, db_dsn: Optional[str]) -> None: + """ Constructor + + Arguments: + db_dsn -- Connection string to state database. None in Alembic + environment + """ + self.db_dsn = db_dsn + self.db_name: Optional[str] = \ + urllib.parse.urlsplit(self.db_dsn).path.strip("/") \ + if self.db_dsn else None + self.metadata = sa.MetaData() + sa.Table( + self.MILESTONE_TABLE_NAME, + self.metadata, + sa.Column("milestone", sa.Enum(DownloaderMilestone), index=True, + primary_key=True), + sa.Column("region", sa.String(), index=True, primary_key=True), + sa.Column("timestamp", sa.DateTime(), nullable=False)) + sa.Table( + self.ALARM_TABLE_NAME, + self.metadata, + sa.Column("alarm_type", sa.Enum(AlarmType), primary_key=True, + index=True), + sa.Column("alarm_reason", sa.String(), primary_key=True, + index=True), + sa.Column("timestamp", sa.DateTime(), nullable=False)) + sa.Table( + self.LOG_TABLE_NAME, + self.metadata, + sa.Column("log_type", sa.Enum(LogType), index=True, nullable=False, + primary_key=True), + sa.Column("text", sa.String(), nullable=False), + sa.Column("timestamp", sa.DateTime(), nullable=False, index=True)) + sa.Table( + self.CHECKS_TABLE, + self.metadata, + sa.Column("check_type", sa.Enum(CheckType), primary_key=True, + index=True), + sa.Column("check_item", sa.String(), primary_key=True, index=True), + sa.Column("errmsg", sa.String(), nullable=True), + sa.Column("timestamp", sa.DateTime(), nullable=False)) + self._engine: Any = None + + def check_server(self) -> bool: + """ True if database server can be connected """ + error_if(not self.db_dsn, + "FS downloader status database DSN was not specified") + assert self.db_dsn is not None + with self._RootDb(self.db_dsn) as rdb: + rdb.conn.execute("SELECT 1") + return True + return False + + def create_db(self, recreate_db=False, recreate_tables=False) -> bool: + """ Creates database if absent, optionally adjust if present + + Arguments: + recreate_db -- Recreate database if it exists + recreate_tables -- Recreate known database tables if database exists + Returns True on success, Fail on failure (if fail_on_error is False) + """ + engine: Any = None + try: + if self._engine: + self._engine.dispose() + self._engine = None + error_if(not self.db_dsn, + "FS downloader status database DSN was not specified") + assert self.db_dsn is not None + engine = self._create_engine(self.db_dsn) + if recreate_db: + with self._RootDb(self.db_dsn) as rdb: + try: + rdb.conn.execute("COMMIT") + rdb.conn.execute( + f'DROP DATABASE IF EXISTS "{self.db_name}"') + except sa.exc.SQLAlchemyError as ex: + error(f"Unable to drop database '{self.db_name}': " + f"{repr(ex)}") + try: + with engine.connect(): + pass + except sa.exc.SQLAlchemyError: + with self._RootDb(self.db_dsn) as rdb: + try: + rdb.conn.execute("COMMIT") + rdb.conn.execute( + f'CREATE DATABASE "{self.db_name}"') + with engine.connect(): + pass + except sa.exc.SQLAlchemyError as ex1: + error(f"Unable to create target database: {ex1}") + try: + if recreate_tables: + with engine.connect() as conn: + conn.execute("COMMIT") + for table_name in self.ALL_TABLE_NAMES: + conn.execute( + f'DROP TABLE IF EXISTS "{table_name}"') + self.metadata.create_all(engine, checkfirst=True) + self._read_metadata() + except sa.exc.SQLAlchemyError as ex: + error(f"Unable to (re)create tables in the database " + f"'{self.db_name}': {repr(ex)}") + self._engine = engine + engine = None + return True + finally: + if engine: + engine.dispose() + + def connect(self) -> None: + """ Connect to database, that is assumed to be existing """ + if self._engine: + return + engine: Any = None + try: + error_if(not self.db_dsn, + "FS downloader status database DSN was not specified") + engine = self._create_engine(self.db_dsn) + self._read_metadata() + with engine.connect(): + pass + self._engine = engine + engine = None + finally: + if engine: + engine.dispose() + + def disconnect(self) -> None: + """ Disconnect database """ + if self._engine: + self._engine.dispose() + self._engine = None + + def _read_metadata(self) -> None: + """ Reads-in metadata from an existing database """ + engine: Any = None + try: + engine = self._create_engine(self.db_dsn) + with engine.connect(): + pass + metadata = sa.MetaData() + metadata.reflect(bind=engine) + for table_name in self.ALL_TABLE_NAMES: + error_if( + table_name not in metadata.tables, + f"Table '{table_name}' not present in the database " + f"'{safe_dsn(self.db_dsn)}'") + self.metadata = metadata + except sa.exc.SQLAlchemyError as ex: + error(f"Can't connect to database " + f"'{safe_dsn(self.db_dsn)}': {repr(ex)}") + finally: + if engine is not None: + engine.dispose() + + def _execute(self, ops: List[Any]) -> Any: # sa.CursorResult + """ Execute database operations + + Arguments: + ops -- List of SqlAlchemy operations to execute + Returns resulting cursor + """ + retry = False + assert ops + while True: + if self._engine is None: + self.connect() + assert self._engine is not None + try: + with self._engine.connect() as conn: + for op in ops: + ret = conn.execute(op) + return ret + except sa.exc.SQLAlchemyError as ex: + if retry: + error(f"FS downloader status database operation " + f"failed: {repr(ex)}") + retry = True + try: + self.disconnect() + except sa.exc.SQLAlchemyError: + self._engine = None + assert self._engine is None + + def _create_engine(self, dsn) -> Any: + """ Creates synchronous SqlAlchemy engine + + Overloaded in RcacheDbAsync to create asynchronous engine + + Returns Engine object + """ + try: + return sa.create_engine(dsn) + except sa.exc.SQLAlchemyError as ex: + error(f"Invalid database DSN: '{safe_dsn(dsn)}': {ex}") + return None # Will never happen, appeasing pylint + + def write_milestone(self, milestone: DownloaderMilestone, + updated_regions: Optional[Iterable[str]] = None, + all_regions: Optional[List[str]] = None) -> None: + """ Write milestone to state database + + Arguments: + milestone -- Milestone to write + updated_regions -- List of region strings of updated regions, None for + region-inspecific milestones + all_regions -- List of all region strings, None for + region-inspecific milestones + """ + table = self.metadata.tables[self.MILESTONE_TABLE_NAME] + ops: List[Any] = [] + if all_regions is not None: + ops.append( + sa.delete(table).where(table.c.region.notin_(all_regions)). + where(table.c.milestone == milestone.name)) + timestamp = datetime.datetime.now() + ins = sa_pg.insert(table).\ + values( + [{"milestone": milestone.name, "region": region or "", + "timestamp": timestamp} + for region in (updated_regions or [None])]) + ins = ins.on_conflict_do_update( + index_elements=[c.name for c in table.c if c.primary_key], + set_={c.name: ins.excluded[c.name] + for c in table.c if not c.primary_key}) + ops.append(ins) + self._execute(ops) + + def read_milestone(self, milestone: DownloaderMilestone) \ + -> Dict[Optional[str], datetime.datetime]: + """ Read milestone information from state database + + Arguments: + milestone -- Milestone to read + Returns by-region dictionary of milestone timetags + """ + table = self.metadata.tables[self.MILESTONE_TABLE_NAME] + sel = sa.select([table.c.region, table.c.timestamp]).\ + where(table.c.milestone == milestone.name) + rp = self._execute([sel]) + return {rec.region or None: rec.timestamp for rec in rp} + + def write_alarm_reasons( + self, reasons: Optional[Dict[AlarmType, Set[str]]] = None) \ + -> None: + """ Write alarm reason to state database + + Arguments: + reasons -- Map of alarm types to set of reasons + """ + table = self.metadata.tables[self.ALARM_TABLE_NAME] + ops: List[Any] = [sa.delete(table)] + if reasons: + timestamp = datetime.datetime.now() + values: List[Dict[str, Any]] = [] + for alarm_type, alarm_reasons in reasons.items(): + values += [{"alarm_type": alarm_type.name, + "alarm_reason": alarm_reason, + "timestamp": timestamp} + for alarm_reason in alarm_reasons] + ops.append(sa.insert(table).values(values)) + self._execute(ops) + + def read_alarm_reasons(self) -> List[AlarmInfo]: + """ Read alarms information from database + + Returns set of missed milestones + """ + table = self.metadata.tables[self.ALARM_TABLE_NAME] + rp = self._execute( + [sa.select( + [table.c.alarm_type, table.c.alarm_reason, + table.c.timestamp])]) + return [AlarmInfo(alarm_type=AlarmType[rec.alarm_type], + alarm_reason=rec.alarm_reason, + timestamp=rec.timestamp) + for rec in rp] + + def write_log(self, log_type: LogType, log: str) -> None: + """ Write downloader log + + Arguments: + log_type -- Type of log being written + log -- Log text + """ + table = self.metadata.tables[self.LOG_TABLE_NAME] + ins = sa_pg.insert(table).\ + values(log_type=log_type.name, text=log, + timestamp=datetime.datetime.now()) + ins = \ + ins.on_conflict_do_update( + index_elements=["log_type"], + set_={c.name: ins.excluded[c.name] + for c in table.c if not c.primary_key}) + self._execute([ins]) + + def read_last_log(self, log_type: LogType) -> Optional[LogInfo]: + """ Read log from database + + Arguments: + log_type -- Type of log to retrieve + Returns LogInfo object + """ + table = self.metadata.tables[self.LOG_TABLE_NAME] + sel = sa.select([table.c.log_type, table.c.text, table.c.timestamp]).\ + where(table.c.log_type == log_type.name) + rp = self._execute([sel]) + row = rp.first() + return \ + LogInfo(text=row.text, timestamp=row.timestamp, + log_type=LogType[row.log_type]) if row else None + + def write_check_results( + self, check_type: CheckType, + results: Optional[Dict[str, Optional[str]]] = None) -> None: + """ Write check results + + Arguments: + check_type -- Type of check + results -- Itemized results: dicxtionary contained error message for + failed checks, None for succeeded ones + """ + table = self.metadata.tables[self.CHECKS_TABLE] + ops: List[Any] = \ + [sa.delete(table).where(table.c.check_type == check_type.name)] + if results: + timestamp = datetime.datetime.now() + ops.append( + sa.insert(table).values( + [{"check_type": check_type.name, "check_item": key, + "errmsg": value, "timestamp": timestamp} + for key, value in results.items()])) + self._execute(ops) + + def read_check_results(self) -> Dict[CheckType, List[CheckInfo]]: + """ Read check_results + Returns dictionary of CheckInfo items lists, indexed by check type + """ + table = self.metadata.tables[self.CHECKS_TABLE] + sel = sa.select([table.c.check_type, table.c.check_item, + table.c.errmsg, table.c.timestamp]) + rp = self._execute([sel]) + ret: Dict[CheckType, List[CheckInfo]] = {} + for rec in rp: + check_info = \ + CheckInfo( + check_type=CheckType[rec.check_type], + check_item=rec.check_item, errmsg=rec.errmsg, + timestamp=rec.timestamp) + ret.setdefault(check_info.check_type, []).append(check_info) + return ret diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..10462ff --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +3.8.15 \ No newline at end of file diff --git a/worker/Dockerfile b/worker/Dockerfile new file mode 100644 index 0000000..0d5b88a --- /dev/null +++ b/worker/Dockerfile @@ -0,0 +1,85 @@ +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program + +# default value of args +ARG BLD_TAG=3.8.15.0 +ARG PRINST_TAG=3.8.15.0 +ARG BLD_NAME=public.ecr.aws/w9v6y1o0/openafc/worker-al-build-image +ARG PRINST_NAME=public.ecr.aws/w9v6y1o0/openafc/worker-al-preinstall + +# Stage Build +FROM ${BLD_NAME}:${BLD_TAG} as build_image + +ARG BUILDREV=localbuild +COPY CMakeLists.txt LICENSE.txt version.txt Doxyfile.in /root/afc/ +COPY cmake /root/afc/cmake/ +COPY pkg /root/afc/pkg/ +COPY selinux /root/afc/selinux/ +COPY src /root/afc/src/ + +RUN mkdir -p -m 777 /root/afc/build +ARG BUILDREV=localbuild +RUN cd /root/afc/build && \ +cmake -DCMAKE_INSTALL_PREFIX=/root/afc/__install -DCMAKE_PREFIX_PATH=/usr -DBUILD_WITH_COVERAGE=off -DCMAKE_BUILD_TYPE=EngineRelease -DSVN_LAST_REVISION=$BUILDREV -G Ninja /root/afc && \ +ninja -j$(nproc) install +RUN cd /root/afc/src/afc-engine-preload && make && cp libaep.so /root/afc/__install/lib/ && cp parse_fs.py /root/afc/__install/bin/ +RUN strip --strip-unneeded /root/afc/__install/bin/afc-engine +RUN strip --strip-unneeded /root/afc/__install/lib/*.so +# +# Stage Install +# +FROM ${PRINST_NAME}:${PRINST_TAG} as install_image +COPY --from=build_image /root/afc/__install /usr/ + +COPY src/afc-packages /wd/afc-packages +RUN pip3 install --use-pep517 --root-user-action=ignore \ + -r /wd/afc-packages/pkgs.worker \ + && rm -rf /wd/afc-packages + +RUN mkdir -m 755 -p /var/spool/fbrat +RUN mkdir -m 755 -p /var/lib/fbrat +RUN mkdir -m 755 -p /var/celery +RUN mkdir -m 755 -p /var/run/celery +RUN mkdir -m 755 -p /var/log/celery +RUN chown -R fbrat:fbrat /var/spool/fbrat /var/lib/fbrat /var/celery + +COPY worker/docker-entrypoint.sh / +RUN chmod +x /docker-entrypoint.sh +COPY worker/afc-engine.sh /usr/bin/ +RUN chmod +x /usr/bin/afc-engine.sh + +# Development env +COPY worker/devel.sh worker/ddd.tar.gz /wd/ +ARG AFC_DEVEL_ENV=production +ENV AFC_DEVEL_ENV ${AFC_DEVEL_ENV} +# set alternative user +ARG AFC_WORKER_USER=${AFC_WORKER_USER:-root} +ARG AFC_WORKER_UID=${AFC_WORKER_UID} +RUN /wd/devel.sh +RUN rm -f /wd/devel.sh /wd/ddd.tar.gz + +FROM alpine:3.18 +COPY --from=install_image / / + +LABEL revision="afc-worker-al" +ARG AFC_WORKER_USER=${AFC_WORKER_USER:-root} +ENV AFC_WORKER_USER=${AFC_WORKER_USER} +ENV AFC_WORKER_CELERY_WORKERS=${AFC_WORKER_CELERY_WORKERS:-"rat_1"} +ENV AFC_WORKER_CELERY_LOG=${AFC_WORKER_CELERY_LOG:-WARNING} +ENV AFC_WORKER_ENG_TOUT=${AFC_WORKER_ENG_TOUT:-600} +ENV AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} + +# True to update Rcache by AFC Response sender (Worker), False to update by +# AFC REsponse receiver (MsgHandler, RatAPI) +ENV RCACHE_UPDATE_ON_SEND=True + +WORKDIR /wd +ENV PGPORT=5432 +ENV XDG_DATA_DIRS=$XDG_DATA_DIRS:/usr/local/share:/usr/share:/usr/share/fbrat:/mnt/nfs +CMD ["/docker-entrypoint.sh"] +HEALTHCHECK --interval=20s --timeout=3s --retries=3 \ + CMD for celery_worker in ${AFC_WORKER_CELERY_WORKERS} ; do \ + celery -A afc_worker status -d $celery_worker@$(hostname) || exit 1 ; done diff --git a/worker/Dockerfile.build b/worker/Dockerfile.build new file mode 100644 index 0000000..6663449 --- /dev/null +++ b/worker/Dockerfile.build @@ -0,0 +1,17 @@ +# +# Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +FROM alpine:3.18 as staging + +COPY worker/docker-entrypoint.sleep.sh / +RUN chmod +x /docker-entrypoint.sleep.sh + +RUN apk add build-base cmake samurai boost-dev \ +qt5-qtbase-dev armadillo-dev minizip-dev libbsd-dev gdal-dev + +FROM alpine:3.18 +COPY --from=staging / / +LABEL version="openafc-alpine-build-image" diff --git a/worker/Dockerfile.preinstall b/worker/Dockerfile.preinstall new file mode 100644 index 0000000..92dace4 --- /dev/null +++ b/worker/Dockerfile.preinstall @@ -0,0 +1,31 @@ +# +# Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +FROM alpine:3.18 as staging + +RUN addgroup -g 1003 fbrat +RUN adduser -g '' -D -u 1003 -G fbrat -h /var/lib/fbrat -s /sbin/nologin fbrat + +RUN apk add py3-pip py3-requests py3-pydantic=~1.10 \ +boost1.82-program_options boost1.82-log boost1.82-filesystem boost1.82-thread \ +qt5-qtbase armadillo minizip libgeotiff qt5-qtbase-sqlite libbsd gdal \ +# celery deps: +py3-click py3-click-plugins py3-wcwidth py3-six py3-tz tzdata \ +# heatmap adds +imagemagick zip + +RUN apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \ + py3-confluent-kafka + +COPY worker/requirements.txt /wd/ +RUN pip3 install --root-user-action=ignore -r /wd/requirements.txt + +FROM alpine:3.18 +COPY --from=staging / / +LABEL version="openafc-worker-preinstall-alpine" + +# for debug +CMD ["/docker-entrypoint.sleep.sh"] diff --git a/worker/afc-engine.sh b/worker/afc-engine.sh new file mode 100755 index 0000000..54f3ff9 --- /dev/null +++ b/worker/afc-engine.sh @@ -0,0 +1,2 @@ +#!/bin/sh +LD_PRELOAD=/usr/lib/libaep.so /usr/bin/afc-engine "$@" diff --git a/worker/ddd.tar.gz b/worker/ddd.tar.gz new file mode 100644 index 0000000..1b7a75a Binary files /dev/null and b/worker/ddd.tar.gz differ diff --git a/worker/devel.sh b/worker/devel.sh new file mode 100755 index 0000000..c465ee7 --- /dev/null +++ b/worker/devel.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# +# Copyright (C) 2021 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# + +if [ "$AFC_DEVEL_ENV" == "devel" ]; then + apk add build-base cmake samurai gdal-dev \ + boost-dev qt5-qtbase-dev armadillo-dev minizip-dev libbsd-dev \ + bash gdb musl-dbg musl-dev strace sudo \ + libxt-dev motif-dev xterm vim py3-numpy + # prebuilt ddd executable installed. + # steps to rebuild ddd binary from a stable release + # tar zxf ddd-3.3.12.tar.gz + # cd ddd-3.3.12 + # ./configure CXXFLAGS=-fpermissive + # make + # make install + # + cd /usr/local + tar xvfpz /wd/ddd.tar.gz + if [ x"$AFC_WORKER_USER" != x"root" ]; then + set_uid="" + if [ x"$AFC_WORKER_UID" != x"" ]; then + set_uid="-u $AFC_WORKER_UID" + fi + adduser $set_uid $AFC_WORKER_USER -G fbrat -h /home/$AFC_WORKER_USER -s /bin/bash -D + echo '%wheel ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/wheel + addgroup $AFC_WORKER_USER wheel + fi +else + apk del apk-tools libc-utils py3-pip +fi + +# Local Variables: +# mode: Python +# indent-tabs-mode: nil +# python-indent: 4 +# End: +# +# vim: sw=4:et:tw=80:cc=+1 diff --git a/worker/docker-entrypoint.sh b/worker/docker-entrypoint.sh new file mode 100755 index 0000000..1c5beae --- /dev/null +++ b/worker/docker-entrypoint.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# +# Copyright (C) 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +AFC_DEVEL_ENV=${AFC_DEVEL_ENV:-production} + +if [ -z "$AFC_WORKER_CELERY_CONCURRENCY" ]; then + export AFC_WORKER_CELERY_CONCURRENCY=$(nproc) +fi +case "$AFC_DEVEL_ENV" in + "devel") + echo "Running debug profile" + echo "AFC_WORKER_CELERY_OPTS = ${AFC_WORKER_CELERY_OPTS}" + echo "AFC_WORKER_CELERY_WORKERS = ${AFC_WORKER_CELERY_WORKERS}" + echo "AFC_WORKER_CELERY_LOG = ${AFC_WORKER_CELERY_LOG}" + echo "AFC_WORKER_ENG_TOUT = ${AFC_WORKER_ENG_TOUT}" + echo "AFC_WORKER_CELERY_CONCURRENCY = ${AFC_WORKER_CELERY_CONCURRENCY}" + ;; + "production") + echo "Running production profile" + ;; + *) + echo "Uknown profile" + ;; +esac + +if [ ! -z ${AFC_AEP_ENABLE+x} ]; then + if [ -z "$AFC_AEP_DEBUG" ]; then + export AFC_AEP_DEBUG=0 + fi + if [ -z "$AFC_AEP_LOGFILE" ]; then + export AFC_AEP_LOGFILE=/aep/log/aep.log + fi + mkdir -p $(dirname "$AFC_AEP_LOGFILE") + if [ -z "$AFC_AEP_REAL_MOUNTPOINT" ]; then + export AFC_AEP_REAL_MOUNTPOINT=/mnt/nfs/rat_transfer + fi + if [ -z "$AFC_AEP_ENGINE_MOUNTPOINT" ]; then + export AFC_AEP_ENGINE_MOUNTPOINT=$AFC_AEP_REAL_MOUNTPOINT + fi + if [ -z "$AFC_AEP_FILELIST" ]; then + export AFC_AEP_FILELIST=/aep/list/aep.list + fi + mkdir -p $(dirname "$AFC_AEP_FILELIST") + if [ -z "$AFC_AEP_CACHE_MAX_FILE_SIZE" ]; then + #50M + export AFC_AEP_CACHE_MAX_FILE_SIZE=50000000 + fi + if [ -z "$AFC_AEP_CACHE_MAX_SIZE" ]; then + #1G + export AFC_AEP_CACHE_MAX_SIZE=1000000000 + fi + if [ -z "$AFC_AEP_CACHE" ]; then + export AFC_AEP_CACHE=/aep/cache + fi + mkdir -p $AFC_AEP_CACHE + /usr/bin/parse_fs.py "$AFC_AEP_REAL_MOUNTPOINT" "$AFC_AEP_FILELIST" + export AFC_ENGINE="/usr/bin/afc-engine.sh" +else + export AFC_ENGINE="/usr/bin/afc-engine" +fi + +celery multi start $AFC_WORKER_CELERY_WORKERS $AFC_WORKER_CELERY_OPTS -A afc_worker --concurrency=$AFC_WORKER_CELERY_CONCURRENCY --pidfile=/var/run/celery/%n.pid --logfile=/proc/1/fd/2 --loglevel=$AFC_WORKER_CELERY_LOG & + +sleep infinity diff --git a/worker/docker-entrypoint.sleep.sh b/worker/docker-entrypoint.sleep.sh new file mode 100755 index 0000000..c7ff5b0 --- /dev/null +++ b/worker/docker-entrypoint.sleep.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# +# Copyright © 2022 Broadcom. All rights reserved. The term "Broadcom" +# refers solely to the Broadcom Inc. corporate affiliate that owns +# the software below. This work is licensed under the OpenAFC Project License, +# a copy of which is included with this software program +# +sleep infinity \ No newline at end of file diff --git a/worker/requirements.txt b/worker/requirements.txt new file mode 100644 index 0000000..46d2eb6 --- /dev/null +++ b/worker/requirements.txt @@ -0,0 +1,3 @@ +celery==5.2.7 +pika==1.3.2 +python_dateutil==2.8.2

    Directory listing for """ + + path_split = vpath.split("/") + url = rurl + if url[len(url) - 1] == "/": + url = url[:len(url) - 1] + i = 0 + for dir in path_split: + if i != 0: + url += "/" + dir + html += " " + "/" + dir + " " + i = i + 1 + + html += """